@flusys/ng-auth 1.0.0-rc → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
1
  import * as i1 from '@angular/common/http';
2
2
  import { HttpClient } from '@angular/common/http';
3
3
  import * as i0 from '@angular/core';
4
- import { inject, Injectable, signal, computed, effect, PLATFORM_ID, ChangeDetectionStrategy, Component, DestroyRef, input, output, makeStateKey, TransferState } from '@angular/core';
4
+ import { inject, Injectable, signal, computed, effect, PLATFORM_ID, ChangeDetectionStrategy, Component, untracked, DestroyRef, input, output, makeStateKey, TransferState } from '@angular/core';
5
5
  import { APP_CONFIG, getServiceUrl, isCompanyFeatureEnabled, isFeatureEnabled, getPermissionMode } from '@flusys/ng-core';
6
6
  import * as i2$1 from '@angular/router';
7
7
  import { Router, RouterOutlet, RouterLink, RouterLinkActive, ActivatedRoute } from '@angular/router';
8
8
  import { of, timeout, switchMap, map, tap, catchError, shareReplay, finalize, BehaviorSubject, throwError, filter, take, firstValueFrom } from 'rxjs';
9
- import { ApiResourceService, CookieService, USER_PROVIDER, COMPANY_API_PROVIDER, USER_PERMISSION_PROVIDER, AUTH_STATE_PROVIDER, AngularModule, PrimeModule, BRANCH_PERMISSIONS, HasPermissionDirective, COMPANY_PERMISSIONS, FileUrlService, PROFILE_UPLOAD_PROVIDER, PROFILE_PERMISSION_PROVIDER, FileUploaderComponent, USER_PERMISSIONS } from '@flusys/ng-shared';
9
+ import { PlatformService, ApiResourceService, CookieService, permissionGuard, USER_PERMISSIONS, COMPANY_PERMISSIONS, BRANCH_PERMISSIONS, USER_PROVIDER, COMPANY_API_PROVIDER, USER_PERMISSION_PROVIDER, AUTH_STATE_PROVIDER, PermissionValidatorService, AngularModule, PrimeModule, HasPermissionDirective, FileUrlService, PROFILE_UPLOAD_PROVIDER, PROFILE_PERMISSION_PROVIDER, FileUploaderComponent } from '@flusys/ng-shared';
10
10
  import { map as map$1, catchError as catchError$1, tap as tap$1 } from 'rxjs/operators';
11
11
  import { isPlatformBrowser, CommonModule } from '@angular/common';
12
12
  import { MessageService, ConfirmationService } from 'primeng/api';
@@ -31,42 +31,16 @@ import * as i5$2 from 'primeng/progressspinner';
31
31
  import { ProgressSpinnerModule } from 'primeng/progressspinner';
32
32
  import * as i3$2 from 'primeng/checkbox';
33
33
 
34
- // AUTH INTERFACES - Matching NestJS Auth Package DTOs
35
- /**
36
- * Type guard to check if response requires email verification
37
- */
38
34
  function isEmailVerificationRequired(response) {
39
35
  return 'requiresEmailVerification' in response && response.requiresEmailVerification === true;
40
36
  }
41
- /**
42
- * Type guard to check if response requires company selection
43
- */
44
37
  function isCompanySelectionResponse(response) {
45
38
  return 'requiresSelection' in response && response.requiresSelection === true;
46
39
  }
47
- /**
48
- * Type guard to check if response is a successful login
49
- */
50
40
  function isLoginResponse(response) {
51
41
  return 'accessToken' in response && !('requiresSelection' in response) && !('requiresEmailVerification' in response);
52
42
  }
53
43
 
54
- // =============================================================================
55
- // BRANCH INTERFACES - Matching NestJS Auth Package DTOs
56
- // =============================================================================
57
-
58
- // =============================================================================
59
- // COMPANY INTERFACES - Matching NestJS Auth Package DTOs
60
- // =============================================================================
61
-
62
- // =============================================================================
63
- // USER PERMISSION INTERFACES - Matching NestJS Auth Package DTOs
64
- // =============================================================================
65
-
66
- // =============================================================================
67
- // USER INTERFACES - Matching NestJS Auth Package DTOs
68
- // =============================================================================
69
-
70
44
  /**
71
45
  * Auth API Service
72
46
  * Handles all authentication-related API calls matching NestJS auth endpoints
@@ -196,6 +170,7 @@ class AuthStateService {
196
170
  // DI Injection
197
171
  appConfig = inject(APP_CONFIG);
198
172
  router = inject(Router);
173
+ platform = inject(PlatformService);
199
174
  // Core State (private writable, public readonly)
200
175
  _user = signal(null, ...(ngDevMode ? [{ debugName: "_user" }] : []));
201
176
  _accessToken = signal('', ...(ngDevMode ? [{ debugName: "_accessToken" }] : [])); // Memory-only, not persisted
@@ -213,7 +188,6 @@ class AuthStateService {
213
188
  _selectionExpiresAt = signal(null, ...(ngDevMode ? [{ debugName: "_selectionExpiresAt" }] : []));
214
189
  selectionSessionId = this._selectionSessionId.asReadonly();
215
190
  availableCompanies = this._availableCompanies.asReadonly();
216
- selectionExpiresAt = this._selectionExpiresAt.asReadonly();
217
191
  // Computed States
218
192
  isAuthenticated = computed(() => !!this._accessToken() && !!this._user(), ...(ngDevMode ? [{ debugName: "isAuthenticated" }] : []));
219
193
  requiresCompanySelection = computed(() => !!this._selectionSessionId() && this._availableCompanies().length > 0, ...(ngDevMode ? [{ debugName: "requiresCompanySelection" }] : []));
@@ -231,15 +205,6 @@ class AuthStateService {
231
205
  // Consider expired 30 seconds before actual expiry
232
206
  return Date.now() >= expiresAt - 30000;
233
207
  }, ...(ngDevMode ? [{ debugName: "isTokenExpired" }] : []));
234
- /**
235
- * Check if we need to refresh on app load (token expired but refresh cookie may exist)
236
- */
237
- needsRefreshOnLoad = computed(() => {
238
- const expiresAt = this._tokenExpiresAt();
239
- const hasToken = !!this._accessToken();
240
- // Need refresh if: we have stored expiry (was logged in) but no current token
241
- return expiresAt !== null && !hasToken;
242
- }, ...(ngDevMode ? [{ debugName: "needsRefreshOnLoad" }] : []));
243
208
  // Config-derived states
244
209
  tenantId = computed(() => this.appConfig.multiTenant.tenantId ?? null, ...(ngDevMode ? [{ debugName: "tenantId" }] : []));
245
210
  constructor() {
@@ -342,7 +307,7 @@ class AuthStateService {
342
307
  }
343
308
  // Storage helpers
344
309
  loadFromStorage() {
345
- if (typeof window === 'undefined')
310
+ if (this.platform.isServer)
346
311
  return;
347
312
  // Load token expiry (NOT the token - it's memory-only for security)
348
313
  const expiryStr = this.get(STORAGE.TOKEN_EXPIRY);
@@ -372,14 +337,14 @@ class AuthStateService {
372
337
  }
373
338
  }
374
339
  store(key, value) {
375
- if (typeof window !== 'undefined')
340
+ if (!this.platform.isServer)
376
341
  localStorage.setItem(key, value);
377
342
  }
378
343
  get(key) {
379
- return typeof window !== 'undefined' ? localStorage.getItem(key) : null;
344
+ return !this.platform.isServer ? localStorage.getItem(key) : null;
380
345
  }
381
346
  remove(key) {
382
- if (typeof window !== 'undefined')
347
+ if (!this.platform.isServer)
383
348
  localStorage.removeItem(key);
384
349
  }
385
350
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AuthStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
@@ -503,63 +468,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
503
468
  /**
504
469
  * Branch API Service
505
470
  * Signal-based service for branch CRUD operations
506
- * Branches default to current company context
507
- *
508
- * @example
509
- * ```typescript
510
- * branchService = inject(BranchApiService);
511
- *
512
- * // Set current company context
513
- * branchService.setCurrentCompanyId('company-uuid');
514
- *
515
- * // Access data via signals
516
- * branches = this.branchService.data;
517
- * isLoading = this.branchService.isLoading;
518
- *
519
- * // Fetch branches (will filter by current company)
520
- * this.branchService.fetchList('search');
521
- *
522
- * // Create branch (companyId will be auto-set if not provided)
523
- * await this.branchService.insertBranch({ name: 'New Branch', slug: 'new-branch' });
524
- * ```
525
471
  */
526
472
  class BranchApiService extends ApiResourceService {
527
- /** Current company context for branch operations */
528
- currentCompanyId = signal(null, ...(ngDevMode ? [{ debugName: "currentCompanyId" }] : []));
529
- /** Branches filtered by current company */
530
- companyBranches = computed(() => {
531
- const companyId = this.currentCompanyId();
532
- if (!companyId)
533
- return this.data();
534
- return this.data().filter((branch) => branch.companyId === companyId);
535
- }, ...(ngDevMode ? [{ debugName: "companyBranches" }] : []));
473
+ _currentCompanyId = signal(null, ...(ngDevMode ? [{ debugName: "_currentCompanyId" }] : []));
536
474
  constructor(http) {
537
475
  super('administration/branch', http);
538
476
  }
539
- /**
540
- * Set the current company context for branch operations
541
- */
542
477
  setCurrentCompanyId(companyId) {
543
- this.currentCompanyId.set(companyId);
544
- }
545
- /**
546
- * Insert a new branch, auto-setting companyId if not provided
547
- */
548
- async insertBranch(data) {
549
- const companyId = data.companyId ?? this.currentCompanyId();
550
- if (!companyId) {
551
- throw new Error('Company ID is required. Set currentCompanyId or provide companyId in data.');
552
- }
553
- return this.insertAsync({ ...data, companyId });
478
+ this._currentCompanyId.set(companyId);
554
479
  }
555
- /**
556
- * Fetch branches for a specific company
557
- */
558
480
  fetchByCompany(companyId, search = '') {
559
481
  this.setCurrentCompanyId(companyId);
560
- this.fetchList(search, {
561
- filter: { companyId },
562
- });
482
+ this.fetchList(search, { filter: { companyId } });
563
483
  }
564
484
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BranchApiService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
565
485
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BranchApiService, providedIn: 'root' });
@@ -572,21 +492,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
572
492
  /**
573
493
  * Company API Service
574
494
  * Signal-based service for company CRUD operations
575
- *
576
- * @example
577
- * ```typescript
578
- * companyService = inject(CompanyApiService);
579
- *
580
- * // Access data via signals
581
- * companies = this.companyService.data;
582
- * isLoading = this.companyService.isLoading;
583
- *
584
- * // Fetch companies
585
- * this.companyService.fetchList('search', { pagination: { currentPage: 0, pageSize: 10 } });
586
- *
587
- * // Create company
588
- * await this.companyService.insertAsync({ name: 'New Company', slug: 'new-company' });
589
- * ```
590
495
  */
591
496
  class CompanyApiService extends ApiResourceService {
592
497
  constructor(http) {
@@ -602,52 +507,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
602
507
 
603
508
  /**
604
509
  * Token Refresh State Service
605
- *
606
- * Manages token refresh state in a request-safe manner for SSR environments.
607
- * Replaces module-level state that would be shared across requests in SSR.
608
- *
609
- * NOTE: Uses BehaviorSubject intentionally for RxJS interoperability.
610
- * The HTTP interceptor needs to queue concurrent requests and have them
611
- * wait for the new token via Observable subscription. This is a valid
612
- * pattern that signals cannot replicate directly (no multi-subscriber
613
- * async coordination). Signal is used for the boolean flag only.
614
- *
615
- * @example
616
- * ```typescript
617
- * const refreshState = inject(TokenRefreshStateService);
618
- *
619
- * if (refreshState.isRefreshing()) {
620
- * // Queue request via refreshTokenSubject$
621
- * } else {
622
- * refreshState.startRefresh();
623
- * // Perform refresh
624
- * }
625
- * ```
510
+ * Manages token refresh state for SSR-safe request queuing.
511
+ * Uses BehaviorSubject for RxJS interop (multi-subscriber async coordination).
626
512
  */
627
513
  class TokenRefreshStateService {
628
- /** Flag to track if token refresh is in progress */
629
514
  _isRefreshing = signal(false, ...(ngDevMode ? [{ debugName: "_isRefreshing" }] : []));
630
515
  isRefreshing = this._isRefreshing.asReadonly();
631
- /** Subject to queue requests while refreshing */
632
516
  _refreshTokenSubject = new BehaviorSubject(null);
633
517
  refreshTokenSubject$ = this._refreshTokenSubject.asObservable();
634
- /**
635
- * Mark refresh as started
636
- */
637
518
  startRefresh() {
638
519
  this._isRefreshing.set(true);
639
520
  this._refreshTokenSubject.next(null);
640
521
  }
641
- /**
642
- * Mark refresh as completed and broadcast new token
643
- */
644
522
  completeRefresh(newToken) {
645
523
  this._isRefreshing.set(false);
646
524
  this._refreshTokenSubject.next(newToken);
647
525
  }
648
- /**
649
- * Mark refresh as failed
650
- */
651
526
  failRefresh() {
652
527
  this._isRefreshing.set(false);
653
528
  }
@@ -662,51 +537,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
662
537
  /**
663
538
  * User API Service
664
539
  * Signal-based service for user CRUD operations
665
- *
666
- * @example
667
- * ```typescript
668
- * userService = inject(UserApiService);
669
- *
670
- * // Access data via signals
671
- * users = this.userService.data;
672
- * isLoading = this.userService.isLoading;
673
- *
674
- * // Fetch users
675
- * this.userService.fetchList('search', { pagination: { currentPage: 0, pageSize: 10 } });
676
- *
677
- * // Create user
678
- * await this.userService.insertAsync({
679
- * name: 'John Doe',
680
- * email: 'john@example.com',
681
- * password: 'password123'
682
- * });
683
- * ```
684
540
  */
685
541
  class UserApiService extends ApiResourceService {
686
542
  constructor(http) {
687
543
  super('administration/users', http);
688
544
  }
689
- /**
690
- * Verify user's email
691
- */
692
- verifyEmail(userId) {
693
- return this.http.post(`${this.baseUrl}/${userId}/verify-email`, {});
694
- }
695
- /**
696
- * Verify user's phone
697
- */
698
- verifyPhone(userId) {
699
- return this.http.post(`${this.baseUrl}/${userId}/verify-phone`, {});
700
- }
701
- /**
702
- * Update user status (active/inactive)
703
- */
704
- updateStatus(userId, isActive) {
705
- return this.http.post(`${this.baseUrl}/${userId}/status`, { isActive });
706
- }
707
- /**
708
- * Update current user's profile
709
- */
710
545
  updateProfile(data) {
711
546
  return this.http.post(`${this.baseUrl}/profile`, data);
712
547
  }
@@ -720,314 +555,121 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
720
555
 
721
556
  /**
722
557
  * User Permission API Service
723
- *
724
558
  * Handles user-company and user-branch permission assignments.
725
- * Uses batch operations for efficient permission management.
726
- *
727
- * Backend Endpoints (POST-only RPC):
728
- * - POST /administration/permissions/user-company/assign - Batch assign/revoke company permissions
729
- * - POST /administration/permissions/get-user-company - Get user's companies
730
- * - POST /administration/permissions/user-branch/assign - Batch assign/revoke branch permissions
731
- * - POST /administration/permissions/get-user-branch - Get user's branches
732
559
  */
733
560
  class UserPermissionApiService {
734
561
  appConfig = inject(APP_CONFIG);
735
562
  http = inject(HttpClient);
736
- baseUrl;
737
- constructor() {
738
- this.baseUrl = this.appConfig.apiBaseUrl + '/administration/permissions';
739
- }
740
- // ============================================
741
- // Company Permission Endpoints
742
- // ============================================
743
- /**
744
- * Batch assign/revoke user-company permissions
745
- *
746
- * @example
747
- * // Add company permissions
748
- * service.assignUserCompanies({
749
- * userId: 'user-id',
750
- * items: [
751
- * { targetId: 'company-1', isAdd: true },
752
- * { targetId: 'company-2', isAdd: true }
753
- * ]
754
- * });
755
- *
756
- * @example
757
- * // Remove company permissions
758
- * service.assignUserCompanies({
759
- * userId: 'user-id',
760
- * items: [
761
- * { targetId: 'company-1', isAdd: false }
762
- * ]
763
- * });
764
- */
563
+ baseUrl = this.appConfig.apiBaseUrl + '/administration/permissions';
564
+ // Company Permissions
765
565
  assignUserCompanies(data) {
766
566
  return this.http.post(`${this.baseUrl}/user-company/assign`, data);
767
567
  }
768
- /**
769
- * Assign single company to user (convenience method)
770
- */
771
568
  assignUserToCompany(userId, companyId) {
772
- return this.assignUserCompanies({
773
- userId,
774
- items: [{ targetId: companyId, isAdd: true }],
775
- });
569
+ return this.assignUserCompanies({ userId, items: [{ targetId: companyId, isAdd: true }] });
776
570
  }
777
- /**
778
- * Revoke single company from user (convenience method)
779
- */
780
571
  revokeUserFromCompany(userId, companyId) {
781
- return this.assignUserCompanies({
782
- userId,
783
- items: [{ targetId: companyId, isAdd: false }],
784
- });
572
+ return this.assignUserCompanies({ userId, items: [{ targetId: companyId, isAdd: false }] });
785
573
  }
786
- /**
787
- * Get user's company permissions
788
- *
789
- * @param userId - User ID to get companies for
790
- * @returns Observable with user's company permissions
791
- */
792
574
  getUserCompanies(userId) {
793
575
  return this.http.post(`${this.baseUrl}/get-user-company`, { userId });
794
576
  }
795
- // ============================================
796
- // Branch Permission Endpoints
797
- // ============================================
798
- /**
799
- * Batch assign/revoke user-branch permissions
800
- *
801
- * @example
802
- * // Add branch permissions
803
- * service.assignUserBranches({
804
- * userId: 'user-id',
805
- * items: [
806
- * { targetId: 'branch-1', isAdd: true },
807
- * { targetId: 'branch-2', isAdd: true }
808
- * ]
809
- * });
810
- *
811
- * @example
812
- * // Remove branch permissions
813
- * service.assignUserBranches({
814
- * userId: 'user-id',
815
- * items: [
816
- * { targetId: 'branch-1', isAdd: false }
817
- * ]
818
- * });
819
- */
577
+ // Branch Permissions
820
578
  assignUserBranches(data) {
821
579
  return this.http.post(`${this.baseUrl}/user-branch/assign`, data);
822
580
  }
823
- /**
824
- * Assign single branch to user (convenience method)
825
- */
826
581
  assignUserToBranch(userId, branchId) {
827
- return this.assignUserBranches({
828
- userId,
829
- items: [{ targetId: branchId, isAdd: true }],
830
- });
582
+ return this.assignUserBranches({ userId, items: [{ targetId: branchId, isAdd: true }] });
831
583
  }
832
- /**
833
- * Revoke single branch from user (convenience method)
834
- */
835
584
  revokeUserFromBranch(userId, branchId) {
836
- return this.assignUserBranches({
585
+ return this.assignUserBranches({ userId, items: [{ targetId: branchId, isAdd: false }] });
586
+ }
587
+ getUserBranches(userId, companyId) {
588
+ return this.http.post(`${this.baseUrl}/get-user-branch`, {
837
589
  userId,
838
- items: [{ targetId: branchId, isAdd: false }],
590
+ ...(companyId && { companyId }),
839
591
  });
840
592
  }
841
- /**
842
- * Get user's branch permissions
843
- *
844
- * @param userId - User ID to get branches for
845
- * @returns Observable with user's branch permissions
846
- */
847
- getUserBranches(userId) {
848
- return this.http.post(`${this.baseUrl}/get-user-branch`, { userId });
849
- }
850
593
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserPermissionApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
851
594
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserPermissionApiService, providedIn: 'root' });
852
595
  }
853
596
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: UserPermissionApiService, decorators: [{
854
597
  type: Injectable,
855
598
  args: [{ providedIn: 'root' }]
856
- }], ctorParameters: () => [] });
599
+ }] });
857
600
 
858
601
  /**
859
602
  * Authentication API Endpoints
860
603
  *
861
604
  * Centralized endpoint definitions to prevent inconsistencies across interceptors
862
605
  * and services. All auth-related endpoints should be referenced from here.
863
- *
864
- * @example
865
- * ```typescript
866
- * if (isAuthEndpoint(req.url)) {
867
- * // Skip interceptor logic
868
- * }
869
- * ```
870
606
  */
871
607
  const AUTH_ENDPOINTS = {
872
608
  LOGIN: '/auth/login',
873
609
  LOGOUT: '/auth/logout',
874
610
  REFRESH: '/auth/refresh',
875
611
  REGISTER: '/auth/register',
612
+ ME: '/auth/me',
876
613
  SELECT: '/auth/select',
877
614
  SWITCH_COMPANY: '/auth/switch-company',
615
+ CHANGE_PASSWORD: '/auth/change-password',
878
616
  VERIFY_EMAIL: '/auth/verify-email',
879
617
  FORGOT_PASSWORD: '/auth/forgot-password',
880
618
  RESET_PASSWORD: '/auth/reset-password',
881
619
  RESEND_VERIFICATION: '/auth/resend-verification',
882
620
  };
883
- /**
884
- * Check if a URL is an auth-related endpoint
885
- */
886
- function isAuthEndpoint(url) {
887
- return Object.values(AUTH_ENDPOINTS).some((endpoint) => url.includes(endpoint));
888
- }
889
- /**
890
- * Check if a URL is a specific auth endpoint
891
- */
621
+ /** Check if a URL matches a specific endpoint */
892
622
  function isEndpoint(url, endpoint) {
893
623
  return url.includes(endpoint);
894
624
  }
895
625
 
896
- /**
897
- * Validates a return URL to prevent open redirect attacks.
898
- * Only allows relative URLs that start with '/' and don't contain protocol.
899
- */
900
626
  function isValidReturnUrl(url) {
901
627
  if (typeof url !== 'string' || !url)
902
628
  return false;
903
- // Must start with '/' and not contain protocol indicators
904
629
  return url.startsWith('/') && !url.includes('://') && !url.startsWith('//');
905
630
  }
906
- /**
907
- * Auth Guard
908
- * Protects routes that require authentication.
909
- *
910
- * Session restoration is handled by guestGuard. This guard assumes
911
- * the session has already been restored if user accessed login page first.
912
- *
913
- * Handles company feature:
914
- * - If company feature enabled and no company selected, redirects to login
915
- * (login page handles company selection as step 2)
916
- * - If not authenticated, redirects to login
917
- *
918
- * @example
919
- * ```typescript
920
- * // In routes configuration
921
- * {
922
- * path: 'dashboard',
923
- * loadComponent: () => import('./dashboard.component'),
924
- * canActivate: [authGuard]
925
- * }
926
- * ```
927
- */
631
+ /** Protects routes requiring authentication */
928
632
  const authGuard = (route, state) => {
929
633
  const authState = inject(AuthStateService);
930
634
  const router = inject(Router);
931
635
  const appConfig = inject(APP_CONFIG);
932
- // Check if user is authenticated
933
636
  if (!authState.isAuthenticated()) {
934
- // Store return URL for redirect after login
935
- return router.createUrlTree([AUTH_ENDPOINTS.LOGIN], {
936
- queryParams: { returnUrl: state.url },
937
- });
637
+ return router.createUrlTree([AUTH_ENDPOINTS.LOGIN], { queryParams: { returnUrl: state.url } });
938
638
  }
939
- // Check if company feature is enabled and company selection is required
940
639
  if (appConfig.enableCompanyFeature) {
941
- // If user needs to select company or no company selected, redirect to login
942
- // Login page handles company selection as step 2
943
640
  if (authState.requiresCompanySelection() || !authState.company()) {
944
- return router.createUrlTree([AUTH_ENDPOINTS.LOGIN], {
945
- queryParams: { returnUrl: state.url },
946
- });
641
+ return router.createUrlTree([AUTH_ENDPOINTS.LOGIN], { queryParams: { returnUrl: state.url } });
947
642
  }
948
643
  }
949
644
  return true;
950
645
  };
951
- /**
952
- * Guest Guard
953
- * Protects auth routes (login, register) from authenticated users.
954
- * Redirects authenticated users to home page.
955
- *
956
- * CRITICAL: Unlike old implementation, this guard RESTORES the session first
957
- * before checking authentication. This handles the case when users directly
958
- * navigate to /auth/login via browser URL while having valid refresh token.
959
- *
960
- * Flow:
961
- * 1. Restore session (if needed) via AuthInitService.initialize()
962
- * 2. Check if user is authenticated
963
- * 3. If authenticated -> redirect to dashboard
964
- * 4. If not authenticated -> allow access to login page
965
- *
966
- * @example
967
- * ```typescript
968
- * // In routes configuration
969
- * {
970
- * path: 'auth/login',
971
- * loadComponent: () => import('./login.component'),
972
- * canActivate: [guestGuard]
973
- * }
974
- * ```
975
- */
646
+ /** Protects auth routes from authenticated users, restores session first */
976
647
  const guestGuard = (route) => {
977
648
  const authState = inject(AuthStateService);
978
649
  const authInit = inject(AuthInitService);
979
650
  const router = inject(Router);
980
651
  const appConfig = inject(APP_CONFIG);
981
- // Helper to check and redirect authenticated users
982
652
  const checkAndRedirect = () => {
983
- // Allow access if user needs company selection (login page handles this)
984
- if (authState.requiresCompanySelection()) {
653
+ if (authState.requiresCompanySelection())
985
654
  return true;
986
- }
987
- // If authenticated, check if company selection is needed
988
655
  if (authState.isAuthenticated()) {
989
- // If company feature enabled but no company, allow access to login for selection
990
- if (appConfig.enableCompanyFeature && !authState.company()) {
656
+ if (appConfig.enableCompanyFeature && !authState.company())
991
657
  return true;
992
- }
993
- // Redirect to home or returnUrl (with open redirect protection)
994
658
  const returnUrl = route.queryParams['returnUrl'];
995
- const safeUrl = isValidReturnUrl(returnUrl) ? returnUrl : '/';
996
- return router.createUrlTree([safeUrl]);
659
+ return router.createUrlTree([isValidReturnUrl(returnUrl) ? returnUrl : '/']);
997
660
  }
998
661
  return true;
999
662
  };
1000
- // If session already restored, just check authentication
1001
- if (authInit.initialized()) {
663
+ if (authInit.initialized())
1002
664
  return checkAndRedirect();
1003
- }
1004
- // Otherwise, restore session first
1005
665
  return authInit.initialize().pipe(map$1(() => checkAndRedirect()), catchError$1(() => of(true)));
1006
666
  };
1007
- /**
1008
- * Company Feature Guard
1009
- * Protects routes that require company feature to be enabled.
1010
- * Redirects to home if company feature is disabled.
1011
- *
1012
- * Use this guard on company/branch management routes.
1013
- *
1014
- * @example
1015
- * ```typescript
1016
- * // In routes configuration
1017
- * {
1018
- * path: 'administration/company',
1019
- * loadComponent: () => import('./company-list.component'),
1020
- * canActivate: [authGuard, companyFeatureGuard]
1021
- * }
1022
- * ```
1023
- */
667
+ /** Protects routes requiring company feature to be enabled */
1024
668
  const companyFeatureGuard = () => {
1025
669
  const router = inject(Router);
1026
670
  const appConfig = inject(APP_CONFIG);
1027
- // If company feature not enabled, redirect to home
1028
- if (!appConfig.enableCompanyFeature) {
671
+ if (!appConfig.enableCompanyFeature)
1029
672
  return router.createUrlTree(['/']);
1030
- }
1031
673
  return true;
1032
674
  };
1033
675
 
@@ -1073,34 +715,7 @@ function authInterceptor(req, next) {
1073
715
  return next(req);
1074
716
  }
1075
717
 
1076
- /**
1077
- * Token Refresh Interceptor
1078
- * Handles token refresh both proactively and reactively.
1079
- *
1080
- * PROACTIVE REFRESH:
1081
- * - Checks token expiry BEFORE making requests
1082
- * - Refreshes token if expired or expiring soon (within 30 seconds)
1083
- * - Prevents unnecessary 401 failures
1084
- *
1085
- * REACTIVE REFRESH:
1086
- * - Still handles 401 errors as fallback
1087
- * - Queues concurrent requests during refresh
1088
- * - Redirects to login on refresh failure
1089
- *
1090
- * SECURITY:
1091
- * - Token kept in memory only (not localStorage)
1092
- * - Uses HTTP-only refresh token cookie
1093
- * - SSR-safe: Uses injectable service
1094
- *
1095
- * @example
1096
- * ```typescript
1097
- * // In app.config.ts
1098
- * provideHttpClient(
1099
- * withFetch(),
1100
- * withInterceptors([authInterceptor, tokenRefreshInterceptor])
1101
- * )
1102
- * ```
1103
- */
718
+ /** Token Refresh Interceptor - Proactive and reactive token refresh with request queuing */
1104
719
  function tokenRefreshInterceptor(req, next) {
1105
720
  const authState = inject(AuthStateService);
1106
721
  const authApi = inject(AuthApiService);
@@ -1132,48 +747,25 @@ function tokenRefreshInterceptor(req, next) {
1132
747
  return handleTokenRefresh(req, next, authState, authApi, messageService, isBrowser, refreshState);
1133
748
  }));
1134
749
  }
1135
- /**
1136
- * Handle token refresh - either proactively or after 401
1137
- */
1138
750
  function handleTokenRefresh(req, next, authState, authApi, messageService, isBrowser, refreshState) {
1139
- // If already refreshing, queue this request
1140
751
  if (refreshState.isRefreshing()) {
1141
- return refreshState.refreshTokenSubject$.pipe(filter((token) => token !== null), take(1), switchMap((token) => {
1142
- return next(req.clone({
1143
- setHeaders: { Authorization: `Bearer ${token}` },
1144
- }));
1145
- }));
752
+ return refreshState.refreshTokenSubject$.pipe(filter((token) => token !== null), take(1), switchMap((token) => next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }))));
1146
753
  }
1147
- // Start refresh process
1148
754
  refreshState.startRefresh();
1149
755
  return authApi.refresh().pipe(switchMap((response) => {
1150
756
  const newToken = response.accessToken;
1151
- // Update auth state (memory-only token + decoded expiry)
1152
757
  authState.setAccessToken(newToken);
1153
- // Complete refresh and notify queued requests
1154
758
  refreshState.completeRefresh(newToken);
1155
- // Continue with original request using new token
1156
- return next(req.clone({
1157
- setHeaders: { Authorization: `Bearer ${newToken}` },
1158
- }));
759
+ return next(req.clone({ setHeaders: { Authorization: `Bearer ${newToken}` } }));
1159
760
  }), catchError((refreshError) => {
1160
761
  refreshState.failRefresh();
1161
762
  return handleRefreshFailure(authState, messageService, isBrowser, refreshError);
1162
763
  }));
1163
764
  }
1164
- /**
1165
- * Handle refresh token failure - logout and redirect
1166
- */
1167
765
  function handleRefreshFailure(authState, messageService, isBrowser, error) {
1168
- // Show message
1169
766
  if (isBrowser) {
1170
- messageService.add({
1171
- severity: 'error',
1172
- summary: 'Session Expired',
1173
- detail: 'Please log in again.',
1174
- });
767
+ messageService.add({ severity: 'error', summary: 'Session Expired', detail: 'Please log in again.' });
1175
768
  }
1176
- // Reset auth state and redirect
1177
769
  authState.resetState();
1178
770
  authState.navigateToLogin();
1179
771
  return throwError(() => error);
@@ -1229,13 +821,13 @@ const AUTH_ROUTES = [
1229
821
  const ADMINISTRATION_ROUTES = [
1230
822
  {
1231
823
  path: '',
1232
- // canActivate: [authGuard, permissionGuard('admin-check')],
1233
824
  canActivate: [authGuard],
1234
825
  loadComponent: () => Promise.resolve().then(function () { return administrationPage_component; }).then((m) => m.AdministrationPageComponent),
1235
826
  children: [
1236
- // Users tab (always available)
827
+ // Users tab
1237
828
  {
1238
829
  path: 'users',
830
+ canActivate: [permissionGuard(USER_PERMISSIONS.READ)],
1239
831
  children: [
1240
832
  {
1241
833
  path: '',
@@ -1254,7 +846,7 @@ const ADMINISTRATION_ROUTES = [
1254
846
  // Company tab (requires company feature)
1255
847
  {
1256
848
  path: 'company',
1257
- canActivate: [companyFeatureGuard],
849
+ canActivate: [companyFeatureGuard, permissionGuard(COMPANY_PERMISSIONS.READ)],
1258
850
  children: [
1259
851
  {
1260
852
  path: '',
@@ -1273,7 +865,7 @@ const ADMINISTRATION_ROUTES = [
1273
865
  // Branch tab (requires company feature)
1274
866
  {
1275
867
  path: 'branch',
1276
- canActivate: [companyFeatureGuard],
868
+ canActivate: [companyFeatureGuard, permissionGuard(BRANCH_PERMISSIONS.READ)],
1277
869
  children: [
1278
870
  {
1279
871
  path: '',
@@ -1311,16 +903,21 @@ const PROFILE_ROUTES = [
1311
903
  ];
1312
904
 
1313
905
  /**
1314
- * Auth Layout State Adapter
1315
- * Bridges AuthStateService signals to ILayoutAuthState interface.
1316
- * Provides ready-to-use integration between ng-auth, ng-layout, and ng-shared provider tokens.
906
+ * Auth Layout Provider Adapters
907
+ *
908
+ * Implements provider interfaces from ng-layout using ng-auth services.
909
+ * Bridges auth state and API to layout components.
910
+ *
911
+ * @example
912
+ * // In app.config.ts
913
+ * import { provideAuthLayoutIntegration } from '@flusys/ng-auth';
914
+ * providers: [...provideAuthLayoutIntegration()]
1317
915
  */
916
+ // =============================================================================
917
+ // Layout State Adapter
918
+ // =============================================================================
1318
919
  class AuthLayoutStateAdapter {
1319
920
  authState = inject(AuthStateService);
1320
- /**
1321
- * Maps company info to company info for layout.
1322
- * Falls back to app name from config when no company is selected.
1323
- */
1324
921
  currentCompanyInfo = computed(() => {
1325
922
  const company = this.authState.company();
1326
923
  if (company) {
@@ -1333,10 +930,6 @@ class AuthLayoutStateAdapter {
1333
930
  }
1334
931
  return null;
1335
932
  }, ...(ngDevMode ? [{ debugName: "currentCompanyInfo" }] : []));
1336
- /**
1337
- * Maps branch info to branch info for layout.
1338
- * Returns null when no branch is selected.
1339
- */
1340
933
  currentBranchInfo = computed(() => {
1341
934
  const branch = this.authState.branch();
1342
935
  if (branch) {
@@ -1349,10 +942,6 @@ class AuthLayoutStateAdapter {
1349
942
  }
1350
943
  return null;
1351
944
  }, ...(ngDevMode ? [{ debugName: "currentBranchInfo" }] : []));
1352
- /**
1353
- * Maps user info from auth state to layout user info.
1354
- * Profile picture URL is managed separately by AuthLayoutSyncService via FileUrlService.
1355
- */
1356
945
  loginUserData = computed(() => {
1357
946
  const user = this.authState.user();
1358
947
  if (user) {
@@ -1371,30 +960,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1371
960
  type: Injectable,
1372
961
  args: [{ providedIn: 'root' }]
1373
962
  }] });
1374
- /**
1375
- * Auth Layout API Adapter
1376
- * Bridges AuthApiService to ILayoutAuthApi interface.
1377
- * Handles logout and navigation for layout components.
1378
- */
963
+ // =============================================================================
964
+ // Layout API Adapter
965
+ // =============================================================================
1379
966
  class AuthLayoutApiAdapter {
1380
967
  authApi = inject(AuthApiService);
1381
968
  authState = inject(AuthStateService);
1382
969
  permissionApi = inject(UserPermissionApiService);
1383
970
  router = inject(Router);
1384
- /**
1385
- * Logout the current user.
1386
- * Calls backend logout endpoint and resets local auth state.
1387
- * Auth state reset handles clearing all localStorage including company/branch.
1388
- */
1389
971
  logOut() {
1390
972
  return this.authApi.logout().pipe(tap$1(() => {
1391
- this.authState.resetState(); // Clears all auth state including localStorage
973
+ this.authState.resetState();
1392
974
  }));
1393
975
  }
1394
- /**
1395
- * Navigate to login page.
1396
- * Optionally preserves current URL as return URL.
1397
- */
1398
976
  navigateLogin(withUrl) {
1399
977
  if (withUrl) {
1400
978
  const currentUrl = this.router.url;
@@ -1404,54 +982,33 @@ class AuthLayoutApiAdapter {
1404
982
  this.authState.navigateToLogin();
1405
983
  }
1406
984
  }
1407
- /**
1408
- * Switch to a different company and branch.
1409
- * Calls backend switch endpoint, then triggers full page reload to
1410
- * refresh permissions, reset component state, and re-filter all data.
1411
- */
1412
985
  switchCompany(companyId, branchId) {
1413
986
  return this.authApi.switchCompany({ companyId, branchId }).pipe(tap$1(() => {
1414
- // Reload page to refresh permissions and all component state
1415
- // This ensures all data is filtered by new company/branch context
1416
987
  window.location.reload();
1417
988
  }));
1418
989
  }
1419
- /**
1420
- * Get list of companies the logged-in user has permissions for.
1421
- * Uses permission API to filter by user's actual company-level permissions.
1422
- */
1423
990
  getUserCompanies() {
1424
991
  const userId = this.authState.user()?.id;
1425
992
  if (!userId) {
1426
993
  return of([]);
1427
994
  }
1428
- return this.permissionApi.getUserCompanies(userId).pipe(map$1((response) => {
1429
- return (response.data || []).map((permission) => ({
1430
- id: permission.companyId,
1431
- name: permission.companyName,
1432
- slug: permission.companySlug ?? '',
1433
- imageId: '',
1434
- }));
1435
- }));
995
+ return this.permissionApi.getUserCompanies(userId).pipe(map$1((response) => (response.data || []).map((permission) => ({
996
+ id: permission.companyId,
997
+ name: permission.companyName,
998
+ slug: permission.companySlug ?? '',
999
+ imageId: '',
1000
+ }))));
1436
1001
  }
1437
- /**
1438
- * Get branches for a specific company that the logged-in user has access to.
1439
- * Fetches all user branches and filters by companyId.
1440
- */
1441
1002
  getCompanyBranches(companyId) {
1442
1003
  const userId = this.authState.user()?.id;
1443
1004
  if (!userId) {
1444
1005
  return of([]);
1445
1006
  }
1446
- return this.permissionApi.getUserBranches(userId).pipe(map$1((response) => {
1447
- return (response.data || [])
1448
- .filter((permission) => permission.companyId === companyId)
1449
- .map((permission) => ({
1450
- id: permission.branchId,
1451
- name: permission.branchName,
1452
- slug: permission.branchSlug ?? '',
1453
- }));
1454
- }));
1007
+ return this.permissionApi.getUserBranches(userId, companyId).pipe(map$1((response) => (response.data || []).map((permission) => ({
1008
+ id: permission.branchId,
1009
+ name: permission.branchName,
1010
+ slug: permission.branchSlug ?? '',
1011
+ }))));
1455
1012
  }
1456
1013
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AuthLayoutApiAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1457
1014
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AuthLayoutApiAdapter, providedIn: 'root' });
@@ -1462,25 +1019,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1462
1019
  }] });
1463
1020
 
1464
1021
  /**
1465
- * Auth User Provider Adapter
1022
+ * Auth Shared Provider Adapters
1466
1023
  *
1467
- * Implements IUserProvider interface from ng-shared using UserApiService from ng-auth.
1468
- * This allows IAM and other packages to access user data without depending on ng-auth directly.
1024
+ * Implements provider interfaces from ng-shared using ng-auth services.
1025
+ * Allows IAM, Storage, and Form-Builder packages to access auth data
1026
+ * without depending on ng-auth directly.
1469
1027
  *
1470
1028
  * @example
1471
1029
  * // In app.config.ts
1472
- * providers: [
1473
- * { provide: USER_PROVIDER, useClass: AuthUserProviderAdapter }
1474
- * ]
1475
- *
1476
- * // In IAM component
1477
- * private readonly userProvider = inject(USER_PROVIDER);
1478
- * this.userProvider.getUsers({ page: 0, pageSize: 50 }).subscribe(...);
1030
+ * import { provideAuthProviders } from '@flusys/ng-auth';
1031
+ * providers: [...provideAuthProviders()]
1479
1032
  */
1033
+ // =============================================================================
1034
+ // User Provider Adapter
1035
+ // =============================================================================
1480
1036
  class AuthUserProviderAdapter {
1481
1037
  userApi = inject(UserApiService);
1482
1038
  getUsers(filter) {
1483
- // Convert filter to API format
1484
1039
  const apiFilter = {
1485
1040
  pagination: {
1486
1041
  currentPage: filter?.page ?? 0,
@@ -1510,19 +1065,9 @@ class AuthUserProviderAdapter {
1510
1065
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AuthUserProviderAdapter, decorators: [{
1511
1066
  type: Injectable
1512
1067
  }] });
1513
-
1514
- /**
1515
- * Auth Company API Provider Adapter
1516
- *
1517
- * Implements ICompanyApiProvider interface from ng-shared using CompanyApiService.
1518
- * Allows IAM package to access company data without depending on ng-auth directly.
1519
- *
1520
- * @example
1521
- * // In app.config.ts
1522
- * providers: [
1523
- * { provide: COMPANY_API_PROVIDER, useClass: AuthCompanyApiProviderAdapter }
1524
- * ]
1525
- */
1068
+ // =============================================================================
1069
+ // Company API Provider Adapter
1070
+ // =============================================================================
1526
1071
  class AuthCompanyApiProviderAdapter {
1527
1072
  companyApi = inject(CompanyApiService);
1528
1073
  getCompanies(filter) {
@@ -1550,23 +1095,13 @@ class AuthCompanyApiProviderAdapter {
1550
1095
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AuthCompanyApiProviderAdapter, decorators: [{
1551
1096
  type: Injectable
1552
1097
  }] });
1553
-
1554
- /**
1555
- * Auth User Permission Provider Adapter
1556
- *
1557
- * Implements IUserPermissionProvider interface from ng-shared using UserPermissionApiService.
1558
- * Allows IAM package to query user permissions without depending on ng-auth directly.
1559
- *
1560
- * @example
1561
- * // In app.config.ts
1562
- * providers: [
1563
- * { provide: USER_PERMISSION_PROVIDER, useClass: AuthUserPermissionProviderAdapter }
1564
- * ]
1565
- */
1098
+ // =============================================================================
1099
+ // User Permission Provider Adapter
1100
+ // =============================================================================
1566
1101
  class AuthUserPermissionProviderAdapter {
1567
1102
  userPermissionApi = inject(UserPermissionApiService);
1568
- getUserBranchPermissions(userId) {
1569
- return this.userPermissionApi.getUserBranches(userId);
1103
+ getUserBranchPermissions(userId, companyId) {
1104
+ return this.userPermissionApi.getUserBranches(userId, companyId);
1570
1105
  }
1571
1106
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AuthUserPermissionProviderAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1572
1107
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AuthUserPermissionProviderAdapter });
@@ -1574,37 +1109,15 @@ class AuthUserPermissionProviderAdapter {
1574
1109
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AuthUserPermissionProviderAdapter, decorators: [{
1575
1110
  type: Injectable
1576
1111
  }] });
1577
-
1578
- /**
1579
- * Auth State Provider Adapter
1580
- *
1581
- * Implements IAuthStateProvider interface from ng-shared using auth services from ng-auth.
1582
- * This allows feature packages (form-builder, etc.) to check auth state without
1583
- * depending on ng-auth directly.
1584
- *
1585
- * @example
1586
- * // In app.config.ts
1587
- * providers: [
1588
- * { provide: AUTH_STATE_PROVIDER, useClass: AuthStateProviderAdapter }
1589
- * ]
1590
- *
1591
- * // In form-builder component
1592
- * private readonly authStateProvider = inject(AUTH_STATE_PROVIDER);
1593
- * const isLoggedIn = this.authStateProvider.isAuthenticated();
1594
- */
1112
+ // =============================================================================
1113
+ // Auth State Provider Adapter
1114
+ // =============================================================================
1595
1115
  class AuthStateProviderAdapter {
1596
1116
  authState = inject(AuthStateService);
1597
1117
  authInit = inject(AuthInitService);
1598
- /**
1599
- * Signal indicating if user is currently authenticated
1600
- */
1601
1118
  get isAuthenticated() {
1602
1119
  return this.authState.isAuthenticated;
1603
1120
  }
1604
- /**
1605
- * Initialize auth state (restore session from cookies/storage)
1606
- * Returns Observable<void> that completes when initialization is done
1607
- */
1608
1121
  initialize() {
1609
1122
  return this.authInit.initialize().pipe(map(() => undefined));
1610
1123
  }
@@ -1615,23 +1128,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
1615
1128
  type: Injectable
1616
1129
  }] });
1617
1130
 
1618
- /**
1619
- * Provides auth layout integration for ng-layout.
1620
- * Call this in your app.config.ts to integrate ng-auth with ng-layout.
1621
- *
1622
- * Also registers LAYOUT_AUTH_STATE since AuthLayoutStateAdapter
1623
- * implements both ILayoutAuthState and ICompanyContextProvider.
1624
- *
1625
- * @example
1626
- * ```typescript
1627
- * // In app.config.ts
1628
- * export const appConfig: ApplicationConfig = {
1629
- * providers: [
1630
- * ...provideAuthLayoutIntegration(),
1631
- * ],
1632
- * };
1633
- * ```
1634
- */
1131
+ /** Provides auth integration for ng-layout */
1635
1132
  function provideAuthLayoutIntegration() {
1636
1133
  return [
1637
1134
  { provide: LAYOUT_AUTH_STATE, useClass: AuthLayoutStateAdapter },
@@ -1639,34 +1136,12 @@ function provideAuthLayoutIntegration() {
1639
1136
  ];
1640
1137
  }
1641
1138
 
1642
- /**
1643
- * Provide Auth Provider Adapters
1644
- *
1645
- * Registers auth implementations for provider interfaces from ng-shared.
1646
- * This allows IAM and Storage packages to use auth services without direct dependencies.
1647
- *
1648
- * @example
1649
- * // In app.config.ts
1650
- * import { provideAuthProviders } from '@flusys/ng-auth';
1651
- *
1652
- * export const appConfig: ApplicationConfig = {
1653
- * providers: [
1654
- * provideAuthProviders(),
1655
- * // ... other providers
1656
- * ]
1657
- * };
1658
- *
1659
- * @returns Array of Angular providers
1660
- */
1139
+ /** Provides auth implementations for ng-shared provider interfaces */
1661
1140
  function provideAuthProviders() {
1662
1141
  return [
1663
- // User data provider (for IAM user selection)
1664
1142
  { provide: USER_PROVIDER, useClass: AuthUserProviderAdapter },
1665
- // Company API provider (for IAM company selection)
1666
1143
  { provide: COMPANY_API_PROVIDER, useClass: AuthCompanyApiProviderAdapter },
1667
- // User permission provider (for IAM permission assignment)
1668
1144
  { provide: USER_PERMISSION_PROVIDER, useClass: AuthUserPermissionProviderAdapter },
1669
- // Auth state provider (for form-builder and other packages)
1670
1145
  { provide: AUTH_STATE_PROVIDER, useClass: AuthStateProviderAdapter },
1671
1146
  ];
1672
1147
  }
@@ -1680,6 +1155,7 @@ function provideAuthProviders() {
1680
1155
  */
1681
1156
  class AdministrationPageComponent {
1682
1157
  appConfig = inject(APP_CONFIG);
1158
+ permissionValidator = inject(PermissionValidatorService);
1683
1159
  /** All available tabs */
1684
1160
  allTabs = [
1685
1161
  {
@@ -1687,12 +1163,14 @@ class AdministrationPageComponent {
1687
1163
  label: 'Users',
1688
1164
  icon: 'pi pi-users',
1689
1165
  route: 'users',
1166
+ permission: USER_PERMISSIONS.READ,
1690
1167
  },
1691
1168
  {
1692
1169
  id: 'companies',
1693
1170
  label: 'Companies',
1694
1171
  icon: 'pi pi-building',
1695
1172
  route: 'company',
1173
+ permission: COMPANY_PERMISSIONS.READ,
1696
1174
  requiresCompanyFeature: true,
1697
1175
  },
1698
1176
  {
@@ -1700,20 +1178,26 @@ class AdministrationPageComponent {
1700
1178
  label: 'Branches',
1701
1179
  icon: 'pi pi-sitemap',
1702
1180
  route: 'branch',
1181
+ permission: BRANCH_PERMISSIONS.READ,
1703
1182
  requiresCompanyFeature: true,
1704
1183
  },
1705
1184
  ];
1706
1185
  /** Check if company feature is enabled */
1707
1186
  companyFeatureEnabled = computed(() => isCompanyFeatureEnabled(this.appConfig), ...(ngDevMode ? [{ debugName: "companyFeatureEnabled" }] : []));
1708
- /** Visible tabs based on config */
1187
+ /** Visible tabs based on config and permissions */
1709
1188
  visibleTabs = computed(() => {
1710
- return this.allTabs.filter((tab) => !tab.requiresCompanyFeature || this.companyFeatureEnabled());
1189
+ return this.allTabs.filter((tab) => {
1190
+ const hasCompanyFeature = !tab.requiresCompanyFeature || this.companyFeatureEnabled();
1191
+ const hasPermission = this.permissionValidator.hasPermission(tab.permission);
1192
+ return hasCompanyFeature && hasPermission;
1193
+ });
1711
1194
  }, ...(ngDevMode ? [{ debugName: "visibleTabs" }] : []));
1712
- /** Dynamic subtitle based on visible features */
1195
+ /** Dynamic subtitle based on visible tabs */
1713
1196
  subtitle = computed(() => {
1714
- return this.companyFeatureEnabled()
1715
- ? 'Manage users, companies, and branches'
1716
- : 'Manage users';
1197
+ const tabs = this.visibleTabs();
1198
+ if (tabs.length === 0)
1199
+ return 'No permissions available';
1200
+ return `Manage ${tabs.map((t) => t.label.toLowerCase()).join(', ')}`;
1717
1201
  }, ...(ngDevMode ? [{ debugName: "subtitle" }] : []));
1718
1202
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AdministrationPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1719
1203
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: AdministrationPageComponent, isStandalone: true, selector: "lib-administration-page", ngImport: i0, template: `
@@ -1743,7 +1227,7 @@ class AdministrationPageComponent {
1743
1227
  }
1744
1228
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AdministrationPageComponent, decorators: [{
1745
1229
  type: Component,
1746
- args: [{ selector: 'lib-administration-page', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive], template: `
1230
+ args: [{ selector: 'lib-administration-page', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive], template: `
1747
1231
  <div class="p-4">
1748
1232
  <div class="mb-4">
1749
1233
  <h1 class="text-2xl font-bold m-0">Administration</h1>
@@ -1855,25 +1339,27 @@ class BranchFormComponent {
1855
1339
  const response = await this.branchService.findByIdAsync(id);
1856
1340
  if (response.success && response.data) {
1857
1341
  this.existingBranch.set(response.data);
1858
- this.formModel.set({
1859
- companyId: response.data.companyId,
1860
- name: response.data.name,
1861
- slug: response.data.slug,
1862
- email: response.data.email || '',
1863
- phone: response.data.phone || '',
1864
- address: response.data.address || '',
1865
- isActive: response.data.isActive,
1866
- });
1342
+ this.formModel.set(this.toFormModel(response.data));
1867
1343
  }
1868
1344
  }
1869
1345
  catch {
1870
- // Error toast handled by global interceptor
1871
1346
  this.router.navigate(['/administration/branch']);
1872
1347
  }
1873
1348
  finally {
1874
1349
  this.isLoading.set(false);
1875
1350
  }
1876
1351
  }
1352
+ toFormModel(branch) {
1353
+ return {
1354
+ companyId: branch.companyId,
1355
+ name: branch.name,
1356
+ slug: branch.slug,
1357
+ email: branch.email || '',
1358
+ phone: branch.phone || '',
1359
+ address: branch.address || '',
1360
+ isActive: branch.isActive,
1361
+ };
1362
+ }
1877
1363
  generateSlug() {
1878
1364
  const model = this.formModel();
1879
1365
  if (!model.slug && model.name) {
@@ -1895,63 +1381,66 @@ class BranchFormComponent {
1895
1381
  }
1896
1382
  this.isLoading.set(true);
1897
1383
  try {
1898
- const model = this.formModel();
1899
1384
  if (this.isEditMode() && this.existingBranch()) {
1900
- // Update existing branch
1901
- const updateData = {
1902
- id: this.existingBranch().id,
1903
- name: model.name,
1904
- slug: model.slug,
1905
- email: model.email || undefined,
1906
- phone: model.phone || undefined,
1907
- address: model.address || undefined,
1908
- isActive: model.isActive,
1909
- };
1910
- await this.branchService.updateAsync(updateData);
1911
- this.messageService.add({
1912
- severity: 'success',
1913
- summary: 'Success',
1914
- detail: 'Branch updated successfully.',
1915
- });
1385
+ await this.updateBranch();
1916
1386
  }
1917
1387
  else {
1918
- // Create new branch
1919
- const createData = {
1920
- companyId: model.companyId,
1921
- name: model.name,
1922
- slug: model.slug,
1923
- email: model.email || undefined,
1924
- phone: model.phone || undefined,
1925
- address: model.address || undefined,
1926
- isActive: model.isActive,
1927
- };
1928
- await this.branchService.insertAsync(createData);
1929
- this.messageService.add({
1930
- severity: 'success',
1931
- summary: 'Success',
1932
- detail: 'Branch created successfully.',
1933
- });
1388
+ await this.createBranch();
1934
1389
  }
1935
- // Navigate back to branch list with company filter
1936
- const companyId = this.existingBranch()?.companyId || model.companyId;
1937
- this.router.navigate(['/administration/branch'], {
1938
- queryParams: { companyId },
1939
- });
1390
+ this.navigateToList();
1940
1391
  }
1941
1392
  catch {
1942
- // Error toast handled by global interceptor
1393
+ // Error handled by global interceptor
1943
1394
  }
1944
1395
  finally {
1945
1396
  this.isLoading.set(false);
1946
1397
  }
1947
1398
  }
1948
- onCancel() {
1949
- const model = this.formModel();
1950
- const companyId = this.existingBranch()?.companyId || model.companyId;
1399
+ async updateBranch() {
1400
+ const model = this.formModel();
1401
+ const updateData = {
1402
+ id: this.existingBranch().id,
1403
+ name: model.name,
1404
+ slug: model.slug,
1405
+ ...this.buildOptionalFields(model),
1406
+ };
1407
+ await this.branchService.updateAsync(updateData);
1408
+ this.onSaveSuccess('Branch updated successfully.');
1409
+ }
1410
+ async createBranch() {
1411
+ const model = this.formModel();
1412
+ const createData = {
1413
+ companyId: model.companyId,
1414
+ name: model.name,
1415
+ slug: model.slug,
1416
+ ...this.buildOptionalFields(model),
1417
+ };
1418
+ await this.branchService.insertAsync(createData);
1419
+ this.onSaveSuccess('Branch created successfully.');
1420
+ }
1421
+ buildOptionalFields(model) {
1422
+ return {
1423
+ email: model.email || undefined,
1424
+ phone: model.phone || undefined,
1425
+ address: model.address || undefined,
1426
+ isActive: model.isActive,
1427
+ };
1428
+ }
1429
+ onSaveSuccess(message) {
1430
+ this.messageService.add({ severity: 'success', summary: 'Success', detail: message });
1431
+ }
1432
+ getTargetCompanyId() {
1433
+ return this.existingBranch()?.companyId || this.formModel().companyId || null;
1434
+ }
1435
+ navigateToList() {
1436
+ const companyId = this.getTargetCompanyId();
1951
1437
  this.router.navigate(['/administration/branch'], {
1952
1438
  queryParams: companyId ? { companyId } : {},
1953
1439
  });
1954
1440
  }
1441
+ onCancel() {
1442
+ this.navigateToList();
1443
+ }
1955
1444
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: BranchFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1956
1445
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: BranchFormComponent, isStandalone: true, selector: "lib-branch-form", ngImport: i0, template: `
1957
1446
  <p-card>
@@ -2070,7 +1559,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2070
1559
  type: Component,
2071
1560
  args: [{
2072
1561
  selector: 'lib-branch-form',
2073
- standalone: true,
2074
1562
  changeDetection: ChangeDetectionStrategy.OnPush,
2075
1563
  imports: [AngularModule, PrimeModule, FormField],
2076
1564
  template: `
@@ -2226,15 +1714,18 @@ class BranchListComponent {
2226
1714
  if (this.initialized)
2227
1715
  return;
2228
1716
  this.initialized = true;
2229
- // Load companies for dropdown
2230
- this.companyService.fetchList('', {
2231
- pagination: { currentPage: 0, pageSize: 100 },
1717
+ // Use untracked to avoid nested effect error when initializing resources
1718
+ untracked(() => {
1719
+ // Load companies for dropdown
1720
+ this.companyService.fetchList('', {
1721
+ pagination: { currentPage: 0, pageSize: 100 },
1722
+ });
1723
+ // Check for company ID from query params or use current company
1724
+ // p-table's onLazyLoad will handle the initial data fetch
1725
+ const queryCompanyId = params?.get('companyId') || null;
1726
+ const currentCompanyId = this.authState.company()?.id;
1727
+ this.selectedCompanyId.set(queryCompanyId || currentCompanyId || null);
2232
1728
  });
2233
- // Check for company ID from query params or use current company
2234
- // p-table's onLazyLoad will handle the initial data fetch
2235
- const queryCompanyId = params?.get('companyId') || null;
2236
- const currentCompanyId = this.authState.company()?.id;
2237
- this.selectedCompanyId.set(queryCompanyId || currentCompanyId || null);
2238
1729
  });
2239
1730
  }
2240
1731
  loadBranches() {
@@ -2396,7 +1887,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2396
1887
  type: Component,
2397
1888
  args: [{
2398
1889
  selector: 'lib-branch-list',
2399
- standalone: true,
2400
1890
  changeDetection: ChangeDetectionStrategy.OnPush,
2401
1891
  imports: [AngularModule, PrimeModule, TagModule, HasPermissionDirective],
2402
1892
  template: `
@@ -2603,44 +2093,22 @@ class CompanyFormComponent {
2603
2093
  }
2604
2094
  this.isLoading.set(true);
2605
2095
  try {
2606
- const model = this.formModel();
2607
- if (this.isEditMode() && this.existingCompany()) {
2608
- // Update existing company
2096
+ const isEdit = this.isEditMode() && this.existingCompany();
2097
+ if (isEdit) {
2609
2098
  const updateData = {
2610
2099
  id: this.existingCompany().id,
2611
- name: model.name,
2612
- slug: model.slug,
2613
- email: model.email || undefined,
2614
- phone: model.phone || undefined,
2615
- website: model.website || undefined,
2616
- address: model.address || undefined,
2617
- isActive: model.isActive,
2100
+ ...this.buildCompanyData(),
2618
2101
  };
2619
2102
  await this.companyService.updateAsync(updateData);
2620
- this.messageService.add({
2621
- severity: 'success',
2622
- summary: 'Success',
2623
- detail: 'Company updated successfully.',
2624
- });
2625
2103
  }
2626
2104
  else {
2627
- // Create new company
2628
- const createData = {
2629
- name: model.name,
2630
- slug: model.slug,
2631
- email: model.email || undefined,
2632
- phone: model.phone || undefined,
2633
- website: model.website || undefined,
2634
- address: model.address || undefined,
2635
- isActive: model.isActive,
2636
- };
2637
- await this.companyService.insertAsync(createData);
2638
- this.messageService.add({
2639
- severity: 'success',
2640
- summary: 'Success',
2641
- detail: 'Company created successfully.',
2642
- });
2105
+ await this.companyService.insertAsync(this.buildCompanyData());
2643
2106
  }
2107
+ this.messageService.add({
2108
+ severity: 'success',
2109
+ summary: 'Success',
2110
+ detail: `Company ${isEdit ? 'updated' : 'created'} successfully.`,
2111
+ });
2644
2112
  this.router.navigate(['/administration/company']);
2645
2113
  }
2646
2114
  catch {
@@ -2653,6 +2121,19 @@ class CompanyFormComponent {
2653
2121
  onCancel() {
2654
2122
  this.router.navigate(['/administration/company']);
2655
2123
  }
2124
+ /** Build company data from form model */
2125
+ buildCompanyData() {
2126
+ const model = this.formModel();
2127
+ return {
2128
+ name: model.name,
2129
+ slug: model.slug,
2130
+ email: model.email || undefined,
2131
+ phone: model.phone || undefined,
2132
+ website: model.website || undefined,
2133
+ address: model.address || undefined,
2134
+ isActive: model.isActive,
2135
+ };
2136
+ }
2656
2137
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CompanyFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2657
2138
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.3", type: CompanyFormComponent, isStandalone: true, selector: "lib-company-form", ngImport: i0, template: `
2658
2139
  <p-card>
@@ -2766,7 +2247,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
2766
2247
  type: Component,
2767
2248
  args: [{
2768
2249
  selector: 'lib-company-form',
2769
- standalone: true,
2770
2250
  changeDetection: ChangeDetectionStrategy.OnPush,
2771
2251
  imports: [AngularModule, PrimeModule, FormField],
2772
2252
  template: `
@@ -2903,11 +2383,6 @@ class CompanyListComponent {
2903
2383
  isLoading = this.companyService.isLoading;
2904
2384
  /** Total count from service */
2905
2385
  total = this.companyService.total;
2906
- loadCompanies() {
2907
- this.companyService.fetchList('', {
2908
- pagination: { currentPage: 0, pageSize: 10 },
2909
- });
2910
- }
2911
2386
  onLazyLoad(event) {
2912
2387
  const first = event.first ?? 0;
2913
2388
  const rows = event.rows ?? 10;
@@ -3044,7 +2519,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3044
2519
  type: Component,
3045
2520
  args: [{
3046
2521
  selector: 'lib-company-list',
3047
- standalone: true,
3048
2522
  changeDetection: ChangeDetectionStrategy.OnPush,
3049
2523
  imports: [AngularModule, PrimeModule, TagModule, HasPermissionDirective],
3050
2524
  template: `
@@ -3168,21 +2642,15 @@ class ForgotPasswordPageComponent {
3168
2642
  }
3169
2643
  this.isLoading.set(true);
3170
2644
  const email = this.formModel().email;
2645
+ // Always show success (security: don't reveal if email exists)
3171
2646
  this.authApi
3172
2647
  .forgotPassword(email)
3173
- .pipe(takeUntilDestroyed(this.destroyRef))
3174
- .subscribe({
3175
- next: () => {
3176
- this.isLoading.set(false);
3177
- this.submittedEmail.set(email);
3178
- this.emailSent.set(true);
3179
- },
3180
- error: () => {
3181
- this.isLoading.set(false);
3182
- this.submittedEmail.set(email);
3183
- this.emailSent.set(true);
3184
- },
3185
- });
2648
+ .pipe(takeUntilDestroyed(this.destroyRef), tap({ next: () => this.showEmailSent(email), error: () => this.showEmailSent(email) }), finalize(() => this.isLoading.set(false)))
2649
+ .subscribe();
2650
+ }
2651
+ showEmailSent(email) {
2652
+ this.submittedEmail.set(email);
2653
+ this.emailSent.set(true);
3186
2654
  }
3187
2655
  resetForm() {
3188
2656
  this.emailSent.set(false);
@@ -3273,7 +2741,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3273
2741
  type: Component,
3274
2742
  args: [{
3275
2743
  selector: 'lib-forgot-password-page',
3276
- standalone: true,
3277
2744
  imports: [AngularModule, PrimeModule, FormField, AppFloatingConfigurator],
3278
2745
  changeDetection: ChangeDetectionStrategy.OnPush,
3279
2746
  template: `
@@ -3362,12 +2829,6 @@ var forgotPasswordPage_component = /*#__PURE__*/Object.freeze({
3362
2829
  ForgotPasswordPageComponent: ForgotPasswordPageComponent
3363
2830
  });
3364
2831
 
3365
- /**
3366
- * Login Page Component
3367
- * Multi-step authentication flow:
3368
- * - Step 1: Email/Password credentials
3369
- * - Step 2: Company/Branch selection (when company feature enabled and user has multiple)
3370
- */
3371
2832
  class LoginPageComponent {
3372
2833
  destroyRef = inject(DestroyRef);
3373
2834
  appConfig = inject(APP_CONFIG);
@@ -3378,52 +2839,32 @@ class LoginPageComponent {
3378
2839
  currentStep = signal('credentials', ...(ngDevMode ? [{ debugName: "currentStep" }] : []));
3379
2840
  isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
3380
2841
  unverifiedEmail = signal('', ...(ngDevMode ? [{ debugName: "unverifiedEmail" }] : []));
3381
- // ============================================
3382
- // Login Form (Signal Forms)
3383
- // ============================================
3384
- /** Login form model */
3385
2842
  loginFormModel = signal({
3386
2843
  email: '',
3387
2844
  password: '',
3388
2845
  rememberMe: true,
3389
2846
  }, ...(ngDevMode ? [{ debugName: "loginFormModel" }] : []));
3390
- /** Login form with validation schema */
3391
2847
  loginForm = form(this.loginFormModel, (f) => {
3392
2848
  required(f.email, { message: 'Email is required' });
3393
2849
  email(f.email, { message: 'Please enter a valid email address' });
3394
2850
  required(f.password, { message: 'Password is required' });
3395
- minLength(f.password, 6, { message: 'Password must be at least 6 characters' });
2851
+ minLength(f.password, 8, { message: 'Password must be at least 8 characters' });
3396
2852
  });
3397
- /** Check if login form is valid */
3398
2853
  isLoginFormValid = computed(() => this.loginForm().valid(), ...(ngDevMode ? [{ debugName: "isLoginFormValid" }] : []));
3399
- // ============================================
3400
- // Selection Form (Signal Forms)
3401
- // ============================================
3402
- /** Selection form model */
3403
2854
  selectionFormModel = signal({
3404
2855
  companyId: '',
3405
2856
  branchId: '',
3406
2857
  rememberSelection: true,
3407
2858
  }, ...(ngDevMode ? [{ debugName: "selectionFormModel" }] : []));
3408
- /** Selection form with validation schema */
3409
2859
  selectionForm = form(this.selectionFormModel, (f) => {
3410
2860
  required(f.companyId, { message: 'Please select a company' });
3411
2861
  required(f.branchId, { message: 'Please select a branch' });
3412
2862
  });
3413
- /** Check if selection form is valid */
3414
2863
  isSelectionFormValid = computed(() => this.selectionForm().valid(), ...(ngDevMode ? [{ debugName: "isSelectionFormValid" }] : []));
3415
- /** Whether company feature is enabled */
3416
2864
  companyFeatureEnabled = computed(() => isCompanyFeatureEnabled(this.appConfig), ...(ngDevMode ? [{ debugName: "companyFeatureEnabled" }] : []));
3417
- // ============================================
3418
- // Company Selection State (Step 2)
3419
- // ============================================
3420
- /** Session ID for company selection */
3421
2865
  sessionId = null;
3422
- /** Available companies */
3423
2866
  companies = signal([], ...(ngDevMode ? [{ debugName: "companies" }] : []));
3424
- /** User's name */
3425
2867
  userName = signal('', ...(ngDevMode ? [{ debugName: "userName" }] : []));
3426
- /** Branches for selected company */
3427
2868
  branches = computed(() => {
3428
2869
  const companyId = this.selectionFormModel().companyId;
3429
2870
  if (!companyId)
@@ -3432,17 +2873,12 @@ class LoginPageComponent {
3432
2873
  return company?.branches ?? [];
3433
2874
  }, ...(ngDevMode ? [{ debugName: "branches" }] : []));
3434
2875
  constructor() {
3435
- // Effect to restore pending company selection state (e.g., on page refresh during Step 2)
3436
2876
  effect(() => {
3437
2877
  if (this.authState.requiresCompanySelection()) {
3438
2878
  this.restorePendingSelectionState();
3439
2879
  }
3440
2880
  });
3441
2881
  }
3442
- /**
3443
- * Restores pending company selection state from auth service.
3444
- * Called when user refreshes page during Step 2 of login flow.
3445
- */
3446
2882
  restorePendingSelectionState() {
3447
2883
  const availableCompanies = this.authState.availableCompanies();
3448
2884
  const sessionId = this.authState.selectionSessionId();
@@ -3455,9 +2891,6 @@ class LoginPageComponent {
3455
2891
  this.autoSelectIfSingle();
3456
2892
  }
3457
2893
  }
3458
- // ============================================
3459
- // Step 1: Login
3460
- // ============================================
3461
2894
  onSubmit() {
3462
2895
  if (!this.isLoginFormValid()) {
3463
2896
  this.messageService.add({
@@ -3502,23 +2935,22 @@ class LoginPageComponent {
3502
2935
  this.authState.navigateAfterLogin(this.returnUrl());
3503
2936
  }
3504
2937
  },
3505
- error: () => {
2938
+ error: (error) => {
3506
2939
  this.isLoading.set(false);
3507
- // Error toast handled by global interceptor
2940
+ const message = error?.error?.message || 'Login failed. Please try again.';
2941
+ this.messageService.add({
2942
+ severity: 'error',
2943
+ summary: 'Login Failed',
2944
+ detail: message,
2945
+ });
3508
2946
  },
3509
2947
  });
3510
2948
  }
3511
- // ============================================
3512
- // Step 2: Company Selection
3513
- // ============================================
3514
- /** Handle company selection change */
3515
2949
  onCompanyChange() {
3516
- // Reset branch when company changes
3517
2950
  this.selectionFormModel.update((model) => ({
3518
2951
  ...model,
3519
2952
  branchId: '',
3520
2953
  }));
3521
- // Auto-select if only one branch
3522
2954
  const branches = this.branches();
3523
2955
  if (branches.length === 1) {
3524
2956
  this.selectionFormModel.update((model) => ({
@@ -3558,7 +2990,6 @@ class LoginPageComponent {
3558
2990
  .subscribe({
3559
2991
  next: (response) => {
3560
2992
  this.isLoading.set(false);
3561
- // Response is unwrapped - LoginResponse directly
3562
2993
  this.authState.setLoginState(response);
3563
2994
  this.messageService.add({
3564
2995
  severity: 'success',
@@ -3569,7 +3000,6 @@ class LoginPageComponent {
3569
3000
  },
3570
3001
  error: () => {
3571
3002
  this.isLoading.set(false);
3572
- // Error toast handled by global interceptor
3573
3003
  },
3574
3004
  });
3575
3005
  }
@@ -3604,15 +3034,10 @@ class LoginPageComponent {
3604
3034
  },
3605
3035
  error: () => {
3606
3036
  this.isLoading.set(false);
3607
- // Error toast handled by global interceptor
3608
3037
  },
3609
3038
  });
3610
3039
  }
3611
- // ============================================
3612
- // Helpers
3613
- // ============================================
3614
3040
  autoSelectIfSingle() {
3615
- // Auto-select if only one company
3616
3041
  if (this.companies().length === 1) {
3617
3042
  this.selectionFormModel.update((model) => ({
3618
3043
  ...model,
@@ -3620,7 +3045,6 @@ class LoginPageComponent {
3620
3045
  }));
3621
3046
  this.onCompanyChange();
3622
3047
  }
3623
- // Try to restore from storage
3624
3048
  const storedCompanyId = this.authState.getStoredCompanyId();
3625
3049
  const storedBranchId = this.authState.getStoredBranchId();
3626
3050
  if (storedCompanyId) {
@@ -3845,7 +3269,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
3845
3269
  type: Component,
3846
3270
  args: [{
3847
3271
  selector: 'lib-login-page',
3848
- standalone: true,
3849
3272
  imports: [AngularModule, PrimeModule, FormField, AppFloatingConfigurator],
3850
3273
  changeDetection: ChangeDetectionStrategy.OnPush,
3851
3274
  template: `
@@ -4125,6 +3548,10 @@ class ProfilePageComponent {
4125
3548
  // Show bottom row only if at least one section is visible
4126
3549
  showBottomRow = computed(() => this.showCompanySection() || this.showPermissionsSection(), ...(ngDevMode ? [{ debugName: "showBottomRow" }] : []));
4127
3550
  // Form
3551
+ originalFormModel = signal({
3552
+ name: '',
3553
+ phone: '',
3554
+ }, ...(ngDevMode ? [{ debugName: "originalFormModel" }] : []));
4128
3555
  formModel = signal({
4129
3556
  name: '',
4130
3557
  phone: '',
@@ -4136,6 +3563,11 @@ class ProfilePageComponent {
4136
3563
  const model = this.formModel();
4137
3564
  return model.name.trim().length > 0;
4138
3565
  }, ...(ngDevMode ? [{ debugName: "isFormValid" }] : []));
3566
+ isFormDirty = computed(() => {
3567
+ const current = this.formModel();
3568
+ const original = this.originalFormModel();
3569
+ return current.name !== original.name || current.phone !== original.phone;
3570
+ }, ...(ngDevMode ? [{ debugName: "isFormDirty" }] : []));
4139
3571
  // Password form
4140
3572
  passwordModel = signal({
4141
3573
  currentPassword: '',
@@ -4196,10 +3628,12 @@ class ProfilePageComponent {
4196
3628
  loadUserData() {
4197
3629
  const user = this.authState.user();
4198
3630
  if (user) {
4199
- this.formModel.set({
3631
+ const initialValues = {
4200
3632
  name: user.name ?? '',
4201
3633
  phone: user.phone ?? '',
4202
- });
3634
+ };
3635
+ this.originalFormModel.set(initialValues);
3636
+ this.formModel.set(initialValues);
4203
3637
  // Load profile picture URL (only if storage is enabled)
4204
3638
  if (user.profilePictureId && isFeatureEnabled(this.appConfig, 'storage')) {
4205
3639
  this.fileUrlService
@@ -4316,7 +3750,7 @@ class ProfilePageComponent {
4316
3750
  };
4317
3751
  this.userService
4318
3752
  .updateProfile(updateData)
4319
- .pipe(takeUntilDestroyed(this.destroyRef))
3753
+ .pipe(takeUntilDestroyed(this.destroyRef), finalize(() => this.isLoading.set(false)))
4320
3754
  .subscribe({
4321
3755
  next: (response) => {
4322
3756
  if (response.success && response.data) {
@@ -4326,17 +3760,20 @@ class ProfilePageComponent {
4326
3760
  name: response.data.name,
4327
3761
  phone: response.data.phone ?? null,
4328
3762
  });
3763
+ // Update original form model to reset dirty state
3764
+ this.originalFormModel.set({
3765
+ name: response.data.name,
3766
+ phone: response.data.phone ?? '',
3767
+ });
4329
3768
  this.messageService.add({
4330
3769
  severity: 'success',
4331
3770
  summary: 'Success',
4332
3771
  detail: 'Profile updated successfully',
4333
3772
  });
4334
3773
  }
4335
- this.isLoading.set(false);
4336
3774
  },
4337
3775
  error: () => {
4338
3776
  // Error toast handled by global interceptor
4339
- this.isLoading.set(false);
4340
3777
  },
4341
3778
  });
4342
3779
  }
@@ -4353,7 +3790,7 @@ class ProfilePageComponent {
4353
3790
  currentPassword: model.currentPassword,
4354
3791
  newPassword: model.newPassword,
4355
3792
  })
4356
- .pipe(takeUntilDestroyed(this.destroyRef))
3793
+ .pipe(takeUntilDestroyed(this.destroyRef), finalize(() => this.isPasswordLoading.set(false)))
4357
3794
  .subscribe({
4358
3795
  next: () => {
4359
3796
  this.messageService.add({
@@ -4367,11 +3804,9 @@ class ProfilePageComponent {
4367
3804
  newPassword: '',
4368
3805
  confirmPassword: '',
4369
3806
  });
4370
- this.isPasswordLoading.set(false);
4371
3807
  },
4372
3808
  error: () => {
4373
3809
  // Error toast handled by global interceptor
4374
- this.isPasswordLoading.set(false);
4375
3810
  },
4376
3811
  });
4377
3812
  }
@@ -4463,7 +3898,7 @@ class ProfilePageComponent {
4463
3898
  icon="pi pi-check"
4464
3899
  type="submit"
4465
3900
  [loading]="isLoading()"
4466
- [disabled]="!isFormValid() || isLoading()" />
3901
+ [disabled]="!isFormValid() || !isFormDirty() || isLoading()" />
4467
3902
  </div>
4468
3903
  </form>
4469
3904
  </div>
@@ -4633,7 +4068,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4633
4068
  type: Component,
4634
4069
  args: [{
4635
4070
  selector: 'lib-profile-page',
4636
- standalone: true,
4637
4071
  imports: [AngularModule, PrimeModule, FormField, FileUploaderComponent],
4638
4072
  template: `
4639
4073
  <!-- Page Header -->
@@ -4722,7 +4156,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
4722
4156
  icon="pi pi-check"
4723
4157
  type="submit"
4724
4158
  [loading]="isLoading()"
4725
- [disabled]="!isFormValid() || isLoading()" />
4159
+ [disabled]="!isFormValid() || !isFormDirty() || isLoading()" />
4726
4160
  </div>
4727
4161
  </form>
4728
4162
  </div>
@@ -4922,7 +4356,9 @@ class RegisterPageComponent {
4922
4356
  required(f.email, { message: 'Email is required' });
4923
4357
  email(f.email, { message: 'Please enter a valid email address' });
4924
4358
  required(f.password, { message: 'Password is required' });
4925
- minLength(f.password, 6, { message: 'Password must be at least 6 characters' });
4359
+ minLength(f.password, 8, {
4360
+ message: 'Password must be at least 8 characters',
4361
+ });
4926
4362
  required(f.confirmPassword, { message: 'Please confirm your password' });
4927
4363
  });
4928
4364
  companyFormModel = signal({
@@ -4942,7 +4378,7 @@ class RegisterPageComponent {
4942
4378
  const userModel = this.userFormModel();
4943
4379
  const userValid = userModel.name.trim().length >= 2 &&
4944
4380
  userModel.email.includes('@') &&
4945
- userModel.password.length >= 6 &&
4381
+ userModel.password.length >= 8 &&
4946
4382
  userModel.confirmPassword.length > 0 &&
4947
4383
  this.passwordsMatch();
4948
4384
  if (!isCompanyFeatureEnabled(this.appConfig))
@@ -5040,7 +4476,6 @@ class RegisterPageComponent {
5040
4476
  user: response.user,
5041
4477
  company: response.company,
5042
4478
  branch: response.branch,
5043
- companyFeatureEnabled: response.companyFeatureEnabled,
5044
4479
  });
5045
4480
  this.messageService.add({
5046
4481
  severity: 'success',
@@ -5052,7 +4487,6 @@ class RegisterPageComponent {
5052
4487
  },
5053
4488
  error: () => {
5054
4489
  this.isLoading.set(false);
5055
- // Error toast handled by global interceptor
5056
4490
  },
5057
4491
  });
5058
4492
  }
@@ -5068,7 +4502,8 @@ class RegisterPageComponent {
5068
4502
  >
5069
4503
  <div
5070
4504
  class="py-8 px-4 sm:py-10 sm:px-8 md:py-12 md:px-12 w-full sm:w-[650px] max-w-[95vw] sm:max-w-[90vw]"
5071
- style="border-radius: var(--p-content-border-radius); background: var(--p-content-background)">
4505
+ style="border-radius: var(--p-content-border-radius); background: var(--p-content-background)"
4506
+ >
5072
4507
  <div class="text-center mb-6">
5073
4508
  <div class="text-3xl font-medium mb-4 text-color">
5074
4509
  Create Account
@@ -5134,7 +4569,7 @@ class RegisterPageComponent {
5134
4569
  id="password"
5135
4570
  type="password"
5136
4571
  [formField]="userForm.password"
5137
- placeholder="Min. 6 characters"
4572
+ placeholder="Min. 8 characters"
5138
4573
  class="w-full"
5139
4574
  autocomplete="new-password"
5140
4575
  />
@@ -5154,110 +4589,115 @@ class RegisterPageComponent {
5154
4589
  autocomplete="new-password"
5155
4590
  />
5156
4591
  @if (!passwordsMatch() && userFormModel().confirmPassword) {
5157
- <small class="text-danger block mt-2"
5158
- >Passwords do not match</small
5159
- >
4592
+ <small class="text-danger block mt-2"
4593
+ >Passwords do not match</small
4594
+ >
5160
4595
  }
5161
4596
  </div>
5162
4597
  </div>
5163
4598
 
5164
4599
  @if (companyFeatureEnabled()) {
5165
- <div class="border-t border-surface pt-6 mt-6">
5166
- <div class="text-center mb-6">
5167
- <div class="text-2xl font-medium mb-2 text-color">
5168
- Company Details
5169
- </div>
5170
- <span class="text-muted-color font-medium"
5171
- >Join existing or create new company</span
5172
- >
5173
- </div>
5174
-
5175
- <div class="mb-4">
5176
- <label class="p-checkbox-native" (click)="onCreateCompanyToggle()">
5177
- <input
5178
- type="checkbox"
5179
- [formField]="companyForm.createNewCompany"
5180
- />
5181
- <span>Create new company</span>
5182
- </label>
5183
- </div>
5184
-
5185
- @if (!companyFormModel().createNewCompany) {
5186
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
5187
- <div>
5188
- <label for="companySlug" class="block font-medium mb-2"
5189
- >Company Code</label
5190
- >
5191
- <input
5192
- pInputText
5193
- id="companySlug"
5194
- [formField]="companyForm.companySlug"
5195
- placeholder="Company code/slug"
5196
- class="w-full"
5197
- />
5198
- <small class="text-muted-color"
5199
- >Enter company code to join</small
4600
+ <div class="border-t border-surface pt-6 mt-6">
4601
+ <div class="text-center mb-6">
4602
+ <div class="text-2xl font-medium mb-2 text-color">
4603
+ Company Details
4604
+ </div>
4605
+ <span class="text-muted-color font-medium"
4606
+ >Join existing or create new company</span
5200
4607
  >
5201
4608
  </div>
5202
4609
 
5203
- <div>
5204
- <label for="branchSlug" class="block font-medium mb-2"
5205
- >Branch Code</label
4610
+ <div class="mb-4">
4611
+ <label
4612
+ class="p-checkbox-native"
4613
+ (click)="onCreateCompanyToggle()"
5206
4614
  >
5207
- <input
5208
- pInputText
5209
- id="branchSlug"
5210
- [formField]="companyForm.branchSlug"
5211
- placeholder="Branch code (optional)"
5212
- class="w-full"
5213
- />
4615
+ <input
4616
+ type="checkbox"
4617
+ [formField]="companyForm.createNewCompany"
4618
+ />
4619
+ <span>Create new company</span>
4620
+ </label>
5214
4621
  </div>
5215
- </div>
5216
- } @else {
5217
- <div class="mb-4">
5218
- <label for="newCompanyName" class="block font-medium mb-2"
5219
- >Company Name *</label
5220
- >
5221
- <input
5222
- pInputText
5223
- id="newCompanyName"
5224
- [formField]="companyForm.newCompanyName"
5225
- placeholder="Your company name"
5226
- class="w-full"
5227
- />
5228
- </div>
5229
4622
 
5230
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
5231
- <div>
5232
- <label for="newCompanyPhone" class="block font-medium mb-2"
5233
- >Company Phone</label
5234
- >
5235
- <input
5236
- pInputText
5237
- id="newCompanyPhone"
5238
- [formField]="companyForm.newCompanyPhone"
5239
- placeholder="Company phone"
5240
- class="w-full"
5241
- />
5242
- </div>
4623
+ @if (!companyFormModel().createNewCompany) {
4624
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
4625
+ <div>
4626
+ <label for="companySlug" class="block font-medium mb-2"
4627
+ >Company Code</label
4628
+ >
4629
+ <input
4630
+ pInputText
4631
+ id="companySlug"
4632
+ [formField]="companyForm.companySlug"
4633
+ placeholder="Company code/slug"
4634
+ class="w-full"
4635
+ />
4636
+ <small class="text-muted-color"
4637
+ >Enter company code to join</small
4638
+ >
4639
+ </div>
4640
+
4641
+ <div>
4642
+ <label for="branchSlug" class="block font-medium mb-2"
4643
+ >Branch Code</label
4644
+ >
4645
+ <input
4646
+ pInputText
4647
+ id="branchSlug"
4648
+ [formField]="companyForm.branchSlug"
4649
+ placeholder="Branch code (optional)"
4650
+ class="w-full"
4651
+ />
4652
+ </div>
4653
+ </div>
4654
+ } @else {
4655
+ <div class="mb-4">
4656
+ <label for="newCompanyName" class="block font-medium mb-2"
4657
+ >Company Name *</label
4658
+ >
4659
+ <input
4660
+ pInputText
4661
+ id="newCompanyName"
4662
+ [formField]="companyForm.newCompanyName"
4663
+ placeholder="Your company name"
4664
+ class="w-full"
4665
+ />
4666
+ </div>
5243
4667
 
5244
- <div>
5245
- <label
5246
- for="newCompanyAddress"
5247
- class="block font-medium mb-2"
5248
- >Address</label
5249
- >
5250
- <input
5251
- pInputText
5252
- id="newCompanyAddress"
5253
- [formField]="companyForm.newCompanyAddress"
5254
- placeholder="Company address"
5255
- class="w-full"
5256
- />
5257
- </div>
4668
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
4669
+ <div>
4670
+ <label
4671
+ for="newCompanyPhone"
4672
+ class="block font-medium mb-2"
4673
+ >Company Phone</label
4674
+ >
4675
+ <input
4676
+ pInputText
4677
+ id="newCompanyPhone"
4678
+ [formField]="companyForm.newCompanyPhone"
4679
+ placeholder="Company phone"
4680
+ class="w-full"
4681
+ />
4682
+ </div>
4683
+
4684
+ <div>
4685
+ <label
4686
+ for="newCompanyAddress"
4687
+ class="block font-medium mb-2"
4688
+ >Address</label
4689
+ >
4690
+ <input
4691
+ pInputText
4692
+ id="newCompanyAddress"
4693
+ [formField]="companyForm.newCompanyAddress"
4694
+ placeholder="Company address"
4695
+ class="w-full"
4696
+ />
4697
+ </div>
4698
+ </div>
4699
+ }
5258
4700
  </div>
5259
- }
5260
- </div>
5261
4701
  }
5262
4702
 
5263
4703
  <p-button
@@ -5288,7 +4728,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
5288
4728
  type: Component,
5289
4729
  args: [{
5290
4730
  selector: 'lib-register-page',
5291
- standalone: true,
5292
4731
  imports: [AngularModule, PrimeModule, FormField, AppFloatingConfigurator],
5293
4732
  changeDetection: ChangeDetectionStrategy.OnPush,
5294
4733
  template: `
@@ -5302,7 +4741,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
5302
4741
  >
5303
4742
  <div
5304
4743
  class="py-8 px-4 sm:py-10 sm:px-8 md:py-12 md:px-12 w-full sm:w-[650px] max-w-[95vw] sm:max-w-[90vw]"
5305
- style="border-radius: var(--p-content-border-radius); background: var(--p-content-background)">
4744
+ style="border-radius: var(--p-content-border-radius); background: var(--p-content-background)"
4745
+ >
5306
4746
  <div class="text-center mb-6">
5307
4747
  <div class="text-3xl font-medium mb-4 text-color">
5308
4748
  Create Account
@@ -5368,7 +4808,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
5368
4808
  id="password"
5369
4809
  type="password"
5370
4810
  [formField]="userForm.password"
5371
- placeholder="Min. 6 characters"
4811
+ placeholder="Min. 8 characters"
5372
4812
  class="w-full"
5373
4813
  autocomplete="new-password"
5374
4814
  />
@@ -5388,110 +4828,115 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
5388
4828
  autocomplete="new-password"
5389
4829
  />
5390
4830
  @if (!passwordsMatch() && userFormModel().confirmPassword) {
5391
- <small class="text-danger block mt-2"
5392
- >Passwords do not match</small
5393
- >
4831
+ <small class="text-danger block mt-2"
4832
+ >Passwords do not match</small
4833
+ >
5394
4834
  }
5395
4835
  </div>
5396
4836
  </div>
5397
4837
 
5398
4838
  @if (companyFeatureEnabled()) {
5399
- <div class="border-t border-surface pt-6 mt-6">
5400
- <div class="text-center mb-6">
5401
- <div class="text-2xl font-medium mb-2 text-color">
5402
- Company Details
5403
- </div>
5404
- <span class="text-muted-color font-medium"
5405
- >Join existing or create new company</span
5406
- >
5407
- </div>
5408
-
5409
- <div class="mb-4">
5410
- <label class="p-checkbox-native" (click)="onCreateCompanyToggle()">
5411
- <input
5412
- type="checkbox"
5413
- [formField]="companyForm.createNewCompany"
5414
- />
5415
- <span>Create new company</span>
5416
- </label>
5417
- </div>
5418
-
5419
- @if (!companyFormModel().createNewCompany) {
5420
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
5421
- <div>
5422
- <label for="companySlug" class="block font-medium mb-2"
5423
- >Company Code</label
5424
- >
5425
- <input
5426
- pInputText
5427
- id="companySlug"
5428
- [formField]="companyForm.companySlug"
5429
- placeholder="Company code/slug"
5430
- class="w-full"
5431
- />
5432
- <small class="text-muted-color"
5433
- >Enter company code to join</small
4839
+ <div class="border-t border-surface pt-6 mt-6">
4840
+ <div class="text-center mb-6">
4841
+ <div class="text-2xl font-medium mb-2 text-color">
4842
+ Company Details
4843
+ </div>
4844
+ <span class="text-muted-color font-medium"
4845
+ >Join existing or create new company</span
5434
4846
  >
5435
4847
  </div>
5436
4848
 
5437
- <div>
5438
- <label for="branchSlug" class="block font-medium mb-2"
5439
- >Branch Code</label
4849
+ <div class="mb-4">
4850
+ <label
4851
+ class="p-checkbox-native"
4852
+ (click)="onCreateCompanyToggle()"
5440
4853
  >
5441
- <input
5442
- pInputText
5443
- id="branchSlug"
5444
- [formField]="companyForm.branchSlug"
5445
- placeholder="Branch code (optional)"
5446
- class="w-full"
5447
- />
4854
+ <input
4855
+ type="checkbox"
4856
+ [formField]="companyForm.createNewCompany"
4857
+ />
4858
+ <span>Create new company</span>
4859
+ </label>
5448
4860
  </div>
5449
- </div>
5450
- } @else {
5451
- <div class="mb-4">
5452
- <label for="newCompanyName" class="block font-medium mb-2"
5453
- >Company Name *</label
5454
- >
5455
- <input
5456
- pInputText
5457
- id="newCompanyName"
5458
- [formField]="companyForm.newCompanyName"
5459
- placeholder="Your company name"
5460
- class="w-full"
5461
- />
5462
- </div>
5463
4861
 
5464
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
5465
- <div>
5466
- <label for="newCompanyPhone" class="block font-medium mb-2"
5467
- >Company Phone</label
5468
- >
5469
- <input
5470
- pInputText
5471
- id="newCompanyPhone"
5472
- [formField]="companyForm.newCompanyPhone"
5473
- placeholder="Company phone"
5474
- class="w-full"
5475
- />
5476
- </div>
4862
+ @if (!companyFormModel().createNewCompany) {
4863
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
4864
+ <div>
4865
+ <label for="companySlug" class="block font-medium mb-2"
4866
+ >Company Code</label
4867
+ >
4868
+ <input
4869
+ pInputText
4870
+ id="companySlug"
4871
+ [formField]="companyForm.companySlug"
4872
+ placeholder="Company code/slug"
4873
+ class="w-full"
4874
+ />
4875
+ <small class="text-muted-color"
4876
+ >Enter company code to join</small
4877
+ >
4878
+ </div>
4879
+
4880
+ <div>
4881
+ <label for="branchSlug" class="block font-medium mb-2"
4882
+ >Branch Code</label
4883
+ >
4884
+ <input
4885
+ pInputText
4886
+ id="branchSlug"
4887
+ [formField]="companyForm.branchSlug"
4888
+ placeholder="Branch code (optional)"
4889
+ class="w-full"
4890
+ />
4891
+ </div>
4892
+ </div>
4893
+ } @else {
4894
+ <div class="mb-4">
4895
+ <label for="newCompanyName" class="block font-medium mb-2"
4896
+ >Company Name *</label
4897
+ >
4898
+ <input
4899
+ pInputText
4900
+ id="newCompanyName"
4901
+ [formField]="companyForm.newCompanyName"
4902
+ placeholder="Your company name"
4903
+ class="w-full"
4904
+ />
4905
+ </div>
5477
4906
 
5478
- <div>
5479
- <label
5480
- for="newCompanyAddress"
5481
- class="block font-medium mb-2"
5482
- >Address</label
5483
- >
5484
- <input
5485
- pInputText
5486
- id="newCompanyAddress"
5487
- [formField]="companyForm.newCompanyAddress"
5488
- placeholder="Company address"
5489
- class="w-full"
5490
- />
5491
- </div>
4907
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
4908
+ <div>
4909
+ <label
4910
+ for="newCompanyPhone"
4911
+ class="block font-medium mb-2"
4912
+ >Company Phone</label
4913
+ >
4914
+ <input
4915
+ pInputText
4916
+ id="newCompanyPhone"
4917
+ [formField]="companyForm.newCompanyPhone"
4918
+ placeholder="Company phone"
4919
+ class="w-full"
4920
+ />
4921
+ </div>
4922
+
4923
+ <div>
4924
+ <label
4925
+ for="newCompanyAddress"
4926
+ class="block font-medium mb-2"
4927
+ >Address</label
4928
+ >
4929
+ <input
4930
+ pInputText
4931
+ id="newCompanyAddress"
4932
+ [formField]="companyForm.newCompanyAddress"
4933
+ placeholder="Company address"
4934
+ class="w-full"
4935
+ />
4936
+ </div>
4937
+ </div>
4938
+ }
5492
4939
  </div>
5493
- }
5494
- </div>
5495
4940
  }
5496
4941
 
5497
4942
  <p-button
@@ -5538,7 +4983,7 @@ class ResetPasswordPageComponent {
5538
4983
  formModel = signal({ newPassword: '', confirmPassword: '' }, ...(ngDevMode ? [{ debugName: "formModel" }] : []));
5539
4984
  resetForm = form(this.formModel, (f) => {
5540
4985
  required(f.newPassword, { message: 'New password is required' });
5541
- minLength(f.newPassword, 6, { message: 'Password must be at least 6 characters' });
4986
+ minLength(f.newPassword, 8, { message: 'Password must be at least 8 characters' });
5542
4987
  required(f.confirmPassword, { message: 'Please confirm your password' });
5543
4988
  });
5544
4989
  passwordMismatch = computed(() => {
@@ -5571,10 +5016,9 @@ class ResetPasswordPageComponent {
5571
5016
  const newPassword = this.formModel().newPassword;
5572
5017
  this.authApi
5573
5018
  .resetPassword(this.token, newPassword)
5574
- .pipe(takeUntilDestroyed(this.destroyRef))
5019
+ .pipe(takeUntilDestroyed(this.destroyRef), finalize(() => this.isLoading.set(false)))
5575
5020
  .subscribe({
5576
5021
  next: () => {
5577
- this.isLoading.set(false);
5578
5022
  this.messageService.add({
5579
5023
  severity: 'success',
5580
5024
  summary: 'Success',
@@ -5583,7 +5027,6 @@ class ResetPasswordPageComponent {
5583
5027
  this.pageState.set('success');
5584
5028
  },
5585
5029
  error: (error) => {
5586
- this.isLoading.set(false);
5587
5030
  const message = error.error?.message || '';
5588
5031
  // Show invalid-token state for expired/invalid token errors
5589
5032
  if (message.toLowerCase().includes('expired') || message.toLowerCase().includes('invalid')) {
@@ -5711,7 +5154,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
5711
5154
  type: Component,
5712
5155
  args: [{
5713
5156
  selector: 'lib-reset-password-page',
5714
- standalone: true,
5715
5157
  imports: [AngularModule, PrimeModule, FormField, AppFloatingConfigurator],
5716
5158
  changeDetection: ChangeDetectionStrategy.OnPush,
5717
5159
  template: `
@@ -5888,8 +5330,7 @@ class UserFormComponent {
5888
5330
  // Basic validation
5889
5331
  const nameValid = model.name.trim().length > 0;
5890
5332
  const emailValid = model.email.includes('@');
5891
- // Password validation - required only for new users
5892
- const passwordValid = isEdit ? true : model.password.length >= 6;
5333
+ const passwordValid = isEdit ? true : model.password.length >= 8;
5893
5334
  return nameValid && emailValid && passwordValid;
5894
5335
  }, ...(ngDevMode ? [{ debugName: "isFormValid" }] : []));
5895
5336
  constructor() {
@@ -5907,29 +5348,27 @@ class UserFormComponent {
5907
5348
  loadUser(id) {
5908
5349
  this.isLoading.set(true);
5909
5350
  this.userService.findById(id)
5910
- .pipe(takeUntilDestroyed(this.destroyRef))
5351
+ .pipe(takeUntilDestroyed(this.destroyRef), finalize(() => this.isLoading.set(false)))
5911
5352
  .subscribe({
5912
5353
  next: (response) => {
5913
5354
  if (response.success && response.data) {
5914
5355
  this.existingUser.set(response.data);
5915
- this.formModel.set({
5916
- name: response.data.name,
5917
- email: response.data.email,
5918
- password: '',
5919
- phone: response.data.phone || '',
5920
- isActive: response.data.isActive,
5921
- emailVerified: response.data.emailVerified,
5922
- });
5356
+ this.formModel.set(this.toFormModel(response.data));
5923
5357
  }
5924
- this.isLoading.set(false);
5925
- },
5926
- error: () => {
5927
- // Error toast handled by global interceptor
5928
- this.router.navigate(['/administration/users']);
5929
- this.isLoading.set(false);
5930
5358
  },
5359
+ error: () => this.router.navigate(['/administration/users']),
5931
5360
  });
5932
5361
  }
5362
+ toFormModel(user) {
5363
+ return {
5364
+ name: user.name,
5365
+ email: user.email,
5366
+ password: '',
5367
+ phone: user.phone || '',
5368
+ isActive: user.isActive,
5369
+ emailVerified: user.emailVerified,
5370
+ };
5371
+ }
5933
5372
  onSubmit() {
5934
5373
  if (!this.isFormValid()) {
5935
5374
  this.messageService.add({
@@ -5940,72 +5379,56 @@ class UserFormComponent {
5940
5379
  return;
5941
5380
  }
5942
5381
  this.isLoading.set(true);
5943
- const model = this.formModel();
5944
5382
  if (this.isEditMode() && this.existingUser()) {
5945
- // Update existing user
5946
- const updateData = {
5947
- id: this.existingUser().id,
5948
- name: model.name,
5949
- email: model.email,
5950
- phone: model.phone || undefined,
5951
- isActive: model.isActive,
5952
- emailVerified: model.emailVerified,
5953
- };
5954
- // Only include password if provided
5955
- if (model.password) {
5956
- updateData.password = model.password;
5957
- }
5958
- this.userService.update(updateData)
5959
- .pipe(takeUntilDestroyed(this.destroyRef))
5960
- .subscribe({
5961
- next: () => {
5962
- this.messageService.add({
5963
- severity: 'success',
5964
- summary: 'Success',
5965
- detail: 'User updated successfully.',
5966
- });
5967
- this.router.navigate(['/administration/users']);
5968
- this.isLoading.set(false);
5969
- },
5970
- error: () => {
5971
- // Error toast handled by global interceptor
5972
- this.isLoading.set(false);
5973
- },
5974
- });
5383
+ this.updateUser();
5975
5384
  }
5976
5385
  else {
5977
- // Create new user
5978
- const createData = {
5979
- name: model.name,
5980
- email: model.email,
5981
- password: model.password,
5982
- phone: model.phone || undefined,
5983
- isActive: model.isActive,
5984
- emailVerified: model.emailVerified,
5985
- };
5986
- this.userService.insert(createData)
5987
- .pipe(takeUntilDestroyed(this.destroyRef))
5988
- .subscribe({
5989
- next: (createResponse) => {
5990
- // Auto-assign to company/branch if company feature enabled
5991
- if (isCompanyFeatureEnabled(this.appConfig) && createResponse.success && createResponse.data) {
5992
- const userId = createResponse.data.id;
5993
- this.assignUserToCompanyAndBranch(userId);
5994
- }
5995
- this.messageService.add({
5996
- severity: 'success',
5997
- summary: 'Success',
5998
- detail: 'User created successfully.',
5999
- });
6000
- this.router.navigate(['/administration/users']);
6001
- this.isLoading.set(false);
6002
- },
6003
- error: () => {
6004
- // Error toast handled by global interceptor
6005
- this.isLoading.set(false);
6006
- },
6007
- });
5386
+ this.createUser();
5387
+ }
5388
+ }
5389
+ updateUser() {
5390
+ const model = this.formModel();
5391
+ const updateData = {
5392
+ id: this.existingUser().id,
5393
+ name: model.name,
5394
+ email: model.email,
5395
+ phone: model.phone || undefined,
5396
+ isActive: model.isActive,
5397
+ emailVerified: model.emailVerified,
5398
+ };
5399
+ if (model.password) {
5400
+ updateData.password = model.password;
6008
5401
  }
5402
+ this.userService.update(updateData)
5403
+ .pipe(takeUntilDestroyed(this.destroyRef), finalize(() => this.isLoading.set(false)))
5404
+ .subscribe({
5405
+ next: () => this.onSaveSuccess('User updated successfully.'),
5406
+ });
5407
+ }
5408
+ createUser() {
5409
+ const model = this.formModel();
5410
+ const createData = {
5411
+ name: model.name,
5412
+ email: model.email,
5413
+ password: model.password,
5414
+ phone: model.phone || undefined,
5415
+ isActive: model.isActive,
5416
+ emailVerified: model.emailVerified,
5417
+ };
5418
+ this.userService.insert(createData)
5419
+ .pipe(takeUntilDestroyed(this.destroyRef), finalize(() => this.isLoading.set(false)))
5420
+ .subscribe({
5421
+ next: (response) => {
5422
+ if (this.companyFeatureEnabled() && response.success && response.data) {
5423
+ this.assignUserToCompanyAndBranch(response.data.id);
5424
+ }
5425
+ this.onSaveSuccess('User created successfully.');
5426
+ },
5427
+ });
5428
+ }
5429
+ onSaveSuccess(message) {
5430
+ this.messageService.add({ severity: 'success', summary: 'Success', detail: message });
5431
+ this.router.navigate(['/administration/users']);
6009
5432
  }
6010
5433
  /**
6011
5434
  * Assign user to current company and branch
@@ -6144,7 +5567,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
6144
5567
  type: Component,
6145
5568
  args: [{
6146
5569
  selector: 'lib-user-form',
6147
- standalone: true,
6148
5570
  changeDetection: ChangeDetectionStrategy.OnPush,
6149
5571
  imports: [AngularModule, PrimeModule, FormField],
6150
5572
  template: `
@@ -6258,34 +5680,19 @@ var userForm_component = /*#__PURE__*/Object.freeze({
6258
5680
  UserFormComponent: UserFormComponent
6259
5681
  });
6260
5682
 
6261
- /**
6262
- * User Company Permission Dialog
6263
- * Shows all companies with checkboxes
6264
- * Initially assigned companies are checked
6265
- * Check to assign, uncheck to revoke
6266
- */
6267
5683
  class UserCompanyPermissionDialogComponent {
6268
5684
  companyApi = inject(CompanyApiService);
6269
5685
  permissionApi = inject(UserPermissionApiService);
6270
5686
  messageService = inject(MessageService);
6271
- /** Dialog visibility */
6272
5687
  visible = input.required(...(ngDevMode ? [{ debugName: "visible" }] : []));
6273
- /** User to manage permissions for */
6274
5688
  user = input.required(...(ngDevMode ? [{ debugName: "user" }] : []));
6275
- /** Close event */
6276
5689
  closed = output();
6277
- /** Refresh event after changes */
6278
5690
  permissionsChanged = output();
6279
- /** Loading state */
6280
5691
  isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
6281
- /** Saving state */
6282
5692
  isSaving = signal(false, ...(ngDevMode ? [{ debugName: "isSaving" }] : []));
6283
- /** Companies with assignment status */
6284
5693
  companies = signal([], ...(ngDevMode ? [{ debugName: "companies" }] : []));
6285
- /** Currently assigned company IDs */
6286
5694
  assignedCompanyIds = new Set();
6287
5695
  constructor() {
6288
- // Load companies and permissions when dialog becomes visible
6289
5696
  effect(() => {
6290
5697
  if (this.visible() && this.user()) {
6291
5698
  this.loadData();
@@ -6298,23 +5705,19 @@ class UserCompanyPermissionDialogComponent {
6298
5705
  return;
6299
5706
  this.isLoading.set(true);
6300
5707
  try {
6301
- // Load all companies
6302
5708
  const companiesResponse = await firstValueFrom(this.companyApi.getAll('', {
6303
5709
  pagination: { currentPage: 0, pageSize: 1000 },
6304
5710
  filter: {},
6305
5711
  select: [],
6306
5712
  sort: {},
6307
5713
  }));
6308
- // Load user's assigned companies
6309
5714
  const assignedResponse = await firstValueFrom(this.permissionApi.getUserCompanies(currentUser.id));
6310
- // Build set of assigned company IDs
6311
5715
  this.assignedCompanyIds.clear();
6312
5716
  if (assignedResponse.data) {
6313
5717
  assignedResponse.data.forEach((permission) => {
6314
5718
  this.assignedCompanyIds.add(permission.companyId);
6315
5719
  });
6316
5720
  }
6317
- // Map companies with assignment status
6318
5721
  const companiesWithStatus = (companiesResponse.data || []).map((company) => ({
6319
5722
  ...company,
6320
5723
  isAssigned: this.assignedCompanyIds.has(company.id),
@@ -6322,7 +5725,6 @@ class UserCompanyPermissionDialogComponent {
6322
5725
  this.companies.set(companiesWithStatus);
6323
5726
  }
6324
5727
  catch {
6325
- // Error toast handled by global interceptor
6326
5728
  }
6327
5729
  finally {
6328
5730
  this.isLoading.set(false);
@@ -6335,7 +5737,6 @@ class UserCompanyPermissionDialogComponent {
6335
5737
  this.isSaving.set(true);
6336
5738
  try {
6337
5739
  if (company.isAssigned) {
6338
- // Assign company
6339
5740
  const response = await firstValueFrom(this.permissionApi.assignUserToCompany(currentUser.id, company.id));
6340
5741
  if (response.success) {
6341
5742
  this.assignedCompanyIds.add(company.id);
@@ -6348,7 +5749,6 @@ class UserCompanyPermissionDialogComponent {
6348
5749
  }
6349
5750
  }
6350
5751
  else {
6351
- // Revoke company
6352
5752
  const response = await firstValueFrom(this.permissionApi.revokeUserFromCompany(currentUser.id, company.id));
6353
5753
  if (response.success) {
6354
5754
  this.assignedCompanyIds.delete(company.id);
@@ -6362,9 +5762,7 @@ class UserCompanyPermissionDialogComponent {
6362
5762
  }
6363
5763
  }
6364
5764
  catch {
6365
- // Revert checkbox on error
6366
5765
  company.isAssigned = !company.isAssigned;
6367
- // Error toast handled by global interceptor
6368
5766
  }
6369
5767
  finally {
6370
5768
  this.isSaving.set(false);
@@ -6431,7 +5829,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
6431
5829
  type: Component,
6432
5830
  args: [{
6433
5831
  selector: 'lib-user-company-permission-dialog',
6434
- standalone: true,
6435
5832
  imports: [AngularModule, PrimeModule, DialogModule, ProgressSpinnerModule],
6436
5833
  changeDetection: ChangeDetectionStrategy.OnPush,
6437
5834
  template: `
@@ -6489,76 +5886,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
6489
5886
  }]
6490
5887
  }], ctorParameters: () => [], propDecorators: { visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: true }] }], user: [{ type: i0.Input, args: [{ isSignal: true, alias: "user", required: true }] }], closed: [{ type: i0.Output, args: ["closed"] }], permissionsChanged: [{ type: i0.Output, args: ["permissionsChanged"] }] } });
6491
5888
 
6492
- /**
6493
- * User Branch Permission Dialog
6494
- *
6495
- * Workflow:
6496
- * 1. Dialog opens → Shows company dropdown with user's permitted companies
6497
- * 2. User selects a company → Branches load for that company
6498
- * 3. Branches show with checkboxes (checked = already assigned)
6499
- * 4. Check to assign, uncheck to revoke
6500
- *
6501
- * Company dropdown only shows companies the user has company-level permissions for.
6502
- * Branch permissions can only be assigned within user's permitted companies.
6503
- */
6504
5889
  class UserBranchPermissionDialogComponent {
6505
5890
  branchApi = inject(BranchApiService);
6506
5891
  permissionApi = inject(UserPermissionApiService);
6507
- authState = inject(AuthStateService);
6508
5892
  messageService = inject(MessageService);
6509
- /** Dialog visibility */
6510
5893
  visible = input.required(...(ngDevMode ? [{ debugName: "visible" }] : []));
6511
- /** User to manage permissions for */
6512
5894
  user = input.required(...(ngDevMode ? [{ debugName: "user" }] : []));
6513
- /** Close event */
6514
5895
  closed = output();
6515
- /** Refresh event after changes */
6516
5896
  permissionsChanged = output();
6517
- /** Loading state */
6518
5897
  isLoading = signal(false, ...(ngDevMode ? [{ debugName: "isLoading" }] : []));
6519
- /** Saving state */
6520
5898
  isSaving = signal(false, ...(ngDevMode ? [{ debugName: "isSaving" }] : []));
6521
- /** Branches with assignment status */
6522
5899
  branches = signal([], ...(ngDevMode ? [{ debugName: "branches" }] : []));
6523
- /** Available companies (filtered by user's company permissions) */
6524
5900
  companies = signal([], ...(ngDevMode ? [{ debugName: "companies" }] : []));
6525
- /** Selected company ID */
6526
5901
  selectedCompanyId = null;
6527
- /** Current company from auth state */
6528
- currentCompany = computed(() => this.authState.company(), ...(ngDevMode ? [{ debugName: "currentCompany" }] : []));
6529
- /** Currently assigned branch IDs */
6530
5902
  assignedBranchIds = new Set();
6531
5903
  constructor() {
6532
- // Load user's companies and branches when dialog becomes visible
6533
5904
  effect(() => {
6534
5905
  if (this.visible() && this.user()) {
6535
5906
  this.loadUserCompaniesAndData();
6536
5907
  }
6537
5908
  });
6538
5909
  }
6539
- /**
6540
- * Load user's assigned companies (does not auto-select or load branches)
6541
- */
6542
5910
  async loadUserCompaniesAndData() {
6543
5911
  const currentUser = this.user();
6544
5912
  if (!currentUser)
6545
5913
  return;
6546
5914
  try {
6547
- // Load user's company permissions
6548
5915
  const companiesResponse = await firstValueFrom(this.permissionApi.getUserCompanies(currentUser.id));
6549
- // Map to company dropdown options
6550
5916
  const userCompanies = (companiesResponse.data || []).map((permission) => ({
6551
5917
  id: permission.companyId,
6552
5918
  name: permission.companyName,
6553
5919
  }));
6554
5920
  this.companies.set(userCompanies);
6555
- // Clear selection and branches when dialog reopens
6556
5921
  this.selectedCompanyId = null;
6557
5922
  this.branches.set([]);
6558
5923
  }
6559
- catch {
6560
- // Error toast handled by global interceptor
6561
- }
5924
+ catch { }
6562
5925
  }
6563
5926
  async loadData() {
6564
5927
  const currentUser = this.user();
@@ -6567,26 +5930,21 @@ class UserBranchPermissionDialogComponent {
6567
5930
  return;
6568
5931
  this.isLoading.set(true);
6569
5932
  try {
6570
- // Load all branches for selected company
6571
5933
  const branchesResponse = await firstValueFrom(this.branchApi.getAll('', {
6572
5934
  pagination: { currentPage: 0, pageSize: 1000 },
6573
5935
  filter: { companyId },
6574
5936
  select: [],
6575
5937
  sort: {},
6576
5938
  }));
6577
- // Load user's assigned branches
6578
5939
  const assignedResponse = await firstValueFrom(this.permissionApi.getUserBranches(currentUser.id));
6579
- // Build set of assigned branch IDs (filter by selected company)
6580
5940
  this.assignedBranchIds.clear();
6581
5941
  if (assignedResponse.data) {
6582
5942
  assignedResponse.data.forEach((permission) => {
6583
- // Filter by selected company
6584
5943
  if (permission.companyId === companyId) {
6585
5944
  this.assignedBranchIds.add(permission.branchId);
6586
5945
  }
6587
5946
  });
6588
5947
  }
6589
- // Map branches with assignment status
6590
5948
  const branchesWithStatus = (branchesResponse.data || []).map((branch) => ({
6591
5949
  ...branch,
6592
5950
  isAssigned: this.assignedBranchIds.has(branch.id),
@@ -6594,7 +5952,6 @@ class UserBranchPermissionDialogComponent {
6594
5952
  this.branches.set(branchesWithStatus);
6595
5953
  }
6596
5954
  catch {
6597
- // Error toast handled by global interceptor
6598
5955
  }
6599
5956
  finally {
6600
5957
  this.isLoading.set(false);
@@ -6612,7 +5969,6 @@ class UserBranchPermissionDialogComponent {
6612
5969
  this.isSaving.set(true);
6613
5970
  try {
6614
5971
  if (branch.isAssigned) {
6615
- // Assign branch
6616
5972
  const response = await firstValueFrom(this.permissionApi.assignUserToBranch(currentUser.id, branch.id));
6617
5973
  if (response.success) {
6618
5974
  this.assignedBranchIds.add(branch.id);
@@ -6625,7 +5981,6 @@ class UserBranchPermissionDialogComponent {
6625
5981
  }
6626
5982
  }
6627
5983
  else {
6628
- // Revoke branch
6629
5984
  const response = await firstValueFrom(this.permissionApi.revokeUserFromBranch(currentUser.id, branch.id));
6630
5985
  if (response.success) {
6631
5986
  this.assignedBranchIds.delete(branch.id);
@@ -6639,9 +5994,7 @@ class UserBranchPermissionDialogComponent {
6639
5994
  }
6640
5995
  }
6641
5996
  catch {
6642
- // Revert checkbox on error
6643
5997
  branch.isAssigned = !branch.isAssigned;
6644
- // Error toast handled by global interceptor
6645
5998
  }
6646
5999
  finally {
6647
6000
  this.isSaving.set(false);
@@ -6727,7 +6080,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
6727
6080
  type: Component,
6728
6081
  args: [{
6729
6082
  selector: 'lib-user-branch-permission-dialog',
6730
- standalone: true,
6731
6083
  imports: [AngularModule, PrimeModule, DialogModule, ProgressSpinnerModule],
6732
6084
  changeDetection: ChangeDetectionStrategy.OnPush,
6733
6085
  template: `
@@ -6849,12 +6201,14 @@ class UserListComponent {
6849
6201
  this.router.navigate(['/administration/users', user.id]);
6850
6202
  }
6851
6203
  onManageCompanyPermissions(user) {
6852
- this.selectedUser.set(user);
6853
- this.showCompanyDialog.set(true);
6204
+ this.openPermissionDialog(user, this.showCompanyDialog);
6854
6205
  }
6855
6206
  onManageBranchPermissions(user) {
6207
+ this.openPermissionDialog(user, this.showBranchDialog);
6208
+ }
6209
+ openPermissionDialog(user, dialogSignal) {
6856
6210
  this.selectedUser.set(user);
6857
- this.showBranchDialog.set(true);
6211
+ dialogSignal.set(true);
6858
6212
  }
6859
6213
  onDelete(user) {
6860
6214
  this.confirmationService.confirm({
@@ -6996,7 +6350,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
6996
6350
  type: Component,
6997
6351
  args: [{
6998
6352
  selector: 'lib-user-list',
6999
- standalone: true,
7000
6353
  changeDetection: ChangeDetectionStrategy.OnPush,
7001
6354
  imports: [
7002
6355
  AngularModule,
@@ -7217,13 +6570,11 @@ class VerifyEmailPageComponent {
7217
6570
  }
7218
6571
  this.isLoading.set(true);
7219
6572
  const email = this.resendFormModel().email;
6573
+ // Always show success (security: don't reveal if email exists)
7220
6574
  this.authApi
7221
6575
  .resendVerification(email)
7222
- .pipe(takeUntilDestroyed(this.destroyRef))
7223
- .subscribe({
7224
- next: () => this.handleResendComplete(email),
7225
- error: () => this.handleResendComplete(email),
7226
- });
6576
+ .pipe(takeUntilDestroyed(this.destroyRef), finalize(() => this.handleResendComplete(email)))
6577
+ .subscribe();
7227
6578
  }
7228
6579
  handleResendComplete(email) {
7229
6580
  this.isLoading.set(false);
@@ -7366,7 +6717,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
7366
6717
  type: Component,
7367
6718
  args: [{
7368
6719
  selector: 'lib-verify-email-page',
7369
- standalone: true,
7370
6720
  imports: [AngularModule, PrimeModule, FormField, AppFloatingConfigurator],
7371
6721
  changeDetection: ChangeDetectionStrategy.OnPush,
7372
6722
  template: `
@@ -7511,5 +6861,5 @@ var verifyEmailPage_component = /*#__PURE__*/Object.freeze({
7511
6861
  * Generated bundle index. Do not edit.
7512
6862
  */
7513
6863
 
7514
- export { ADMINISTRATION_ROUTES, AUTH_ENDPOINTS, AUTH_ROUTES, AdministrationPageComponent, AuthApiService, AuthCompanyApiProviderAdapter, AuthInitService, AuthLayoutApiAdapter, AuthLayoutStateAdapter, AuthStateProviderAdapter, AuthStateService, AuthUserPermissionProviderAdapter, AuthUserProviderAdapter, BranchApiService, BranchFormComponent, BranchListComponent, CompanyApiService, CompanyFormComponent, CompanyListComponent, ForgotPasswordPageComponent, LoginPageComponent, PROFILE_ROUTES, ProfilePageComponent, RegisterPageComponent, ResetPasswordPageComponent, TokenRefreshStateService, UserApiService, UserBranchPermissionDialogComponent, UserCompanyPermissionDialogComponent, UserFormComponent, UserListComponent, UserPermissionApiService, VerifyEmailPageComponent, authGuard, authInterceptor, companyFeatureGuard, guestGuard, isAuthEndpoint, isCompanySelectionResponse, isEmailVerificationRequired, isEndpoint, isLoginResponse, provideAuthLayoutIntegration, provideAuthProviders, tokenRefreshInterceptor };
6864
+ export { ADMINISTRATION_ROUTES, AUTH_ENDPOINTS, AUTH_ROUTES, AdministrationPageComponent, AuthApiService, AuthCompanyApiProviderAdapter, AuthInitService, AuthLayoutApiAdapter, AuthLayoutStateAdapter, AuthStateProviderAdapter, AuthStateService, AuthUserPermissionProviderAdapter, AuthUserProviderAdapter, BranchApiService, BranchFormComponent, BranchListComponent, CompanyApiService, CompanyFormComponent, CompanyListComponent, ForgotPasswordPageComponent, LoginPageComponent, PROFILE_ROUTES, ProfilePageComponent, RegisterPageComponent, ResetPasswordPageComponent, TokenRefreshStateService, UserApiService, UserBranchPermissionDialogComponent, UserCompanyPermissionDialogComponent, UserFormComponent, UserListComponent, UserPermissionApiService, VerifyEmailPageComponent, authGuard, authInterceptor, companyFeatureGuard, guestGuard, isCompanySelectionResponse, isEmailVerificationRequired, isEndpoint, isLoginResponse, provideAuthLayoutIntegration, provideAuthProviders, tokenRefreshInterceptor };
7515
6865
  //# sourceMappingURL=flusys-ng-auth.mjs.map