Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/app/core/models/enum/enum.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export enum InAppIntegration {
QBD_DIRECT = 'QuickBooks Connector'
}

export type IntegrationAppKey = keyof typeof InAppIntegration;

export enum ToastSeverity {
SUCCESS = 'success',
ERROR = 'error',
Expand Down Expand Up @@ -900,7 +902,8 @@ export enum SizeOption {
export enum ThemeOption {
BRAND = 'brand',
LIGHT = 'light',
DARK = 'dark'
DARK = 'dark',
SUCCESS = 'success'
}

export enum QBDPreRequisiteState {
Expand Down
11 changes: 11 additions & 0 deletions src/app/core/models/integrations/integrations.model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { AccountingIntegrationApp, AppUrl, ClickEvent, InAppIntegration, IntegrationView } from "../enum/enum.model";

export type Integration = {
id: number;
org_id: string;
org_name: string;
tpa_id: string;
tpa_name: string;
type: string;
is_active: boolean;
is_beta: boolean;
}

export type IntegrationsView = {
[IntegrationView.ACCOUNTING]: boolean,
[IntegrationView.ALL]: boolean,
Expand Down
16 changes: 16 additions & 0 deletions src/app/core/services/common/integrations.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { IntegrationsService } from './integrations.service';

xdescribe('IntegrationsService', () => {
let service: IntegrationsService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(IntegrationsService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});
Comment on lines +5 to +16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Enable the test suite by replacing xdescribe with describe.

The test suite is currently disabled due to the xdescribe. To ensure the tests run, remove the x prefix.

Apply this diff to enable the tests:

- xdescribe('IntegrationsService', () => {
+ describe('IntegrationsService', () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
xdescribe('IntegrationsService', () => {
let service: IntegrationsService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(IntegrationsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
describe('IntegrationsService', () => {
let service: IntegrationsService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(IntegrationsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

22 changes: 22 additions & 0 deletions src/app/core/services/common/integrations.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { AppUrl } from '../../models/enum/enum.model';
import { ApiService } from './api.service';
import { HelperService } from './helper.service';
import { Observable } from 'rxjs';
import { Integration } from '../../models/integrations/integrations.model';

@Injectable({
providedIn: 'root'
})
export class IntegrationsService {
constructor(
private apiService: ApiService,
private helper: HelperService
) {
}

getIntegrations(): Observable<Integration[]> {
this.helper.setBaseApiURL(AppUrl.INTEGRATION);
return this.apiService.get(`/integrations/`, {});
}
Comment on lines +18 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling and consider caching.

The getIntegrations method should handle API errors and implement caching for better performance.

Consider implementing:

+ private integrationsCache$ = new ReplaySubject<Integration[]>(1);

  getIntegrations(): Observable<Integration[]> {
    this.helper.setBaseApiURL(AppUrl.INTEGRATION);
-    return this.apiService.get(`/integrations/`, {});
+    return this.apiService.get<Integration[]>(`/integrations/`, {}).pipe(
+      catchError(error => {
+        console.error('Failed to fetch integrations:', error);
+        return throwError(() => new Error('Failed to fetch integrations. Please try again.'));
+      }),
+      tap(integrations => this.integrationsCache$.next(integrations)),
+      shareReplay(1)
+    );
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getIntegrations(): Observable<Integration[]> {
this.helper.setBaseApiURL(AppUrl.INTEGRATION);
return this.apiService.get(`/integrations/`, {});
}
private integrationsCache$ = new ReplaySubject<Integration[]>(1);
getIntegrations(): Observable<Integration[]> {
this.helper.setBaseApiURL(AppUrl.INTEGRATION);
return this.apiService.get<Integration[]>(`/integrations/`, {}).pipe(
catchError(error => {
console.error('Failed to fetch integrations:', error);
return throwError(() => new Error('Failed to fetch integrations. Please try again.'));
}),
tap(integrations => this.integrationsCache$.next(integrations)),
shareReplay(1)
);
}

}
5 changes: 5 additions & 0 deletions src/app/integrations/integrations-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { IntegrationsComponent } from './integrations.component';
import { LandingComponent } from './landing/landing.component';
import { LandingV2Component } from './landing-v2/landing-v2.component';

const routes: Routes = [
{
Expand All @@ -12,6 +13,10 @@ const routes: Routes = [
path: 'landing',
component: LandingComponent
},
{
path: 'landing_v2',
component: LandingV2Component
},
{
path: 'bamboo_hr',
loadChildren: () => import('./bamboo-hr/bamboo-hr.module').then(m => m.BambooHrModule)
Expand Down
4 changes: 3 additions & 1 deletion src/app/integrations/integrations.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { SharedModule } from '../shared/shared.module';
import { Sage300Component } from './sage300/sage300.component';
import { XeroComponent } from './xero/xero.component';
import { TravelperkComponent } from './travelperk/travelperk.component';
import { LandingV2Component } from './landing-v2/landing-v2.component';

@NgModule({
declarations: [
Expand All @@ -18,7 +19,8 @@ import { TravelperkComponent } from './travelperk/travelperk.component';
QbdComponent,
Sage300Component,
XeroComponent,
TravelperkComponent
TravelperkComponent,
LandingV2Component
],
imports: [
CommonModule,
Expand Down
239 changes: 239 additions & 0 deletions src/app/integrations/landing-v2/landing-v2.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
<div class="tw-text-slightly-normal-text-color tw-py-8 tw-px-6">
<div class="landing-v2--container">
<header>
<h3 class="landing-v2--section-heading">
List of integrations
</h3>

<p class="landing-v2--section-caption">
If your company uses any of the applications listed below, you can easily integrate them with Fyle.
<br>
Need an integration we don't support yet? Let us know at <a class="tw-text-link-primary"
href="mailto:[email protected]">support{{'@'}}fylehq.com</a>
</p>
</header>
<div *ngIf="!exposeC1Apps" class="tw-flex tw-text-14-px tw-border-y tw-border-separator">
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.ALL}"
(click)="switchView(IntegrationsView.ALL)">
All
<p *ngIf="integrationTabs.ALL" class="landing-v2--active-tag"></p>
</div>
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.ACCOUNTING}"
(click)="switchView(IntegrationsView.ACCOUNTING)">
Accounting
<p *ngIf="integrationTabs.ACCOUNTING" class="landing-v2--active-tag"></p>
</div>
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.HRMS}"
(click)="switchView(IntegrationsView.HRMS)">
HRMS
<p *ngIf="integrationTabs.HRMS" class="landing-v2--active-tag"></p>
</div>
<div *ngIf="org.allow_travelperk" class="landing-v2--tab"
[ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.TRAVEL}"
(click)="switchView(IntegrationsView.TRAVEL)">
Travel
<p *ngIf="integrationTabs.TRAVEL" class="landing-v2--active-tag"></p>
</div>
</div>
Comment on lines +15 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add ARIA labels for better accessibility.

The tab navigation needs proper ARIA attributes for better screen reader support.

-        <div *ngIf="!exposeC1Apps" class="tw-flex tw-text-14-px tw-border-y tw-border-separator">
+        <div *ngIf="!exposeC1Apps" class="tw-flex tw-text-14-px tw-border-y tw-border-separator" role="tablist" aria-label="Integration categories">
-            <div class="landing-v2--tab"
+            <div class="landing-v2--tab" role="tab" [attr.aria-selected]="integrationTabs.ALL" [attr.tabindex]="integrationTabs.ALL ? 0 : -1"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div *ngIf="!exposeC1Apps" class="tw-flex tw-text-14-px tw-border-y tw-border-separator">
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.ALL}"
(click)="switchView(IntegrationsView.ALL)">
All
<p *ngIf="integrationTabs.ALL" class="landing-v2--active-tag"></p>
</div>
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.ACCOUNTING}"
(click)="switchView(IntegrationsView.ACCOUNTING)">
Accounting
<p *ngIf="integrationTabs.ACCOUNTING" class="landing-v2--active-tag"></p>
</div>
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.HRMS}"
(click)="switchView(IntegrationsView.HRMS)">
HRMS
<p *ngIf="integrationTabs.HRMS" class="landing-v2--active-tag"></p>
</div>
<div *ngIf="org.allow_travelperk" class="landing-v2--tab"
[ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.TRAVEL}"
(click)="switchView(IntegrationsView.TRAVEL)">
Travel
<p *ngIf="integrationTabs.TRAVEL" class="landing-v2--active-tag"></p>
</div>
</div>
<div *ngIf="!exposeC1Apps" class="tw-flex tw-text-14-px tw-border-y tw-border-separator" role="tablist" aria-label="Integration categories">
<div class="landing-v2--tab" role="tab" [attr.aria-selected]="integrationTabs.ALL" [attr.tabindex]="integrationTabs.ALL ? 0 : -1"
[ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.ALL}"
(click)="switchView(IntegrationsView.ALL)">
All
<p *ngIf="integrationTabs.ALL" class="landing-v2--active-tag"></p>
</div>
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.ACCOUNTING}"
(click)="switchView(IntegrationsView.ACCOUNTING)">
Accounting
<p *ngIf="integrationTabs.ACCOUNTING" class="landing-v2--active-tag"></p>
</div>
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.HRMS}"
(click)="switchView(IntegrationsView.HRMS)">
HRMS
<p *ngIf="integrationTabs.HRMS" class="landing-v2--active-tag"></p>
</div>
<div *ngIf="org.allow_travelperk" class="landing-v2--tab"
[ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.TRAVEL}"
(click)="switchView(IntegrationsView.TRAVEL)">
Travel
<p *ngIf="integrationTabs.TRAVEL" class="landing-v2--active-tag"></p>
</div>
</div>


<div
class="tw-grid tw-grid-cols-2 md:tw-grid-cols-3 lg:tw-grid-cols-4 xl:tw-grid-cols-5 tw-gap-4 tw-justify-items-stretch">
<div *ngIf="isAppShown('NETSUITE')">
<div class="landing-v2--accounting-app"
(click)="openAccountingIntegrationApp(AccountingIntegrationApp.NETSUITE)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/netsuite-logo-new.png" />
@if (isAppConnected('NETSUITE')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<div>
<span class="landing-v2--accounting-app-name">
NetSuite
</span>
<span class="landing-v2--accounting-app-type">
Accounting
</span>
</div>
</div>
</div>
<div *ngIf="isAppShown('INTACCT')" class="landing-v2--accounting-app"
(click)="openAccountingIntegrationApp(AccountingIntegrationApp.SAGE_INTACCT)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/intacct-logo-new.png" />
@if (isAppConnected('INTACCT')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<div>
<span class="landing-v2--accounting-app-name">
Sage Intacct
</span>
<span class="landing-v2--accounting-app-type">
Accounting
</span>
</div>
</div>
<div *ngIf="isAppShown('QBO')" class="landing-v2--accounting-app"
(click)="openAccountingIntegrationApp(AccountingIntegrationApp.QBO)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/quickbooks-logo.png" class="!tw-h-[30.7px]" />
@if (isAppConnected('QBO')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<div>
<span class="landing-v2--accounting-app-name">
QuickBooks Online
</span>
<span class="landing-v2--accounting-app-type">
Accounting
</span>
</div>
</div>
<div *ngIf="isAppShown('XERO')" class="landing-v2--accounting-app"
(click)="openAccountingIntegrationApp(AccountingIntegrationApp.XERO)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/xero-logo-new.png" />
@if (isAppConnected('XERO')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<div>
<span class="landing-v2--accounting-app-name">
Xero
</span>
<span class="landing-v2--accounting-app-type">
Accounting
</span>
</div>
</div>
<div *ngIf="isAppShown('QBD')" class="landing-v2--accounting-app"
(click)="openInAppIntegration(InAppIntegration.QBD)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/quickbooks-logo.png" class="!tw-h-[30.7px]" />
@if (isAppConnected('QBD')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<div>
<span class="landing-v2--accounting-app-name">
QuickBooks Desktop (IIF)
</span>
<span class="landing-v2--accounting-app-type">
Accounting
</span>
</div>
</div>
<!-- Direct -->
<div *ngIf="isAppShown('QBD_DIRECT')" class="landing-v2--accounting-app"
(click)="openInAppIntegration(InAppIntegration.QBD_DIRECT)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/quickbooks-logo.png" class="!tw-h-[30.7px]" />
@if (isAppConnected('QBD_DIRECT')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<span class="landing-v2--accounting-app-name tw-items-center tw-gap-4">
<div>
QuickBooks Desktop (Web Connector)
<div class="landing-v2--accounting-app-type">Accounting</div>
</div>
<app-badge [theme]="ThemeOption.DARK" text="Beta"></app-badge>
</span>
</div>
Comment on lines +41 to +156
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Extract integration card to a separate component.

The integration card pattern is repeated multiple times. Consider extracting it to a reusable component to reduce code duplication and improve maintainability.

Create a new component IntegrationCardComponent:

@Component({
  selector: 'app-integration-card',
  template: `
    <div class="landing-v2--accounting-app" (click)="onCardClick()">
      <div class="tw-flex tw-justify-between tw-items-center">
        <img [src]="logoSrc" [class]="logoClass" />
        <app-badge *ngIf="isConnected" text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
        <button *ngIf="!isConnected" class="btn-connect">Connect</button>
      </div>
      <div>
        <span class="landing-v2--accounting-app-name">
          {{ name }}
        </span>
        <span class="landing-v2--accounting-app-type">
          {{ type }}
        </span>
      </div>
    </div>
  `
})
export class IntegrationCardComponent {
  @Input() name: string;
  @Input() type: string;
  @Input() logoSrc: string;
  @Input() logoClass: string;
  @Input() isConnected: boolean;
  @Output() connect = new EventEmitter<void>();
}

<div *ngIf="isAppShown('SAGE300')" class="landing-v2--accounting-app"
(click)="openInAppIntegration(InAppIntegration.SAGE300)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/sage300-logo.png" class="tw-py-[4px]" />
@if (isAppConnected('SAGE300')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<span class="landing-v2--accounting-app-name tw-items-center tw-gap-4">
<div>
Sage 300 CRE
<div class="landing-v2--accounting-app-type">Accounting</div>
</div>
<app-badge *ngIf="!orgsToHideSage300BetaBadge.includes(org.fyle_org_id)" [theme]="ThemeOption.DARK"
text="Beta"></app-badge>
</span>
</div>
<div *ngIf="isAppShown('BUSINESS_CENTRAL')" class="landing-v2--accounting-app"
(click)="openInAppIntegration(InAppIntegration.BUSINESS_CENTRAL)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/BusinessCentral-logo.svg" />
@if (isAppConnected('BUSINESS_CENTRAL')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<span class="landing-v2--accounting-app-name tw-items-center tw-gap-4">
<div>
Dynamics 365 Business Central
<div class="landing-v2--accounting-app-type">Accounting</div>
</div>
<app-badge *ngIf="!orgsToHideBusinessCentralBetaBadge.includes(org.fyle_org_id)"
[theme]="ThemeOption.DARK" text="Beta"></app-badge>
</span>
</div>

<div *ngIf="isAppShown('BAMBOO_HR')">
<div class="landing-v2--accounting-app" (click)="openInAppIntegration(InAppIntegration.BAMBOO_HR)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/bamboo-hr-logo.png" />
@if (isAppConnected('BAMBOO_HR')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<div>
<span class="landing-v2--accounting-app-name">
BambooHR
</span>
<span class="landing-v2--accounting-app-type">
HRMS
</span>
</div>
</div>
</div>

<div *ngIf="isAppShown('TRAVELPERK')">
<div class="landing-v2--accounting-app" (click)="openInAppIntegration(InAppIntegration.TRAVELPERK)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/travelperk-logo.png" class="tw-py-[5px]" />
@if (isAppConnected('TRAVELPERK')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<div>
<span class="landing-v2--accounting-app-name">
TravelPerk
</span>
<span class="landing-v2--accounting-app-type">
Travel
</span>
</div>
</div>
</div>
</div>
</div>
</div>
Loading
Loading