import { Component, OnInit, OnDestroy, ChangeDetectorRef, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { SelectItem } from 'primeng/api'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; import { of } from 'rxjs'; import { catchError, finalize, take } from 'rxjs/operators'; import { User, PartnerSystemUser, SatlocIntegration } from '../models/user.model'; import { PartnerService } from '@app/partners/services/partner.service'; import { Partner } from '@app/partners/models/partner.model'; import * as userActions from '../actions/account.actions'; import * as fromUsers from '../reducers'; import { RoleIds, Roles, globals, OperationalStatus, Labels, KnownPartnerCodes } from '@app/shared/global'; import { BaseComp } from '@app/shared/base/base.component'; import { ConstraintMessageComponent } from '@app/shared/constraint-message/constraint-message.component'; import { AccountEditorComponent } from '@app/shared/account-editor/account-editor.component'; import { PartnerUtilsService } from '@app/shared/services/partner-utils.service'; import { handlePartnerErr } from '@app/profile/common'; // Partner Integration Constants export const VENDOR_SYSTEM_FIELD = 'vendorSystem'; export const SATLOC_VENDOR = KnownPartnerCodes.SATLOC; @Component({ selector: 'agm-account-edit', templateUrl: './account-edit.component.html', styleUrls: ['./account-edit.component.css'] }) export class AccountEditComponent extends BaseComp implements OnInit, OnDestroy { readonly globals = globals; readonly Labels = Labels; readonly VENDOR_SYSTEM_FIELD = VENDOR_SYSTEM_FIELD; readonly SATLOC_VENDOR = SATLOC_VENDOR; form: FormGroup; selectedItem: User; kinds: SelectItem[]; // ============================================================================ // VIEW CHILDREN // ============================================================================ @ViewChild('accountTypeConstraint') accountTypeConstraint: ConstraintMessageComponent; @ViewChild('vendorSystemConstraint') vendorSystemConstraint: ConstraintMessageComponent; @ViewChild('accountEditor') accountEditor: AccountEditorComponent; // ============================================================================ // PARTNER INTEGRATION PROPERTIES // ============================================================================ // Partner system integration state showVendorOptions: boolean = false; selectedVendor: string = ''; // Partner system user data for dual-model approach partnerSystemUser: PartnerSystemUser | null = null; existingPartnerSystemUsers: PartnerSystemUser[] = []; // Dynamic partner loading from API partners: Partner[] = []; vendorsLoading: boolean = false; vendorOptions: SelectItem[] = [ { label: $localize`:Select vendor dropdown option@@selectVendor:Select Partner System`, value: '' } ]; availableVendorOptions: SelectItem[] = []; // Satloc integration properties satlocLoading: boolean = false; satlocError: string | null = null; satlocIntegration: SatlocIntegration = { enabled: false, status: OperationalStatus.ERROR, account_info: null, credentials_stored: false, last_error: null }; // Save before test dialog state showSaveBeforeTestDialog: boolean = false; pendingTestAfterSave: boolean = false; // Credential change tracking credentialsChanged: boolean = false; originalUsername: string = ''; originalPassword: string = ''; // Vendor change tracking for soft-lock confirmation originalVendorSystem: string = ''; // Account Does Not Exist flow tracking isAccountDoesNotExistFlow: boolean = false; // Return navigation properties for vehicle-edit flow returnTo: string | null = null; vehicleId: string | null = null; partnerId: string | null = null; partnerCode: string | null = null; customerId: string | null = null; // Post-save validation state postSaveValidationInProgress: boolean = false; postSaveValidationSuccess: boolean = false; postSaveValidationError: boolean = false; postSaveErrorMessage: string | null = null; private _account: User | PartnerSystemUser; private _isNew: boolean; get account(): User | PartnerSystemUser { return this._account; } set account(account: User | PartnerSystemUser) { this._account = account; this.selectedItem = Object.assign({}, account); const isPartnerSystemUser = this.isPartnerSystemUser(account); const isPartnerAccount = account.kind === RoleIds.PARTNER; const accountType = account.kind; this.form.patchValue({ profile: this.selectedItem, account: { active: this.selectedItem.active, username: this.selectedItem.username, password: this.selectedItem.password }, kind: accountType, [VENDOR_SYSTEM_FIELD]: this.getVendorFromAccount(account) }); if (this.isAccountDoesNotExistFlow && this.isNew) { const partnerLabel = this.partnerCode || (this.partnerId ? this.getPartnerLabelFromId(this.partnerId) : null); if (partnerLabel) { const matchingVendor = this.vendorOptions.find(vendor => vendor.label.toLowerCase() === partnerLabel.toLowerCase() ); if (matchingVendor) { this.selectedVendor = matchingVendor.label; this.form.patchValue({ [VENDOR_SYSTEM_FIELD]: matchingVendor.value }); } else { this.selectedVendor = partnerLabel; this.form.patchValue({ [VENDOR_SYSTEM_FIELD]: partnerLabel }); } } } this.showVendorOptions = isPartnerAccount || isPartnerSystemUser || this.isAccountDoesNotExistFlow; this.selectedVendor = this.getVendorFromAccount(account); if (this.isPartnerSystemUser(account)) { const partnerSystemUser = account as PartnerSystemUser; // Use getVendorFromAccount which now prefers partner ObjectId → partnerCode const vendorType = this.getVendorFromAccount(account) || SATLOC_VENDOR; this.selectedVendor = vendorType; if (this.partnerUtils.isSatlocPartner(vendorType)) { this.satlocIntegration = { enabled: true, status: partnerSystemUser.syncStatus === OperationalStatus.ACTIVE ? OperationalStatus.ACTIVE : OperationalStatus.ERROR, account_info: null, credentials_stored: true, last_error: partnerSystemUser.syncStatus === OperationalStatus.ERROR ? $localize`:Connection error message@@connectionError:Connection error` : null }; } } if (isPartnerAccount) { const vendorType = this.getVendorFromAccount(account); this.selectedVendor = vendorType; if (this.partnerUtils.isSatlocPartner(vendorType)) { this.satlocIntegration = { enabled: true, status: OperationalStatus.ERROR, account_info: null, credentials_stored: true, last_error: null }; } } this.updateAvailableVendorOptions(); // Store original vendor for soft-lock confirmation (WI-1) if (!this.isNew) { this.originalVendorSystem = this.selectedVendor; } if (isPartnerAccount || isPartnerSystemUser) { this.form.get(this.VENDOR_SYSTEM_FIELD)?.setValidators([Validators.required]); this.form.get(this.VENDOR_SYSTEM_FIELD)?.updateValueAndValidity(); } // WI-1: Soft lock - Only disable for special flow, enable for existing accounts if (!this.isNew || (this.isAccountDoesNotExistFlow && this.isNew)) { this.form.get('kind')?.disable(); // Only disable vendor for special flow, not for regular existing accounts if (this.isAccountDoesNotExistFlow) { this.form.get(this.VENDOR_SYSTEM_FIELD)?.disable(); } else { this.form.get(this.VENDOR_SYSTEM_FIELD)?.enable(); } } else { this.form.get('kind')?.enable(); this.form.get(this.VENDOR_SYSTEM_FIELD)?.enable(); } } private updateVendorFieldState(): void { // WI-1: Soft lock - Enable vendor field for existing accounts with confirmation dialog // Previously: disabled for existing accounts to prevent changes // Now: enabled with confirmation dialog when changed (see onVendorChange) if (this.isAccountDoesNotExistFlow) { // Special flow: vendor is pre-configured, keep disabled this.form.get(this.VENDOR_SYSTEM_FIELD)?.disable(); } else { // All other cases: enable the field (new accounts OR existing accounts) this.form.get(this.VENDOR_SYSTEM_FIELD)?.enable(); } } get isNew(): boolean { return this._isNew; } get isCurrentAccountPartnerSystemUser(): boolean { return this.form?.get('kind')?.value === RoleIds.PARTNER_SYSTEM_USER; } // ============================================================================ // DISABLED STATES FEEDBACK // ============================================================================ get shouldShowAccountTypeDisabledMessage(): boolean { return ((!this.isNew) || (this.isAccountDoesNotExistFlow && this.isNew)) && this.form?.get('kind')?.disabled; } get shouldShowVendorSystemDisabledMessage(): boolean { // WI-4: Only show disabled message for special flow (accountDoesNotExist) // Regular existing accounts now use soft-lock with confirmation dialog return this.isAccountDoesNotExistFlow && this.form?.get(this.VENDOR_SYSTEM_FIELD)?.disabled && this.showVendorOptions; } /** * Get context-aware constraint message for account type field */ get accountTypeConstraintMessage(): string { if (this.isAccountDoesNotExistFlow && this.isNew) { return Labels.ACCOUNT_TYPE_FLOW_DISABLED_MESSAGE; } return Labels.ACCOUNT_TYPE_DISABLED_MESSAGE; } /** * Get context-aware constraint message for vendor system field */ get vendorSystemConstraintMessage(): string { if (this.isAccountDoesNotExistFlow && this.isNew) { return Labels.VENDOR_SYSTEM_FLOW_DISABLED_MESSAGE; } return Labels.VENDOR_SYSTEM_DISABLED_MESSAGE; } /** * Get context-aware constraint title for account type field */ get accountTypeConstraintTitle(): string { if (this.isAccountDoesNotExistFlow && this.isNew) { return Labels.ACCOUNT_TYPE_FLOW_DISABLED_TITLE; } return Labels.ACCOUNT_TYPE_DISABLED_TITLE; } /** * Get context-aware constraint title for vendor system field */ get vendorSystemConstraintTitle(): string { if (this.isAccountDoesNotExistFlow && this.isNew) { return Labels.VENDOR_SYSTEM_FLOW_DISABLED_TITLE; } return Labels.VENDOR_SYSTEM_DISABLED_TITLE; } constructor( private readonly route: ActivatedRoute, private readonly fb: FormBuilder, private readonly partnerService: PartnerService, public readonly partnerUtils: PartnerUtilsService, private readonly cdr: ChangeDetectorRef ) { super(); this.form = this.fb.group({ profile: [], account: [], kind: [], [VENDOR_SYSTEM_FIELD]: [''] }); this.kinds = [ { label: Roles[RoleIds.APP_ADM], value: RoleIds.APP_ADM }, { label: Roles[RoleIds.OFFICER], value: RoleIds.OFFICER }, { label: Roles[RoleIds.INSPECTOR], value: RoleIds.INSPECTOR } ]; this.availableVendorOptions = [...this.vendorOptions]; } // Type guard for PartnerSystemUser private isPartnerSystemUser(account: User | PartnerSystemUser): account is PartnerSystemUser { return account.kind === RoleIds.PARTNER_SYSTEM_USER; } /** * Look up a loaded partner by ObjectId and return its partnerCode. * Returns null if partners are not yet loaded or the partner is not found. */ private getPartnerCodeFromPartnerId(partnerId: string | { _id: string } | null | undefined): string | null { if (!partnerId || !this.partners?.length) return null; const id = typeof partnerId === 'string' ? partnerId : (partnerId as any)._id; const partner = this.partners.find(p => p._id === id); return partner?.partnerCode || null; } // Helper to safely get vendor from account private getVendorFromAccount(account: User | PartnerSystemUser): string { if (this.isPartnerSystemUser(account)) { const partnerSystemUser = account as PartnerSystemUser; // Prefer partner ObjectId → partnerCode (authoritative canonical value) if (partnerSystemUser.partner) { const rawPartnerId = typeof partnerSystemUser.partner === 'string' ? partnerSystemUser.partner : (partnerSystemUser.partner as any)._id; const partnerCode = this.getPartnerCodeFromPartnerId(rawPartnerId); if (partnerCode) return partnerCode; } // If partner is already a populated object with partnerCode if (typeof partnerSystemUser.partner === 'object' && (partnerSystemUser.partner as any).partnerCode) { return (partnerSystemUser.partner as any).partnerCode; } return ''; } else if (account.kind === RoleIds.PARTNER) { return SATLOC_VENDOR; } else { return ''; } } ngOnInit() { this.sub$ = this.route.queryParams.subscribe(params => { this.returnTo = params['returnTo'] || null; this.vehicleId = params['vehicleId'] || null; this.partnerId = params['partner'] || null; this.partnerCode = params['partnerCode'] || null; this.customerId = params['customerId'] || null; this.isAccountDoesNotExistFlow = params['accountDoesNotExist'] === 'true' || params['accountDoesNotExist'] === true; if (this.isAccountDoesNotExistFlow) { const hasPartnerSystemUser = this.kinds.some(kind => kind.value === RoleIds.PARTNER_SYSTEM_USER); if (!hasPartnerSystemUser) { this.kinds.push({ label: Labels.PARTNER_SYSTEM_LABEL, value: RoleIds.PARTNER_SYSTEM_USER }); } } }); this.sub$ = this.route.data.subscribe((data) => { const account = data[0] as User || null; if (account) { this._isNew = (account._id === '0'); if (this.isNew) { if (!account.kind) { account.kind = this.kinds[0].value; } account.parent = this.authSvc.byPUserId; if (this.isAccountDoesNotExistFlow) { account.kind = RoleIds.PARTNER_SYSTEM_USER; } // Ensure all partner system users default to active if (account.kind === RoleIds.PARTNER_SYSTEM_USER) { account.active = true; } } this.account = account; if (!this.isNew && account) { this.originalUsername = account.username || ''; this.originalPassword = account.password || ''; // testAuth is triggered once by updateSelectedVendorAfterLoading() after vendor options load } } }); this.loadVendorOptions(); this.sub$.add(this.appActions.ofTypes([ userActions.CREATE_SUCCESS, userActions.UPDATE_SUCCESS ]) .subscribe((action) => { const savedAccount = action['payload']; if (this.shouldPerformPostSaveValidation(savedAccount)) { this.performPostSaveValidation(savedAccount); } else { this.handleNormalSaveSuccess(action); } })); // Form change detection - track credential modifications const accountControl = this.form.get('account'); if (accountControl) { this.sub$.add( accountControl.valueChanges.subscribe((accountValue) => { if (accountValue && !this.isNew) { const currentUsername = accountValue.username || ''; const currentPassword = accountValue.password || ''; this.credentialsChanged = (currentUsername !== this.originalUsername) || (currentPassword !== this.originalPassword); if (!this.postSaveValidationError) { this.clearPostSaveValidation(); } } }) ); } } ngOnDestroy() { super.ngOnDestroy(); } onAccountTypeChange(selectedType: any): void { if (!this.isNew && this.account && (this.account.kind === RoleIds.PARTNER || this.account.kind === RoleIds.PARTNER_SYSTEM_USER)) { return; } this.showVendorOptions = (selectedType === RoleIds.PARTNER || selectedType === RoleIds.PARTNER_SYSTEM_USER); // Ensure partner system users are always active by default if (selectedType === RoleIds.PARTNER_SYSTEM_USER && this.account) { this.account.active = true; } if (!this.showVendorOptions) { this.form.patchValue({ [VENDOR_SYSTEM_FIELD]: '' }); this.selectedVendor = null; this.updateAvailableVendorOptions(); } else { this.form.get(VENDOR_SYSTEM_FIELD)?.setValidators([Validators.required]); this.form.get(VENDOR_SYSTEM_FIELD)?.updateValueAndValidity(); this.updateAvailableVendorOptions(); } } onVendorChange(selectedVendor: any): void { // WI-1: Soft lock - Show confirmation dialog when changing vendor for existing accounts if (!this.isNew && this.originalVendorSystem && this.originalVendorSystem !== selectedVendor) { this.confirmSvc.confirm({ header: Labels.VENDOR_CHANGE_CONFIRM_TITLE, message: Labels.VENDOR_CHANGE_CONFIRM_MESSAGE, acceptLabel: globals.yes, rejectLabel: globals.no, accept: () => { // Allow the change this.selectedVendor = selectedVendor; this.form.get(VENDOR_SYSTEM_FIELD)?.updateValueAndValidity(); }, reject: () => { // Revert to original value this.form.get(VENDOR_SYSTEM_FIELD)?.setValue(this.originalVendorSystem); this.selectedVendor = this.originalVendorSystem; } }); return; } this.selectedVendor = selectedVendor; this.form.get(VENDOR_SYSTEM_FIELD)?.updateValueAndValidity(); } // ============================================================================ // PARTNER CONNECTION TESTING // ============================================================================ /** * Test partner connection for partner system user accounts. */ onTestPartnerConnection(): void { if (this.isNew) { return; } // Check if credentials have changed but not saved if (this.credentialsChanged && !this.isNew) { this.showSaveBeforeTestDialog = true; return; } if (!this.isPartnerSystemUser(this.account)) { const errorMsg = Labels.CONNECTION_TEST_ONLY_AVAILABLE_FOR_PARTNER_USERS; this.satlocError = errorMsg; this.satlocIntegration.status = OperationalStatus.ERROR; return; } const formAccount = this.form.get('account')?.value; const username = formAccount?.username; const password = formAccount?.password; if (!username || !password) { const errorMsg = Labels.MISSING_USERNAME_PASSWORD_FOR_CONNECTION_TEST; this.satlocError = errorMsg; this.satlocIntegration.status = OperationalStatus.ERROR; return; } const account = this.account; // 'customer' is a Mongoose virtual stripped by .lean() — fall back to 'parent' (the real DB field) const customerId = (typeof account?.customer === 'string' ? account.customer : (account?.customer as any)?._id) ?? (typeof (account as any)?.parent === 'string' ? (account as any).parent : ((account as any)?.parent as any)?._id); const partnerId = typeof account?.partner === 'string' ? account.partner : (account?.partner as any)?._id; if (!customerId || !partnerId) { const errorMsg = Labels.MISSING_CUSTOMER_PARTNER_ID_FOR_CONNECTION_TEST; this.satlocError = errorMsg; this.satlocIntegration.status = OperationalStatus.ERROR; return; } this.satlocLoading = true; this.satlocError = null; const testAuthObservable = this.partnerService.testPartnerAuth(customerId, partnerId, username, password); testAuthObservable .pipe( finalize(() => { this.satlocLoading = false; }) ) .subscribe({ next: (result: any) => { const isSuccess = this.partnerService.isAuthenticationSuccessful(result); if (isSuccess) { this.satlocIntegration = { enabled: true, status: OperationalStatus.ACTIVE, account_info: null, credentials_stored: true, last_error: null }; this.satlocError = null; } else { // Use centralized error handler to extract .tag and map to localized message const errorResult = handlePartnerErr(result); this.satlocIntegration.status = OperationalStatus.ERROR; this.satlocError = errorResult.message; } }, error: (error) => { // Use centralized error handler for HTTP errors const errorResult = handlePartnerErr(error); this.satlocIntegration.status = OperationalStatus.ERROR; this.satlocError = errorResult.message; } }); } /** * User confirmed saving and testing with modified credentials. */ onConfirmSaveBeforeTest(): void { this.showSaveBeforeTestDialog = false; this.pendingTestAfterSave = true; this.saveAccount(); } /** * User cancelled save before test dialog. */ onCancelSaveBeforeTest(): void { this.showSaveBeforeTestDialog = false; this.pendingTestAfterSave = false; } /** * Determine if post-save validation is required for this account. */ private shouldPerformPostSaveValidation(savedAccount: User | PartnerSystemUser): boolean { if (!this.isPartnerSystemUser(savedAccount)) { return false; } if (!this.isNew && !this.credentialsChanged) { return false; } const partnerSystemUser = savedAccount as PartnerSystemUser; // 'customer' is a Mongoose virtual stripped by .lean() — fall back to 'parent' (the real DB field) const customerId = (typeof partnerSystemUser?.customer === 'string' ? partnerSystemUser.customer : (partnerSystemUser?.customer as any)?._id) ?? (typeof (partnerSystemUser as any)?.parent === 'string' ? (partnerSystemUser as any).parent : ((partnerSystemUser as any)?.parent as any)?._id); const partnerId = typeof partnerSystemUser?.partner === 'string' ? partnerSystemUser.partner : (partnerSystemUser?.partner as any)?._id; const hasRequiredData = !!(customerId && partnerId && partnerSystemUser.username && partnerSystemUser.password); return hasRequiredData; } /** * Perform post-save credential validation. */ private performPostSaveValidation(savedAccount: PartnerSystemUser): void { this.postSaveValidationInProgress = true; this.postSaveValidationSuccess = false; this.postSaveValidationError = false; this.postSaveErrorMessage = null; // � Capture original "new" state before modifying it const wasNewAccount = this.isNew; // �🔄 CRITICAL: Transition from "new" mode to "edit" mode after account creation // This ensures that if post-save validation fails, the user can: // 1. Test credentials again (test connection button works) // 2. See save dialog when testing with modified credentials (Phase 1-3) // 3. Re-save the account with corrected credentials if (wasNewAccount) { this.account = savedAccount; this._isNew = false; this.originalUsername = savedAccount.username || ''; this.originalPassword = savedAccount.password || ''; this.credentialsChanged = false; // Mark username as saved in account editor to prevent "username taken" error if (this.accountEditor && savedAccount.username) { this.accountEditor.markUsernameAsSaved(savedAccount.username); } } // 'customer' is a Mongoose virtual stripped by .lean() — fall back to 'parent' (the real DB field) const customerId = (typeof savedAccount?.customer === 'string' ? savedAccount.customer : (savedAccount?.customer as any)?._id) ?? (typeof (savedAccount as any)?.parent === 'string' ? (savedAccount as any).parent : ((savedAccount as any)?.parent as any)?._id); const partnerId = typeof savedAccount?.partner === 'string' ? savedAccount.partner : (savedAccount?.partner as any)?._id; if (!customerId || !partnerId || !savedAccount.username || !savedAccount.password) { this.postSaveValidationInProgress = false; if (wasNewAccount) { this.postSaveValidationError = true; this.postSaveErrorMessage = 'Unable to validate credentials: missing customer or partner information.'; this.account = savedAccount; this._isNew = false; } else { this.handleNormalSaveSuccess({ payload: savedAccount }); } return; } const testAuthObservable = this.partnerService.testPartnerAuth( customerId, partnerId, savedAccount.username, savedAccount.password ); testAuthObservable .pipe( catchError(error => { return of({ authSuccess: false, success: false, error: error }); }), finalize(() => { this.cdr.detectChanges(); }) ) .subscribe({ next: (result: any) => { const isSuccess = this.partnerService.isAuthenticationSuccessful(result); if (isSuccess) { this.postSaveValidationInProgress = false; this.postSaveValidationSuccess = true; this.postSaveValidationError = false; this.postSaveErrorMessage = null; this.credentialsChanged = false; this.originalUsername = savedAccount.username || ''; this.originalPassword = savedAccount.password || ''; // Update satlocIntegration status to show success icon this.satlocIntegration = { enabled: true, status: OperationalStatus.ACTIVE, account_info: null, credentials_stored: true, last_error: null }; this.satlocError = null; // Navigation behavior depends on whether this was a new account or existing account if (wasNewAccount) { // New account created from vehicle-edit flow: Navigate back immediately // This provides seamless flow when creating missing partner accounts if (this.returnTo === 'vehicle-edit') { this.account = savedAccount; this.handleVehicleEditReturnFlow(userActions.CREATE_SUCCESS); } else { // New account from normal flow: Stay on page to show success status // User can see validation succeeded before navigating away manually this.store.dispatch(new userActions.Select(savedAccount)); } } else { // Existing account: Navigate back to account list (normal save flow) // Credentials were changed and validated successfully, so navigate away this.handleNormalSaveSuccess({ payload: savedAccount }); } } else { this.handlePostSaveValidationFailure(result, savedAccount); } }, error: (error) => { this.handlePostSaveValidationFailure(error, savedAccount); } }); } /** * Handle post-save validation failure. * Unwraps error structure if needed and extracts localized error message. */ private handlePostSaveValidationFailure(error: any, savedAccount?: PartnerSystemUser): void { // Unwrap error if it was wrapped by catchError operator // catchError wraps as: { authSuccess: false, success: false, error: HttpErrorResponse } const actualError = error?.error instanceof HttpErrorResponse ? error.error : error; const errorResult = handlePartnerErr(actualError); this.postSaveValidationInProgress = false; this.postSaveValidationSuccess = false; this.postSaveValidationError = true; this.postSaveErrorMessage = errorResult.message; // Update satlocIntegration status to show error icon this.satlocIntegration.status = OperationalStatus.ERROR; this.satlocError = errorResult.message; if (savedAccount) { this.account = savedAccount; this.originalUsername = savedAccount.username || ''; this.originalPassword = savedAccount.password || ''; this.credentialsChanged = false; } } /** * Normal save success handling. */ private handleNormalSaveSuccess(action: any): void { const savedAccount = action.payload; // If save was triggered by "Save and Test" dialog, run test connection if (this.pendingTestAfterSave) { this.pendingTestAfterSave = false; this.account = savedAccount; this._isNew = false; this.originalUsername = savedAccount.username || ''; this.originalPassword = savedAccount.password || ''; this.credentialsChanged = false; // Mark username as saved in account editor to prevent "username taken" error if (this.accountEditor && savedAccount.username) { this.accountEditor.markUsernameAsSaved(savedAccount.username); } // Trigger test connection after brief delay to ensure state is updated setTimeout(() => { this.onTestPartnerConnection(); }, 200); return; } if (this.returnTo === 'vehicle-edit') { this.account = savedAccount; // Mark username as saved in account editor to prevent "username taken" error if (this.accountEditor && savedAccount.username) { this.accountEditor.markUsernameAsSaved(savedAccount.username); } this.handleVehicleEditReturnFlow(action.type); } else { this.store.dispatch(new userActions.Select(savedAccount)); this.goBack(); } } /** * Clear post-save validation state. */ private clearPostSaveValidation(): void { this.postSaveValidationSuccess = false; this.postSaveValidationError = false; this.postSaveErrorMessage = null; } private loadVendorOptions(): void { this.vendorsLoading = true; this.form.get(this.VENDOR_SYSTEM_FIELD)?.disable(); this.partnerService.getPartners() .pipe( catchError(error => { return of([]); }), finalize(() => { this.vendorsLoading = false; this.updateVendorFieldState(); }) ) .subscribe((partners: Partner[]) => { this.partners = partners.filter(p => p.active); const partnerVendorOptions = this.partners .filter(partner => partner.partnerCode) .map(partner => ({ label: partner.partnerCode!, value: partner.partnerCode! })); const satlocOption = { label: $localize`:Satloc vendor option@@satloc:Satloc`, value: SATLOC_VENDOR }; const hasSatloc = partnerVendorOptions.some(option => this.partnerUtils.isSatlocPartner(option.value) ); let partnerLabelOption = null; if (this.isAccountDoesNotExistFlow) { const partnerLabel = this.partnerCode || (this.partnerId ? this.getPartnerLabelFromId(this.partnerId) : null); if (partnerLabel) { const hasPartnerLabel = partnerVendorOptions.some(option => option.value.toLowerCase() === partnerLabel.toLowerCase() ); if (!hasPartnerLabel) { partnerLabelOption = { label: partnerLabel, value: partnerLabel }; } } } this.vendorOptions = [ { label: Labels.SELECT_PARTNER_SYSTEM, value: '' }, ...(!hasSatloc ? [satlocOption] : []), ...(partnerLabelOption ? [partnerLabelOption] : []), ...partnerVendorOptions ]; this.availableVendorOptions = [...this.vendorOptions]; this.updateAvailableVendorOptions(); this.updateSelectedVendorAfterLoading(); this.loadExistingPartnerSystemUsers(); }); } /** * Get partner label (partnerCode) from partner ID * Used to derive vendor selection for Account Does Not Exist flow */ private getPartnerLabelFromId(partnerId: string): string | null { if (!partnerId || !this.partners) { return null; } const partner = this.partners.find(p => p._id === partnerId); return partner?.partnerCode || partner?.name || null; } /** * Updates selectedVendor after vendors are dynamically loaded * Handles case-insensitive matching between stored vendor and loaded vendor options */ private updateSelectedVendorAfterLoading(): void { if (!this.account) return; const storedVendor = this.getVendorFromAccount(this.account); if (storedVendor) { const matchingVendorOption = this.vendorOptions.find(vendor => vendor.value && this.partnerUtils.matchesPartnerCode(vendor.value, storedVendor) ); if (matchingVendorOption) { this.selectedVendor = matchingVendorOption.value; this.form.patchValue({ [VENDOR_SYSTEM_FIELD]: matchingVendorOption.value }); if (!this.isNew && this.selectedVendor && !this.satlocLoading && this.isPartnerSystemUser(this.account)) { setTimeout(() => { this.onTestPartnerConnection(); }, 200); } this.updateAvailableVendorOptions(); } } if (this.isAccountDoesNotExistFlow) { const partnerLabel = this.partnerCode || (this.partnerId ? this.getPartnerLabelFromId(this.partnerId) : null); if (partnerLabel) { const matchingVendor = this.vendorOptions.find(vendor => vendor.value && vendor.value.toLowerCase() === partnerLabel.toLowerCase() ); if (matchingVendor) { this.selectedVendor = matchingVendor.value; if (!this.form.get(VENDOR_SYSTEM_FIELD)?.disabled) { this.form.patchValue({ [VENDOR_SYSTEM_FIELD]: matchingVendor.value }); } this.updateAvailableVendorOptions(); } } } } private loadExistingPartnerSystemUsers(): void { // PSUs are already in the NgRx store from POST /users/search — read from store // instead of making N additional GET /api/partners/systemUsers calls. this.store.select(fromUsers.getAllUsers) .pipe(take(1)) .subscribe(users => { this.existingPartnerSystemUsers = users.filter( u => u.kind === RoleIds.PARTNER_SYSTEM_USER ) as PartnerSystemUser[]; this.updateAvailableVendorOptions(); }); } private updateAvailableVendorOptions(): void { // Use partner ObjectId → partnerCode for deduplication (authoritative) const existingVendorTypes = this.existingPartnerSystemUsers.map(su => { if (su.partner) { const rawId = typeof su.partner === 'string' ? su.partner : (su.partner as any)._id; return this.getPartnerCodeFromPartnerId(rawId); } return null; }).filter(vendor => vendor); this.availableVendorOptions = this.vendorOptions.filter(option => { if (!option.value) return true; if (!this.isNew && this.selectedVendor && this.partnerUtils.matchesPartnerCode(this.selectedVendor, option.value)) return true; // Guard: always keep the edited account's own vendor type, even when selectedVendor is falsy if (!this.isNew && this.account?._id) { const thisAccount = this.existingPartnerSystemUsers.find(su => su._id === (this.account as any)?._id); // Use partner ObjectId for this account's own vendor type lookup (authoritative) let thisAccountVendor: string | null = null; if (thisAccount?.partner) { const rawId = typeof thisAccount.partner === 'string' ? thisAccount.partner : (thisAccount.partner as any)._id; thisAccountVendor = this.getPartnerCodeFromPartnerId(rawId); } if (thisAccountVendor && option.value && this.partnerUtils.matchesPartnerCode(thisAccountVendor, option.value)) { return true; } } const isVendorAlreadyExists = existingVendorTypes.some(existingVendor => existingVendor && option.value && this.partnerUtils.matchesPartnerCode(existingVendor, option.value) ); return !isVendorAlreadyExists; }); this.updateAccountTypeOptions(); } private updateAccountTypeOptions(): void { const hasAvailableVendors = this.availableVendorOptions.length > 1; let partnerSystemLabel = Labels.PARTNER_SYSTEM_LABEL; // Always enable PARTNER_SYSTEM_USER option, but show constraint when no vendors available if (!hasAvailableVendors) { partnerSystemLabel += $localize` (All Partner System configured)`; } this.kinds = [ { label: Roles[RoleIds.APP_ADM], value: RoleIds.APP_ADM }, { label: Roles[RoleIds.OFFICER], value: RoleIds.OFFICER }, { label: Roles[RoleIds.INSPECTOR], value: RoleIds.INSPECTOR }, { label: partnerSystemLabel, value: RoleIds.PARTNER_SYSTEM_USER, disabled: false // Always enabled } ]; } saveAccount() { if (!this.form || !this.form.valid) return; const formValue = this.form.getRawValue(); if ((formValue.kind === RoleIds.PARTNER_SYSTEM_USER) && (!formValue[VENDOR_SYSTEM_FIELD] || formValue[VENDOR_SYSTEM_FIELD] === '')) { this.form.get(VENDOR_SYSTEM_FIELD)?.markAsTouched(); return; } const userObj = Object.assign( this.selectedItem, formValue.profile, formValue.account, { kind: formValue.kind } ); if (formValue.kind === RoleIds.PARTNER_SYSTEM_USER) { const selectedVendorType = formValue[VENDOR_SYSTEM_FIELD]; const partnerConfig = { vendorSystemType: selectedVendorType, vendorConfiguration: this.buildVendorConfiguration(selectedVendorType) }; const enhancedUserObj = { ...userObj, partnerConfig }; const enhancedAction = this._isNew ? new userActions.Create(enhancedUserObj) : new userActions.Update(enhancedUserObj); this.store.dispatch(enhancedAction); } else { const enhancedAction = this._isNew ? new userActions.Create(userObj) : new userActions.Update(userObj); this.store.dispatch(enhancedAction); } } private buildVendorConfiguration(vendorType: string): any { const baseConfig = { companyId: null, apiKey: null, apiSecret: null }; switch (vendorType) { case SATLOC_VENDOR: return { ...baseConfig }; default: return baseConfig; } } private async handleVehicleEditReturnFlow(actionType?: string): Promise { if (!this.partnerId || !this.customerId || !this.vehicleId) { this.goBack(); return; } if (actionType === userActions.CREATE_SUCCESS) { this.router.navigate(['/entities/aircraft', this.vehicleId], { queryParams: { connectionTestResult: 'success', message: Labels.PARTNER_ACCOUNT_CREATED_SUCCESSFULLY } }); return; } try { const account = this.account; if (!account.username || !account.password) { throw new Error(Labels.ACCOUNT_MISSING_CREDENTIALS_FOR_CONNECTION_TEST); } const authResult = await this.partnerService.testPartnerAuth( this.customerId, this.partnerId, account.username, account.password ).toPromise(); // Use centralized success check (handles { ok: true }, { authSuccess: true }, or { success: true }) if (this.partnerService.isAuthenticationSuccessful(authResult)) { // Success - navigate back to vehicle-edit this.router.navigate(['/entities/aircraft', this.vehicleId], { queryParams: { connectionTestResult: 'success', message: Labels.ACCOUNT_AUTHENTICATION_SUCCESSFUL } }); } else { // Authentication failed - stay on page and show error const errorResult = handlePartnerErr(authResult); this.satlocIntegration.status = OperationalStatus.ERROR; this.satlocError = errorResult.message; } } catch (error) { // Error during auth test - stay on page and show error const errorResult = handlePartnerErr(error); this.satlocIntegration.status = OperationalStatus.ERROR; this.satlocError = errorResult.message; } } goBack() { this.router.navigate(['../', { id: this.account._id }]); } }