diff --git a/apps/proxy-auth/src/app/app.component.ts b/apps/proxy-auth/src/app/app.component.ts index 5ec761d3..6239c45c 100644 --- a/apps/proxy-auth/src/app/app.component.ts +++ b/apps/proxy-auth/src/app/app.component.ts @@ -26,15 +26,15 @@ export class AppComponent extends BaseComponent implements OnInit, OnDestroy { public initOtpProvider() { if (!environment.production) { const sendOTPConfig = { - referenceId: '4512365m176216342869087ae458e09', - type: 'authorization', + // referenceId: '4512365m176216342869087ae458e09', + type: 'organization-details', // loginRedirectUrl: 'https://www.google.com', // showCompanyDetails: false, authToken: - 'clV0YUt4UURVbzJYZTRwMHdBNkZ6QjZoay9qMmRRcjZhMGVXMGtCT1ZtdGNaelFxMmlNaGdNcEJuRy9UWmFSZHQvMHc0YnJYUHExakh5NDNGVjZMOEdXVmg3OG82R094Yk5tdE9XckxjUTV1dlNzUERXRWxaOWIwWm5JRmlMVHl5UmpZUHVDK2piOURJUi9IdytncFZBRWc5QnRyRDRVeUFOZlBCY1FST0FOZStISUVtK055VWNxaGduZWpGeUZxVWxYWjd6YXI2YTF0aGxHZTNka1BlQT09', + 'ZVlWU2U4cnlOVUh5M1lYcTZLUUVaczZGdFlHN2lKOXNIU24rTWx3WWpnQzE5YXJVaTF0R215UkEvNGpIS2tJVC83Q01EQlk2QWZ6Z1UxYlQvZCtSeThxdDdiUHVuNm9RbVhPNDVnTFFUN3dKZkRIT294a3BvWFFNSGIxUFV6Wk5yZkpmYXk0MzVmUzlrTXp1bkRYTkRUdzBKMW9yRi8vTDgrak9ESzlKblVXU1hvWCtHSytkaW9nemYxTTFwNEVPSThlNk9ZRXd0YTJUanJqRk1sZUdGUT09', // type: 'user-management', isHidden: true, - theme: 'light', + theme: 'dark', // isPreview: true, isLogin: true, target: '_self', diff --git a/apps/proxy-auth/src/app/otp/organization-details/organization-details.component.html b/apps/proxy-auth/src/app/otp/organization-details/organization-details.component.html new file mode 100644 index 00000000..ba4f5f1e --- /dev/null +++ b/apps/proxy-auth/src/app/otp/organization-details/organization-details.component.html @@ -0,0 +1,174 @@ +
+
+ + + + +
+
+
+ + + + + + Company Name +
+
{{ organizationForm.get('companyName')?.value || '—' }}
+
+ +
+
+ + + + + + Email +
+
{{ organizationForm.get('email')?.value || '—' }}
+
+ +
+
+ + + + + Phone Number +
+
{{ organizationForm.get('phoneNumber')?.value || '—' }}
+
+ +
+
+ + + + + + Timezone +
+
{{ organizationForm.get('timeZoneName')?.value || '—' }}
+
+
+ + +
+ + Company Name + + + Company name is required. + + + Company name must be at least 3 characters. + + + + + Email + + Email is required. + + Please enter a valid email. + + + + + Phone Number + + + Phone number must be 10–15 digits. + + + + + Timezone + + + {{ getTimezoneLabel(tz) }} + + + + Timezone is required. + + + + +
+ + +
+
+
+
diff --git a/apps/proxy-auth/src/app/otp/organization-details/organization-details.component.scss b/apps/proxy-auth/src/app/otp/organization-details/organization-details.component.scss new file mode 100644 index 00000000..dae0ddf1 --- /dev/null +++ b/apps/proxy-auth/src/app/otp/organization-details/organization-details.component.scss @@ -0,0 +1,386 @@ +// ── Container ───────────────────────────────────────────────────────── +.container { + padding: 15px 60px; + text-align: left; + position: relative; + min-height: 600px; + height: 100vh; + width: 100%; + z-index: 10; + max-width: 2000px; + background: transparent; +} + +.organization-details-container { + padding: 0; + width: 100%; + max-width: 600px; + background: transparent; +} + +// ── Header ──────────────────────────────────────────────────────────── +.page-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 1.5rem; +} + +.page-title { + margin: 0; + font-size: 1.5rem; + font-weight: 600; +} + +.header-actions { + display: flex; + align-items: center; + gap: 8px; +} + +// Edit button — fully native, no mat-stroked-button dependency +.edit-btn { + display: inline-flex; + align-items: center; + gap: 6px; + font-family: Roboto, sans-serif; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.03em; + padding: 6px 14px; + height: 36px; + border-radius: 4px; + cursor: pointer; + background: transparent; + transition: background 0.15s ease; + outline: none; +} + +// Cancel button — fully native, no mat-button dependency +.cancel-btn { + display: inline-flex; + align-items: center; + font-family: Roboto, sans-serif; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.03em; + padding: 6px 14px; + height: 36px; + border-radius: 4px; + cursor: pointer; + background: transparent; + border: none; + transition: background 0.15s ease; + outline: none; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +// Save button — fully native, no mat-flat-button dependency +.save-btn { + display: inline-flex; + align-items: center; + justify-content: center; + font-family: Roboto, sans-serif; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.03em; + padding: 6px 20px; + height: 36px; + border-radius: 4px; + cursor: pointer; + border: none; + background: #1976d2; + color: #ffffff; + transition: background 0.15s ease; + outline: none; + + &:hover:not(:disabled) { + background: #1565c0; + } + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } +} + +// ── View mode ───────────────────────────────────────────────────────── +.view-container { + border-radius: 8px; + overflow: hidden; + width: 100%; + max-width: 600px; + background: transparent; +} + +.field-row { + display: grid; + grid-template-columns: 160px 1fr; + align-items: center; + gap: 16px; + padding: 16px 20px; + border-bottom: 1px solid var(--field-border); + transition: background 0.15s ease; + + &:last-child { + border-bottom: none; + } + &:hover { + background: var(--field-row-hover); + } + + @media (max-width: 480px) { + grid-template-columns: 1fr; + gap: 4px; + padding: 12px 16px; + } +} + +.field-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--label-color); + white-space: nowrap; +} + +// Inline SVG button icon +.btn-icon { + width: 15px; + height: 15px; + flex-shrink: 0; +} + +// Inline SVG field icons +.field-icon { + width: 15px; + height: 15px; + flex-shrink: 0; + opacity: 0.5; + color: inherit; +} + +.field-value { + font-size: 14px; + color: var(--value-color); + word-break: break-all; + line-height: 1.4; +} + +// ── Edit form ───────────────────────────────────────────────────────── +.organization-form { + gap: 0.5rem; + width: 100%; + background: transparent; +} + +.full-width { + width: 100%; +} + +.form-actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 8px; + margin-top: 8px; + width: 100%; +} + +// ── Light theme ─────────────────────────────────────────────────────── +.light-theme { + --field-border: rgba(0, 0, 0, 0.12); + --field-row-hover: rgba(0, 0, 0, 0.02); + --label-color: rgba(0, 0, 0, 0.5); + --value-color: rgba(0, 0, 0, 0.87); + + background: transparent !important; + + .page-title { + color: #000000; + } + + .view-container { + border: 1px solid rgba(0, 0, 0, 0.12); + } + + .edit-btn { + color: rgba(0, 0, 0, 0.7); + border: 1px solid rgba(0, 0, 0, 0.23); + &:hover { + background: rgba(0, 0, 0, 0.04); + } + } + + .cancel-btn { + color: rgba(0, 0, 0, 0.6); + &:hover { + background: rgba(0, 0, 0, 0.04); + } + } + + .save-btn { + background: #1976d2; + color: #ffffff; + } +} + +// ── Dark theme ──────────────────────────────────────────────────────── +.dark-theme { + --field-border: rgba(255, 255, 255, 0.15); + --field-row-hover: rgba(255, 255, 255, 0.04); + --label-color: rgba(255, 255, 255, 0.5); + --value-color: rgba(255, 255, 255, 0.87); + + background: transparent !important; + color: #ffffff !important; + + .page-title { + color: #ffffff !important; + } + + .view-container { + border: 1px solid rgba(255, 255, 255, 0.15); + } + + .edit-btn { + color: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(255, 255, 255, 0.3); + &:hover { + background: rgba(255, 255, 255, 0.06); + } + } + + .cancel-btn { + color: rgba(255, 255, 255, 0.6); + &:hover { + background: rgba(255, 255, 255, 0.06); + } + } + + // Material form field overrides for dark inside ShadowDom + .mat-mdc-text-field-wrapper { + background-color: transparent !important; + } + .mat-mdc-input-element { + color: #ffffff !important; + } + .mat-mdc-floating-label { + color: #ffffff !important; + } + .mdc-floating-label { + color: #ffffff !important; + } + mat-label { + color: #ffffff !important; + } + .mat-mdc-form-field-flex .mdc-floating-label { + color: #ffffff !important; + } + .mat-mdc-form-field .mat-mdc-floating-label { + color: #ffffff !important; + } + .mdc-text-field--outlined .mdc-floating-label { + color: #ffffff !important; + } + .mat-form-field-label { + color: #ffffff !important; + } + .mat-form-field-outline { + color: rgba(255, 255, 255, 0.5) !important; + } + .mat-form-field .mat-input-element { + color: #ffffff !important; + } + .mat-form-field-hint-wrapper, + .mat-form-field-error-wrapper { + color: rgba(255, 255, 255, 0.7) !important; + } + + // mat-select: fix selected value + arrow color inside shadow root + // Material 14 legacy select uses .mat-select-value, .mat-select-value-text + .mat-select-value, + .mat-select-value-text, + .mat-select-value-text span { + color: #ffffff !important; + } + .mat-select-placeholder { + color: rgba(255, 255, 255, 0.42) !important; + } + .mat-select-arrow { + color: rgba(255, 255, 255, 0.7) !important; + } + .mat-select-arrow svg { + fill: rgba(255, 255, 255, 0.7) !important; + } + .mat-select-trigger { + color: #ffffff !important; + } + .mat-select-trigger .mat-select-value { + color: #ffffff !important; + } + + // MDC select (if used elsewhere) + .mat-mdc-select-value { + color: #ffffff !important; + } + .mat-mdc-select-value-text { + color: #ffffff !important; + } + .mat-mdc-select-value-text span { + color: #ffffff !important; + } + .mat-mdc-select-placeholder { + color: rgba(255, 255, 255, 0.42) !important; + } + .mat-mdc-select-arrow svg { + fill: rgba(255, 255, 255, 0.7) !important; + } + .mat-mdc-select-arrow { + color: rgba(255, 255, 255, 0.7) !important; + } + .mdc-text-field .mat-mdc-select-value { + color: #ffffff !important; + } + .mat-mdc-form-field .mat-mdc-select-value { + color: #ffffff !important; + } + + // Outline border color for select + input fields + .mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__leading, + .mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__notch, + .mdc-text-field--outlined:not(.mdc-text-field--disabled) .mdc-notched-outline__trailing { + border-color: rgba(255, 255, 255, 0.3) !important; + } +} + +// ── Snackbars ───────────────────────────────────────────────────────── +::ng-deep .success-snackbar { + background-color: #59a75c !important; + color: #ffffff !important; + + .mat-mdc-snack-bar-label { + color: #ffffff !important; + } + .mat-button { + color: #ffffff !important; + } +} + +::ng-deep .error-snackbar { + background-color: #d32f2f !important; + color: #ffffff !important; + + .mat-snack-bar-label { + color: #ffffff !important; + } + .mat-button { + color: #ffffff !important; + } +} diff --git a/apps/proxy-auth/src/app/otp/organization-details/organization-details.component.ts b/apps/proxy-auth/src/app/otp/organization-details/organization-details.component.ts new file mode 100644 index 00000000..35d57d65 --- /dev/null +++ b/apps/proxy-auth/src/app/otp/organization-details/organization-details.component.ts @@ -0,0 +1,194 @@ +import { Input, OnDestroy, OnInit } from '@angular/core'; + +import { Component, ViewEncapsulation } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { BaseComponent } from 'libs/ui/base-component/src/lib/base-component/base.component'; +import { OtpService } from '../service/otp.service'; +import { finalize, takeUntil } from 'rxjs'; +import { MatSnackBar } from '@angular/material/snack-bar'; + +@Component({ + selector: 'organization-details', + templateUrl: './organization-details.component.html', + encapsulation: ViewEncapsulation.ShadowDom, + styleUrls: ['../../../styles.scss', './organization-details.component.scss'], +}) +export class OrganizationDetailsComponent extends BaseComponent implements OnInit, OnDestroy { + @Input() public authToken: string; + @Input() public theme: string; + + public organizationForm = new FormGroup({ + companyName: new FormControl('', [Validators.required, Validators.minLength(3)]), + email: new FormControl('', [Validators.required, Validators.email]), + phoneNumber: new FormControl('', [Validators.pattern(/^$|^[0-9]{10,15}$/)]), + timezone: new FormControl('', [Validators.required]), + timeZoneName: new FormControl('', [Validators.required]), + }); + + public updateInProgress = false; + + /** Timezone options from API (e.g. { value: string, label?: string }[] or string[]) */ + public timezones: any[] = []; + + // ── NEW: controls view vs edit mode ────────────────────────── + public isEditing = false; + // ───────────────────────────────────────────────────────────── + + private initialFormValue: { + companyName: string; + email: string; + phoneNumber: string; + timezone: string; + timeZoneName: string; + } | null = null; + + // Snapshot taken when the user clicks Edit, so Cancel can restore it + private editSnapshot: typeof this.initialFormValue = null; + + constructor(private otpService: OtpService, private snackBar: MatSnackBar) { + super(); + } + + ngOnInit(): void { + if (this.authToken) { + this.otpService + .getOrganizationDetails(this.authToken) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (res) => { + const company = res?.data?.[0]?.currentCompany; + if (company && typeof company === 'object') { + const timeZoneName = + company.timeZoneName ?? + this.timezones.find((tz) => tz?.offset === company.timezone)?.label ?? + ''; + const value = { + companyName: company.name ?? '', + email: company.email ?? '', + phoneNumber: company.mobile ?? '', + timezone: company.timezone ?? '', + timeZoneName: timeZoneName ?? '', + }; + this.organizationForm.patchValue(value); + this.initialFormValue = value; + } + }, + error: () => {}, + }); + + this.otpService + .getTimezones(this.authToken) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (res) => { + const raw = res?.data ?? res; + if (Array.isArray(raw)) { + this.timezones = res.data; + } + }, + error: () => {}, + }); + } + this.organizationForm + .get('timeZoneName') + ?.valueChanges.pipe(takeUntil(this.destroy$)) + .subscribe((value) => { + this.organizationForm + .get('timezone') + ?.setValue(this.timezones.find((tz) => tz?.label === value)?.offset ?? ''); + }); + } + + ngOnDestroy(): void { + super.ngOnDestroy(); + } + + // ── NEW: enter edit mode ────────────────────────────────────── + public startEdit(): void { + // Snapshot current values so Cancel can restore them + this.editSnapshot = { ...this.organizationForm.value } as typeof this.initialFormValue; + this.isEditing = true; + } + + // ── NEW: cancel and restore form to pre-edit state ──────────── + public cancelEdit(): void { + if (this.editSnapshot) { + this.organizationForm.patchValue(this.editSnapshot); + } + this.organizationForm.markAsPristine(); + this.organizationForm.markAsUntouched(); + this.isEditing = false; + } + + public getTimezoneValue(tz): string { + return typeof tz === 'string' ? tz : tz.label ?? ''; + } + + public getTimezoneLabel(tz): string { + return typeof tz === 'string' ? tz : tz.label ?? ''; + } + + public onSubmit(): void { + if (!this.organizationForm.valid || !this.authToken || this.updateInProgress) { + this.organizationForm.markAllAsTouched(); + return; + } + + const organizationDetails = this.organizationForm.value; + const current = { + companyName: organizationDetails.companyName ?? '', + email: organizationDetails.email ?? '', + phoneNumber: organizationDetails.phoneNumber ?? '', + timezone: organizationDetails.timezone ?? '', + timeZoneName: organizationDetails.timeZoneName ?? '', + }; + + // No-op if nothing changed + if ( + this.initialFormValue && + this.initialFormValue.companyName === current.companyName && + this.initialFormValue.email === current.email && + this.initialFormValue.phoneNumber === current.phoneNumber && + this.initialFormValue.timezone === current.timezone && + this.initialFormValue.timeZoneName === current.timeZoneName + ) { + this.isEditing = false; // just close edit mode silently + return; + } + + this.updateInProgress = true; + this.otpService + .updateCompany(this.authToken, { + name: organizationDetails.companyName ?? '', + email: organizationDetails.email ?? '', + mobile: organizationDetails.phoneNumber ?? '', + timezone: organizationDetails.timezone ?? '', + timeZoneName: organizationDetails.timeZoneName ?? '', + }) + .pipe( + takeUntil(this.destroy$), + finalize(() => (this.updateInProgress = false)) + ) + .subscribe({ + next: (res) => { + this.initialFormValue = { ...current }; + this.isEditing = false; // ← close edit mode on success + this.snackBar.open(res?.data?.message ?? 'Information successfully updated', '✕', { + duration: 3000, + horizontalPosition: 'center', + verticalPosition: 'top', + panelClass: ['success-snackbar'], + }); + }, + error: () => { + // Stay in edit mode so user can retry + this.snackBar.open('Something went wrong', '✕', { + duration: 3000, + horizontalPosition: 'center', + verticalPosition: 'top', + panelClass: ['error-snackbar'], + }); + }, + }); + } +} diff --git a/apps/proxy-auth/src/app/otp/otp.module.ts b/apps/proxy-auth/src/app/otp/otp.module.ts index d9057006..9a32662a 100644 --- a/apps/proxy-auth/src/app/otp/otp.module.ts +++ b/apps/proxy-auth/src/app/otp/otp.module.ts @@ -44,6 +44,8 @@ import { MatTabsModule } from '@angular/material/tabs'; import { SubscriptionCenterComponent } from './component/subscription-center/subscription-center.component'; import { MatDividerModule } from '@angular/material/divider'; import { UiConfirmDialogModule } from '@proxy/ui/confirm-dialog'; +import { OrganizationDetailsComponent } from './organization-details/organization-details.component'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; export const CHAT_COMPONENTS: any[] = [ SendOtpComponent, @@ -53,6 +55,7 @@ export const CHAT_COMPONENTS: any[] = [ UserProfileComponent, UserManagementComponent, SubscriptionCenterComponent, + OrganizationDetailsComponent, ]; @NgModule({ @@ -95,6 +98,7 @@ export const CHAT_COMPONENTS: any[] = [ }), ServicesHttpWrapperNoAuthModule, DirectivesMarkAllAsTouchedModule, + MatSnackBarModule, ], declarations: [...CHAT_COMPONENTS], providers: [ diff --git a/apps/proxy-auth/src/app/otp/send-otp/send-otp.component.html b/apps/proxy-auth/src/app/otp/send-otp/send-otp.component.html index 52dd5c13..e95ddf11 100644 --- a/apps/proxy-auth/src/app/otp/send-otp/send-otp.component.html +++ b/apps/proxy-auth/src/app/otp/send-otp/send-otp.component.html @@ -4,7 +4,7 @@ [ngClass]="{ 'with-reference-id': referenceId, 'without-reference-id': authToken, - 'dark-theme': theme === 'dark' && (type === 'user-management' || authToken) + 'dark-theme': (theme === 'dark' && (type === 'user-management' || authToken)) || type === 'organization-details' }" >
+