From 2a6f85890fe570ba2e284cb10048063372ec07da Mon Sep 17 00:00:00 2001 From: Gagan7900 <54118395+Gagan7900@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:07:12 +0530 Subject: [PATCH] code refactoring, remove duplicate gaurds and other files --- src/app/app.config.ts | 13 +- src/app/components/dashboards/crm/crm.html | 529 --------------- src/app/components/dashboards/crm/crm.scss | 0 src/app/components/dashboards/crm/crm.ts | 618 ------------------ .../components/dashboards/dashboard.routes.ts | 12 - src/app/components/error/error.routes.ts | 15 - .../components/error/error404/error404.html | 27 - .../components/error/error404/error404.scss | 0 src/app/components/error/error404/error404.ts | 16 - src/app/components/error/particleoptions.ts | 36 - src/app/core/guards/auth.guard.ts | 37 -- src/app/core/guards/guest.guard.ts | 53 -- .../core/services/common/firebase.service.ts | 27 - .../services/common/permission.service.ts | 5 +- .../context/tenant-context.service.ts | 5 +- .../services/context/user-context.service.ts | 5 +- .../authentication/pages/login/login.ts | 11 +- .../onboarding-tabs/onboarding-tabs.html | 126 ++++ .../onboarding-tabs/onboarding-tabs.scss | 159 +++++ .../onboarding-tabs/onboarding-tabs.ts | 155 +++++ .../organization-onboarding.endpoints.ts | 2 + .../organization-onboarding-state.service.ts | 59 +- .../organization-onboarding.service.ts | 19 + .../models/organization-onboarding.model.ts | 7 + .../organization-onboarding.html | 72 +- .../organization-onboarding.scss | 45 ++ .../organization-onboarding.spec.ts | 197 ++++++ .../organization-onboarding.ts | 43 ++ .../organization-basics.ts | 16 + .../organization-plan-limits.html | 28 +- .../organization-plan-limits.ts | 63 +- .../components/data-table/data-table.html | 10 +- src/environments/environment.model.ts | 11 - src/environments/environment.prod.ts | 9 - src/environments/environment.ts | 9 - src/styles.scss | 4 +- 36 files changed, 943 insertions(+), 1500 deletions(-) delete mode 100644 src/app/components/dashboards/crm/crm.html delete mode 100644 src/app/components/dashboards/crm/crm.scss delete mode 100644 src/app/components/dashboards/crm/crm.ts delete mode 100644 src/app/components/dashboards/dashboard.routes.ts delete mode 100644 src/app/components/error/error.routes.ts delete mode 100644 src/app/components/error/error404/error404.html delete mode 100644 src/app/components/error/error404/error404.scss delete mode 100644 src/app/components/error/error404/error404.ts delete mode 100644 src/app/components/error/particleoptions.ts delete mode 100644 src/app/core/guards/auth.guard.ts delete mode 100644 src/app/core/guards/guest.guard.ts delete mode 100644 src/app/core/services/common/firebase.service.ts create mode 100644 src/app/features/organizations/organization-onboarding/components/onboarding-tabs/onboarding-tabs.html create mode 100644 src/app/features/organizations/organization-onboarding/components/onboarding-tabs/onboarding-tabs.scss create mode 100644 src/app/features/organizations/organization-onboarding/components/onboarding-tabs/onboarding-tabs.ts create mode 100644 src/app/features/organizations/organization-onboarding/organization-onboarding.spec.ts diff --git a/src/app/app.config.ts b/src/app/app.config.ts index b26bebc6..45e88166 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,13 +1,8 @@ import { ApplicationConfig, importProvidersFrom } from '@angular/core'; -import { RouterOutlet, provideRouter, withRouterConfig } from '@angular/router'; +import { provideRouter, withRouterConfig } from '@angular/router'; import { provideAnimations } from '@angular/platform-browser/animations'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { App_Route } from './app.routes'; -import { AngularFireModule } from '@angular/fire/compat'; -import { environment } from '../environments/environment'; -import { AngularFireAuthModule } from '@angular/fire/compat/auth'; -import { AngularFireDatabaseModule } from '@angular/fire/compat/database'; -import { AngularFirestoreModule } from '@angular/fire/compat/firestore'; import { ToastrModule } from 'ngx-toastr'; import { provideCharts, withDefaultRegisterables } from 'ng2-charts'; import { authInterceptor } from './core/interceptors/auth.interceptor'; @@ -18,15 +13,9 @@ export const appConfig: ApplicationConfig = { providers: [ provideRouter(App_Route, withRouterConfig({ onSameUrlNavigation: 'reload' })), provideHttpClient(withInterceptors([authInterceptor, errorInterceptor,loadingInterceptor])), - RouterOutlet, provideAnimations(), - AngularFireAuthModule, - AngularFirestoreModule, - AngularFireDatabaseModule, - AngularFireModule, provideCharts(withDefaultRegisterables()), importProvidersFrom( - // AngularFireModule.initializeApp(environment.firebase), ToastrModule.forRoot({ timeOut: 1500, closeButton: true, diff --git a/src/app/components/dashboards/crm/crm.html b/src/app/components/dashboards/crm/crm.html deleted file mode 100644 index 55adc727..00000000 --- a/src/app/components/dashboards/crm/crm.html +++ /dev/null @@ -1,529 +0,0 @@ -
-
-

Welcome back, - Json Taylor !

-

Track your sales activity, leads and deals - here.

-
-
- - -
-
- -
-
-
-
-
-
-
-
-
-
Your target is incomplete
- You have - completed - 48% of the given - target, you can also check your status. - Click - here -
-
- - -
-
-
-
-
-
-
-
-
- Top Deals -
-
- - -
-
-
-
    - @for (deal of topDeals; track $index) { -
  • -
    - -
    - - @if (deal.avatarImg) { - - } @else { - - {{ deal.initials }} - - } - -
    - -
    -

    - {{ deal.name }} -

    -

    - {{ deal.email }} -

    -
    - -
    - {{ deal.amount }} -
    - -
    -
  • - } - -
-
-
-
-
-
-
-
Profit Earned
- -
-
- - -
-
-
-
-
-
- @for (item of crmCards; track $index) { -
-
-
-
-
- - - -
-
-
-
-

{{item.title}}

-

{{item.number}}

-
- -
-
-
- View All -
-
-

{{item.percentage}} -

-

this month

-
-
-
-
-
-
-
- } - -
-
-
-
- Revenue Analytics -
- -
-
- - -
-
-
-
-
-
-
-
-
- Deals Statistics -
-
-
- -
- -
-
-
-
-
- - - @for (row of SalesRepData.rows; track row.id) { - - - - - - - -
- - img - - {{ row.name }} -
- - - {{ row.category }} - - {{ row.mail }} - - - - {{ row.location }} - - - - {{ row.date }} - - - - - - - } -
-
-
-
- -
-
-
-
-
-
-
-
-
-
- Leads By Source -
-
- - -
-
-
- -
- - - -
- Total - 4,145 -
-
-
-
- - @for (item of deviceLeads; track item.label) { -
-
- - - {{ item.label }} - - -
- {{ item.value }} -
- -
-
- } - -
-
-
-
-
-
-
- Deals Status -
- -
-
-
-

4,289

-
- 1.02 - compared to last week -
-
- -
-
-
-
-
-
-
-
-
-
-
    - - @for (deal of dealStats; track deal.title) { -
  • -
    -
    {{ deal.title }}
    - -
    - {{ deal.count }} -
    -
    -
  • - } - -
-
-
-
-
-
-
-
- Recent Activity -
- -
-
-
-
    - @for (item of recentActivity; track $index) { -
  • -
    - -
    - - - -
    - -
    - {{ item.mainText }} - - @if (item.linkText) { - {{ item.linkText }} - } - - @if (item.boldText) { - {{ item.boldText }} - } - - @if (item.highlightText) { - {{ item.highlightText }} - } - - @if (item.tagText) { - {{ item.tagText }} - } - - @if (item.afterText) { - {{ item.afterText }} - } - - @if (item.showAddIcon) { - - - - } - - @if (item.showCheckIcon) { - - } - - @if (item.subText) { - - {{ item.subText }} - - } -
    - -
    - - {{ item.time }} - -
    - -
    -
  • - } -
-
-
-
-
-
-
-
diff --git a/src/app/components/dashboards/crm/crm.scss b/src/app/components/dashboards/crm/crm.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/src/app/components/dashboards/crm/crm.ts b/src/app/components/dashboards/crm/crm.ts deleted file mode 100644 index 12d5cc75..00000000 --- a/src/app/components/dashboards/crm/crm.ts +++ /dev/null @@ -1,618 +0,0 @@ -import { Chart, ChartConfiguration, Plugin } from 'chart.js'; -import { Component } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { SpkApexcharts } from "../../../@spk/charts/charts/spk-apexcharts/spk-apexcharts"; -import { ApexOptions } from 'ng-apexcharts'; -import { SpkReusableTables } from "../../../@spk/tables/spk-reusable-tables/spk-reusable-tables"; -import { SpkChartjs } from "../../../@spk/charts/charts/spk-chartjs/spk-chartjs"; -@Component({ - selector: 'app-crm', - standalone: true, - imports: [RouterModule, SpkApexcharts, SpkReusableTables, SpkChartjs], - templateUrl: './crm.html', - styleUrl: './crm.scss' -}) -export class Crm { - YourtargetisincompleteChart: ApexOptions = { - chart: { - height: 127, - width: 100, - type: 'radialBar', - }, - - series: [48], - // colors: ['#fff'], - plotOptions: { - radialBar: { - hollow: { - margin: 0, - size: '55%', - background: '#fff', - }, - dataLabels: { - name: { - offsetY: -10, - color: '#4b9bfa', - fontSize: '.625rem', - show: false, - }, - value: { - offsetY: 5, - color: '#4b9bfa', - fontSize: '.875rem', - show: true, - fontWeight: 600, - }, - }, - }, - }, - stroke: { - lineCap: 'round', - }, - labels: ['Status'], - colors: ['#fff'] - } - - topDeals = [ - { - name: 'Michael Jordan', - email: 'michael.jordan@example.com', - amount: '$2,893', - avatarImg: './assets/images/faces/10.jpg' - }, - { - name: 'Emigo Kiaren', - email: 'emigo.kiaren@gmail.com', - amount: '$4,289', - initials: 'EK', - color: 'warning', // Used for text-warning and bg-warning/10 - }, - { - name: 'Randy Origoan', - email: 'randy.origoan@gmail.com', - amount: '$6,347', - avatarImg: './assets/images/faces/12.jpg' - }, - { - name: 'George Pieterson', - email: 'george.pieterson@gmail.com', - amount: '$3,894', - initials: 'GP', - color: 'success', - }, - { - name: 'Kiara Advain', - email: 'kiaraadvain214@gmail.com', - amount: '$2,679', - initials: 'KA', - color: 'primary', - } - ]; - - profitEarnedChart: ApexOptions = { - series: [ - { - name: 'Profit Earned', - data: [44, 42, 57, 86, 58, 55, 70], - }, - { - name: 'Total Sales', - data: [34, 22, 37, 56, 21, 35, 60], - }, - ], - chart: { - type: 'bar', - height: 180, - toolbar: { - show: false, - }, - }, - grid: { - borderColor: '#f1f1f1', - strokeDashArray: 3, - }, - colors: ['rgb(132, 90, 223)', '#e4e7ed'], - plotOptions: { - bar: { - colors: { - ranges: [ - { - from: -100, - to: -46, - color: '#ebeff5', - }, - { - from: -45, - to: 0, - color: '#ebeff5', - }, - ], - }, - columnWidth: '60%', - borderRadius: 5, - }, - }, - dataLabels: { - enabled: false, - }, - stroke: { - show: true, - width: 2, - colors: undefined, - }, - legend: { - show: false, - position: 'top', - }, - yaxis: { - title: { - style: { - color: '#adb5be', - fontSize: '13px', - fontFamily: 'poppins, sans-serif', - fontWeight: 600, - cssClass: 'apexcharts-yaxis-label', - }, - }, - labels: { - formatter: function (y: number) { - return y.toFixed(0) + ''; - }, - }, - }, - xaxis: { - type: 'category', - categories: ['S', 'M', 'T', 'W', 'T', 'F', 'S'], - axisBorder: { - show: true, - color: 'rgba(119, 119, 142, 0.05)', - offsetX: 0, - offsetY: 0, - }, - axisTicks: { - show: true, - borderType: 'solid', - color: 'rgba(119, 119, 142, 0.05)', - - offsetX: 0, - offsetY: 0, - }, - labels: { - rotate: -90, - }, - }, - } - - - - crmCardsChartOptions({ series, colors }: { colors: string[], series: { data: number[], name: string }[] }): ApexOptions { - return { - chart: { - type: 'line', - height: 40, - width: 100, - sparkline: { - enabled: true - } - }, - dataLabels: { - enabled: false - }, - - stroke: { - show: true, - curve: 'smooth', - lineCap: 'butt', - colors: undefined, - width: 1.5, - dashArray: 0, - }, - fill: { - type: 'gradient', - gradient: { - opacityFrom: 0.9, - opacityTo: 0.9, - stops: [0, 98], - } - }, - series: series, - yaxis: { - min: 0, - show: false, - axisBorder: { - show: false - }, - }, - xaxis: { - labels: { - show: false, - }, - axisBorder: { - show: false - }, - }, - tooltip: { - enabled: true, - }, - colors: colors, - } - } - - crmCards = [ - { - color: 'primary', - icon: 'ti ti-users', - title: 'Total Customers', - number: '1,02,890', - chartId: 'crm-total-customers', - chartoptions: this.crmCardsChartOptions({ - series: [{ - name: 'Value', - data: [20, 14, 19, 10, 23, 20, 22, 9, 12] - }], colors: ["rgb(132, 90, 223)"], - }), - viewallTextColor: 'text-primary', - percentage: '+40%', - percentageColor: 'text-success', - }, - { - color: 'secondary', - icon: 'ti ti-wallet', - title: 'Total Revenue', - number: '$56,562', - chartId: 'crm-total-revenue', - chartoptions: this.crmCardsChartOptions({ - series: [ - { - name: 'Value', - data: [20, 14, 20, 22, 9, 12, 19, 10, 25], - }, - ], colors: ['rgb(35, 183, 229)'], - }), - viewallTextColor: 'text-secondary', - percentage: '+25%', - percentageColor: 'text-success', - }, - { - color: 'success', - icon: 'ti ti-wave-square', - title: 'Conversion Ratio', - number: '12.08%', - chartId: 'crm-conversion-ratio', - chartoptions: this.crmCardsChartOptions({ - series: [ - { - name: 'Value', - data: [20, 20, 22, 9, 14, 19, 10, 25, 12], - }, - ], colors: ['rgb(38, 191, 148)'], - }), - viewallTextColor: 'text-success', - percentage: '-12%', - percentageColor: 'text-danger', - }, - { - color: 'warning', - icon: 'ti ti-briefcase', - title: 'Total Deals', - number: '2,543', - chartId: 'crm-total-deals', - chartoptions: this.crmCardsChartOptions({ - series: [ - { - name: 'Value', - data: [20, 20, 22, 9, 12, 14, 19, 10, 25], - }, - ], colors: ['rgb(245, 184, 73)'], - }), - viewallTextColor: 'text-warning', - percentage: '+19%', - percentageColor: 'text-success', - } - ]; - - - - RevenueAnalytics: ApexOptions = { - series: [ - { - type: 'line', - name: 'Profit', - data: [ - { x: 'Jan', y: 100 }, - { x: 'Feb', y: 210 }, - { x: 'Mar', y: 180 }, - { x: 'Apr', y: 454 }, - { x: 'May', y: 230 }, - { x: 'Jun', y: 320 }, - { x: 'Jul', y: 656 }, - { x: 'Aug', y: 830 }, - { x: 'Sep', y: 350 }, - { x: 'Oct', y: 350 }, - { x: 'Nov', y: 210 }, - { x: 'Dec', y: 410 }, - ], - }, - { - type: 'line', - name: 'Revenue', - data: [ - { x: 'Jan', y: 180 }, - { x: 'Feb', y: 620 }, - { x: 'Mar', y: 476 }, - { x: 'Apr', y: 220 }, - { x: 'May', y: 520 }, - { x: 'Jun', y: 780 }, - { x: 'Jul', y: 435 }, - { x: 'Aug', y: 515 }, - { x: 'Sep', y: 738 }, - { x: 'Oct', y: 454 }, - { x: 'Nov', y: 525 }, - { x: 'Dec', y: 230 }, - ], - }, - { - type: 'area', - name: 'Sales', - data: [ - { x: 'Jan', y: 200 }, - { x: 'Feb', y: 530 }, - { x: 'Mar', y: 110 }, - { x: 'Apr', y: 130 }, - { x: 'May', y: 480 }, - { x: 'Jun', y: 520 }, - { x: 'Jul', y: 780 }, - { x: 'Aug', y: 435 }, - { x: 'Sep', y: 475 }, - { x: 'Oct', y: 738 }, - { x: 'Nov', y: 454 }, - { x: 'Dec', y: 480 }, - ], - }, - ], - chart: { - type: 'line', - height: 350, - animations: { - speed: 500, - }, - toolbar: { - show: true - }, - zoom: { - enabled: true, - }, - dropShadow: { - enabled: false, - enabledOnSeries: undefined, - top: 8, - left: 0, - blur: 3, - color: '#000', - opacity: 0.1, - }, - }, - colors: ["rgb(132, 90, 223)", "rgba(35, 183, 229, 0.85)", "rgba(119, 119, 142, 0.05)"], - dataLabels: { - enabled: false, - }, - grid: { - padding:{ - left:0, - right:0, - top:0, - bottom:0 - }, - borderColor: '#f1f1f1', - strokeDashArray: 3, - yaxis: { - lines: { - show: false - } - }, - }, - stroke: { - curve: 'smooth', - width: [2, 2, 0], - dashArray: [0, 5, 0], - }, - xaxis: { - axisTicks: { - show: false, - }, - }, - yaxis: { - - labels: { - formatter: (value: number) => `$${value}`, - }, - - }, - tooltip: { - y: [ - { - formatter: (value: number) => `$${value}`, - }, - { - formatter: (value: number) => `$${value.toFixed(0)}`, - }, - { - formatter: (value: number) => `$${value.toFixed(0)}`, - }, - ], - }, - legend: { - show: true, - offsetY: 15, - customLegendItems: ['Profit', 'Revenue', 'Sales'], - inverseOrder: true, - markers: { - size: 5 - } - }, - title: { - text: 'Revenue Analytics with sales & profit (USD)', - align: 'left', - style: { - fontSize: '.8125rem', - fontWeight: 'semibold', - color: '#8c9097', - }, - }, - markers: { - hover: { - sizeOffset: 5, - }, - }, - } - - SalesRepData = { - columns: [ - { header: 'Sales Rep', tableHeadColumn: '!text-start !text-[0.85rem] min-w-[200px]' }, - { header: 'Category', tableHeadColumn: '!text-start !text-[0.85rem]' }, - { header: 'Mail', tableHeadColumn: '!text-start !text-[0.85rem]' }, - { header: 'Location', tableHeadColumn: '!text-start !text-[0.85rem]' }, - { header: 'Date', tableHeadColumn: '!text-start !text-[0.85rem]' }, - { header: 'Action', tableHeadColumn: '!text-start !text-[0.85rem]' } - ], - rows: [ - { id: 1, name: 'Mayor Kelly', img: './assets/images/faces/4.jpg', category: 'Manufacture', mail: 'mayorkelly@gmail.com', location: 'Germany', locationClass: 'info', date: 'Sep 15 - Oct 12, 2023', checked: false }, - { id: 2, name: 'Andrew Garfield', img: './assets/images/faces/15.jpg', category: 'Development', mail: 'andrewgarfield@gmail.com', location: 'Canada', locationClass: 'primary', date: 'Apr 10 - Dec 12, 2023', checked: true }, - { id: 3, name: 'Simon Cowel', img: './assets/images/faces/11.jpg', category: 'Service', mail: 'simoncowel234@gmail.com', location: 'Europe', locationClass: 'danger', date: 'Sep 15 - Oct 12, 2023', checked: false }, - { id: 4, name: 'Mirinda Hers', img: './assets/images/faces/8.jpg', category: 'Marketing', mail: 'mirindahers@gmail.com', location: 'USA', locationClass: 'warning', date: 'Apr 14 - Dec 14, 2023', checked: true }, - { id: 5, name: 'Jacob Smith', img: './assets/images/faces/9.jpg', category: 'Social Plataform', mail: 'jacobsmith@gmail.com', location: 'Singapore', locationClass: 'success', date: 'Feb 25 - Nov 25, 2023', checked: true } - ] - }; - - - - - - - LeadsBySourceChart: ChartConfiguration<'doughnut'> = { - type: 'doughnut', - data: { - datasets: [{ - data: [32, 27, 25, 16], - backgroundColor: [ - 'rgb(132, 90, 223)', // Purple - 'rgb(35, 183, 229)', // Blue - 'rgb(245, 184, 73)', // Orange - 'rgb(38, 191, 148)', // Green - ], - // 1. INCREASE SPACING: This creates the clear gap between segments - spacing: -20, - // 2. ROUNDED BORDER: 10-20 is usually the sweet spot for this thinness - borderRadius: 20, - borderWidth: 0, - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - // 3. THINNESS: Ensure this is high enough (80% to 90%) - cutout: '86%', - plugins: { - legend: { display: false }, - tooltip: { enabled: true } - } - } - }; - recentActivity = [ - { - statusClass: 'text-primary bg-primary/10', - time: '4:45PM', - mainText: 'Update of calendar events & ', - boldMain: true, - linkText: 'Added new events in next week.', - linkClass: 'text-primary font-semibold' - }, - { - statusClass: 'text-secondary bg-secondary/10', - time: '3 hrs', - mainText: 'New theme for ', - boldText: 'Spruko Website', - afterText: ' completed', - subText: 'Lorem ipsum, dolor sit amet.' - }, - { - statusClass: 'text-success bg-success/10', - time: '22 hrs', - mainText: 'Created a ', - highlightText: 'New Task', - highlightClass: 'text-success font-semibold', - afterText: ' today', - showAddIcon: true - }, - { - statusClass: 'text-pink bg-pink/10', - time: 'Today', - mainText: 'New member ', - tagText: '@andreas gurrero', - tagClass: 'py-[0.2rem] px-[0.45rem] font-semibold rounded-sm text-pink text-[0.75em] bg-pink/10', - afterText: ' added today to AI Summit.' - }, - { - statusClass: 'text-warning bg-warning/10', - time: '22 hrs', - mainText: '32 New people joined summit.' - }, - { - statusClass: 'text-info bg-info/10', - time: '12 hrs', - mainText: 'Neon Tarly added ', - highlightText: 'Robert Bright', - highlightClass: 'text-info font-semibold', - afterText: ' to AI summit project.' - }, - { - statusClass: 'text-[#232323] dark:text-white bg-[#232323]/10 dark:bg-white/20', - time: '4 hrs', - mainText: 'Replied to new support request ', - showCheckIcon: true - }, - { - statusClass: 'text-purple bg-purple/10', - time: '4 hrs', - mainText: 'Completed documentation of ', - linkText: 'AI Summit.', - linkClass: 'text-purple underline font-semibold' - } - ]; - - deviceLeads = [ - { - label: 'Mobile', - value: '1,624', - legendClass: 'mobile', - containerClass: '!ps-4 p-[0.95rem] text-center border-e border-dashed border-defaultborder dark:border-defaultborder/10' - }, - { - label: 'Desktop', - value: '1,267', - legendClass: 'desktop', - containerClass: 'p-[0.95rem] text-center border-e border-dashed border-defaultborder dark:border-defaultborder/10' - }, - { - label: 'Laptop', - value: '1,153', - legendClass: 'laptop', - containerClass: 'p-[0.95rem] text-center border-e border-dashed border-defaultborder dark:border-defaultborder/10' - }, - { - label: 'Tablet', - value: '679', - legendClass: 'tablet', - containerClass: '!pe-4 p-[0.95rem] text-center' - } - ]; - - dealStats = [ - { title: 'Successful Deals', count: '987 deals', liClass: 'primary' }, - { title: 'Pending Deals', count: '1,073 deals', liClass: 'info' }, - { title: 'Rejected Deals', count: '1,674 deals', liClass: 'warning' }, - { title: 'Upcoming Deals', count: '921 deals', liClass: 'success' } - ]; -} - - diff --git a/src/app/components/dashboards/dashboard.routes.ts b/src/app/components/dashboards/dashboard.routes.ts deleted file mode 100644 index 7f34349f..00000000 --- a/src/app/components/dashboards/dashboard.routes.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Routes } from '@angular/router'; - -export const dashboardRoutingModule: Routes = [ - - { - path: 'crm', - loadComponent: () => import('./crm/crm').then((m) => m.Crm), - title: 'YNEX - Crm', - }, - - -]; diff --git a/src/app/components/error/error.routes.ts b/src/app/components/error/error.routes.ts deleted file mode 100644 index 04e3aeb7..00000000 --- a/src/app/components/error/error.routes.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Routes } from '@angular/router'; - -export const errorRoutingModule: Routes = [ - { - path: 'error', children: [ - { - path: 'error404', - loadComponent: () => import('./error404/error404').then( (m) => m.Error404 ), - title: 'YNEX - Error 404' - }, - - ] - } -]; - diff --git a/src/app/components/error/error404/error404.html b/src/app/components/error/error404/error404.html deleted file mode 100644 index cf0f11a3..00000000 --- a/src/app/components/error/error404/error404.html +++ /dev/null @@ -1,27 +0,0 @@ -
- - - -
-
-
-
-
-
-

404

-

Oops 😭,The page you are looking for is not available.

-
-
-

We are sorry for the inconvenience,The page you are trying to access has - been removed or never been existed.

-
-
- BACK TO HOME -
-
-
-
-
-
-
diff --git a/src/app/components/error/error404/error404.scss b/src/app/components/error/error404/error404.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/src/app/components/error/error404/error404.ts b/src/app/components/error/error404/error404.ts deleted file mode 100644 index 5f5998ac..00000000 --- a/src/app/components/error/error404/error404.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component } from '@angular/core'; -import {RouterModule} from'@angular/router'; -import { SpkParticles } from "../../../@spk/plugins&reusable/spk-particles/spk-particles"; -import {particlesOptions} from "../particleoptions" -@Component({ - selector: 'app-error404', - standalone: true, - imports: [RouterModule, SpkParticles], - templateUrl: './error404.html', - styleUrls: ['./error404.scss'] -}) -export class Error404 { -particlesOptions=particlesOptions -} - - diff --git a/src/app/components/error/particleoptions.ts b/src/app/components/error/particleoptions.ts deleted file mode 100644 index 6e7c01ec..00000000 --- a/src/app/components/error/particleoptions.ts +++ /dev/null @@ -1,36 +0,0 @@ - export const particlesOptions = { - fpsLimit: 60, // 200 is excessive; 60 is smooth and efficient - interactivity: { - events: { - onClick: { enable: true }, - onHover: { enable: true }, - resize: { enable: true } - }, - modes: { - push: { quantity: 4 }, - repulse: { distance: 200, duration: 0.4 } - } - }, - particles: { - number: { - value: 80, - density: { enable: true, value_area: 800 } - }, - color: { value: "#845adf" }, - shape: { type: "circle" }, - opacity: { value: 0.5 }, - size: { value: 2, random: true }, - line_linked: { - enable: true, - distance: 150, - color: "#d1d9e0", - opacity: 0.4, - width: 1 - }, - move: { - enable: true, - speed: 2, - out_mode: "out" - } - } - }; diff --git a/src/app/core/guards/auth.guard.ts b/src/app/core/guards/auth.guard.ts deleted file mode 100644 index cbb7a8b7..00000000 --- a/src/app/core/guards/auth.guard.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { inject } from '@angular/core'; -import { CanActivateChildFn, Router } from '@angular/router'; -import { catchError, map, of } from 'rxjs'; -import { AuthService } from '../../features/authentication/data-access/auth.service'; -import { TokenStorageService } from '../services/auth/token-storage.service'; - -export const authGuard: CanActivateChildFn = (_childRoute, state) => { - const authService = inject(AuthService); - const tokenStorage = inject(TokenStorageService); - const router = inject(Router); - - const loginUrlTree = router.createUrlTree(['/auth/login'], { - queryParams: { returnUrl: state.url }, - }); - - if (!authService.accessToken || !authService.currentUser) { - authService.logout(); - return loginUrlTree; - } - - if (!tokenStorage.isAccessTokenExpired()) { - return true; - } - - if (!authService.refreshToken || tokenStorage.isRefreshTokenExpired()) { - authService.logout(); - return loginUrlTree; - } - - return authService.refreshAccessToken().pipe( - map(() => true), - catchError(() => { - authService.logout(); - return of(loginUrlTree); - }) - ); -}; diff --git a/src/app/core/guards/guest.guard.ts b/src/app/core/guards/guest.guard.ts deleted file mode 100644 index f580219c..00000000 --- a/src/app/core/guards/guest.guard.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { inject } from '@angular/core'; -import { CanActivateFn, Router } from '@angular/router'; -import { catchError, map, of } from 'rxjs'; -import { AuthService } from '../../features/authentication/data-access/auth.service'; -import { TokenStorageService } from '../services/auth/token-storage.service'; - -const DEFAULT_AUTHENTICATED_REDIRECT = '/dashboards/crm'; - -function resolveSafeReturnUrl(returnUrl: string | null): string { - const candidate = returnUrl?.trim(); - if (!candidate) { - return DEFAULT_AUTHENTICATED_REDIRECT; - } - - const lowerCandidate = candidate.toLowerCase(); - const isSafeInternal = - candidate.startsWith('/') && - !candidate.startsWith('//') && - !lowerCandidate.includes('http://') && - !lowerCandidate.includes('https://'); - - return isSafeInternal ? candidate : DEFAULT_AUTHENTICATED_REDIRECT; -} - -export const guestGuard: CanActivateFn = (route) => { - const authService = inject(AuthService); - const tokenStorage = inject(TokenStorageService); - const router = inject(Router); - - const targetUrl = resolveSafeReturnUrl(route.queryParamMap.get('returnUrl')); - const targetUrlTree = router.createUrlTree([targetUrl]); - - if (!authService.accessToken || !authService.currentUser) { - return true; - } - - if (!tokenStorage.isAccessTokenExpired()) { - return targetUrlTree; - } - - if (!authService.refreshToken || tokenStorage.isRefreshTokenExpired()) { - authService.logout(); - return true; - } - - return authService.refreshAccessToken().pipe( - map(() => targetUrlTree), - catchError(() => { - authService.logout(); - return of(true); - }) - ); -}; \ No newline at end of file diff --git a/src/app/core/services/common/firebase.service.ts b/src/app/core/services/common/firebase.service.ts deleted file mode 100644 index 8254331d..00000000 --- a/src/app/core/services/common/firebase.service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Injectable } from '@angular/core'; -import { AngularFireModule } from '@angular/fire/compat'; -import { AngularFirestoreModule } from '@angular/fire/compat/firestore'; -import { AngularFireDatabaseModule } from '@angular/fire/compat/database'; -import { AngularFireAuthModule } from '@angular/fire/compat/auth'; -import { environment } from '../../../../environments/environment'; - -@Injectable({ - providedIn: 'root', -}) -export class FirebaseService { - constructor() { - // AngularFireModule.initializeApp(environment.firebase); - } - - getFirestore() { - return AngularFirestoreModule; - } - - getDatabase() { - return AngularFireDatabaseModule; - } - - getAuth() { - return AngularFireAuthModule; - } -} diff --git a/src/app/core/services/common/permission.service.ts b/src/app/core/services/common/permission.service.ts index 938c2da0..357de4bd 100644 --- a/src/app/core/services/common/permission.service.ts +++ b/src/app/core/services/common/permission.service.ts @@ -1,6 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable, inject, signal } from '@angular/core'; import { map, Observable, tap } from 'rxjs'; +import { API_CONFIG } from '../../config/api.config'; import { PermissionContext } from '../../models/context/context.model'; interface PermissionResponse { @@ -15,7 +16,9 @@ export class PermissionService { readonly permissionContext = signal(null); loadPermissions(): Observable { - return this.http.get(``).pipe( + return this.http.get( + `${API_CONFIG.baseUrl}${API_CONFIG.endpoints.permissionContext}` + ).pipe( map((response) => ({ roles: response.roles ?? [], permissions: response.permissions ?? [], diff --git a/src/app/core/services/context/tenant-context.service.ts b/src/app/core/services/context/tenant-context.service.ts index da6ba416..fb922757 100644 --- a/src/app/core/services/context/tenant-context.service.ts +++ b/src/app/core/services/context/tenant-context.service.ts @@ -1,6 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable, inject, signal } from '@angular/core'; import { Observable, tap } from 'rxjs'; +import { API_CONFIG } from '../../config/api.config'; import { TenantContext } from '../../models/context/context.model'; @Injectable({ providedIn: 'root' }) @@ -9,7 +10,9 @@ export class TenantContextService { readonly tenantContext = signal(null); loadTenantContext(): Observable { - return this.http.get(``).pipe( + return this.http.get( + `${API_CONFIG.baseUrl}${API_CONFIG.endpoints.tenantContext}` + ).pipe( tap((context) => { this.tenantContext.set(context); }) diff --git a/src/app/core/services/context/user-context.service.ts b/src/app/core/services/context/user-context.service.ts index 9c202794..6ea08a25 100644 --- a/src/app/core/services/context/user-context.service.ts +++ b/src/app/core/services/context/user-context.service.ts @@ -1,6 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable, inject, signal } from '@angular/core'; import { map, Observable, tap } from 'rxjs'; +import { API_CONFIG } from '../../config/api.config'; import { CurrentUserContext } from '../../models/context/context.model'; @Injectable({ providedIn: 'root' }) @@ -9,7 +10,9 @@ export class UserContextService { readonly currentUserContext = signal(null); loadCurrentUserProfile(): Observable { - return this.http.get>(``).pipe( + return this.http.get>( + `${API_CONFIG.baseUrl}${API_CONFIG.endpoints.currentUserProfile}` + ).pipe( map((response) => ({ id: response.id?.trim() ?? '', email: response.email?.trim() ?? '', diff --git a/src/app/features/authentication/pages/login/login.ts b/src/app/features/authentication/pages/login/login.ts index c35d7b02..3c829195 100644 --- a/src/app/features/authentication/pages/login/login.ts +++ b/src/app/features/authentication/pages/login/login.ts @@ -1,4 +1,5 @@ -import { ChangeDetectorRef, Component, inject } from '@angular/core'; +import { ChangeDetectorRef, Component, DestroyRef, inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormBuilder, Validators } from '@angular/forms'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { AuthService } from '../../data-access/auth.service'; @@ -18,10 +19,11 @@ import { AppContextService } from '../../../../core/services/context/app-context export class Login { private readonly formBuilder = inject(FormBuilder); private readonly cdr = inject(ChangeDetectorRef); + private readonly destroyRef = inject(DestroyRef); private readonly fallbackRoute = '/dashboards/crm'; public readonly adminLoginForm = this.formBuilder.nonNullable.group({ - username: ['superadmin@syscom.dev', [Validators.required]], + username: ['', [Validators.required]], password: ['', Validators.required], rememberMe: [false] }); @@ -71,8 +73,9 @@ export class Login { ), finalize(() => { this.isSubmitting = false; - this.cdr.detectChanges(); - }) + this.cdr.markForCheck(); + }), + takeUntilDestroyed(this.destroyRef) ) .subscribe({ next: () => { diff --git a/src/app/features/organizations/organization-onboarding/components/onboarding-tabs/onboarding-tabs.html b/src/app/features/organizations/organization-onboarding/components/onboarding-tabs/onboarding-tabs.html new file mode 100644 index 00000000..5d144fb9 --- /dev/null +++ b/src/app/features/organizations/organization-onboarding/components/onboarding-tabs/onboarding-tabs.html @@ -0,0 +1,126 @@ +
+ +
+ +
+ + + {{ currentStepIndex() + 1 }} + + Step {{ currentStepIndex() + 1 }} of {{ steps().length }}: {{ currentStepLabel() }} + + + {{ getProgressPercent() }}% + +
+ + + +
+ + + +
+
+ +
+ + +
+ @if (isEditMode()) { +
+ +
+ +
+ + +
+ +
+
+ } @else { +
+ +
+ +
+ @if (isFirstStep()) { + + } @else { + + + + } +
+ + +
+ +
+
+ + +
+ @if (!isLastStep()) { + + } @else { + + } +
+
+ } +
+ +
+ +
diff --git a/src/app/features/organizations/organization-onboarding/components/onboarding-tabs/onboarding-tabs.scss b/src/app/features/organizations/organization-onboarding/components/onboarding-tabs/onboarding-tabs.scss new file mode 100644 index 00000000..efce4921 --- /dev/null +++ b/src/app/features/organizations/organization-onboarding/components/onboarding-tabs/onboarding-tabs.scss @@ -0,0 +1,159 @@ +.tabs-header-card { + background-color: transparent; + border: none; + box-shadow: none; + padding: 0.25rem 0 0.75rem; + + @media (min-width: 640px) { + padding: 0.5rem 0 1.25rem; + } +} + +.tabs-mobile-banner { + background-color: rgba(124, 61, 235, 0.08); + color: var(--primary, #7c3deb); + + :host-context(.dark) &, + :host-context([data-theme-mode="dark"]) & { + background-color: rgba(168, 85, 247, 0.15) !important; + color: #c084fc !important; + } +} + +.tabs-bar-nav { + position: relative; +} + +.tabs-bar-list { + position: relative; + z-index: 10; + display: flex; + width: 100%; + list-style: none; + margin: 0; + padding: 0; + gap: 0.5rem; + overflow-x: auto; + border-bottom: 1px solid #e2e8f0; + + :host-context(.dark) &, + :host-context([data-theme-mode="dark"]) & { + border-bottom-color: rgba(255, 255, 255, 0.12); + } +} + +.tabs-bar-item { + flex: 1 1 0; + min-width: 0; +} + +.tab-btn { + display: flex; + width: 100%; + align-items: center; + justify-content: center; + gap: 0.5rem; + background: transparent; + border: none; + border-bottom: 3px solid transparent; + padding: 0.625rem 0.5rem; + font-size: 0.8125rem; + font-weight: 500; + white-space: nowrap; + transition: all 0.25s ease; + outline: none; + + @media (min-width: 640px) { + font-size: 0.875rem; + padding: 0.75rem 1rem; + } + + &:disabled { + cursor: default; + } + + // Pending Tab + &.pending { + color: #64748b; + + :host-context(.dark) &, + :host-context([data-theme-mode="dark"]) & { + color: #94a3b8 !important; + } + } + + // Completed Tab + &.completed { + color: #334155; + + :host-context(.dark) &, + :host-context([data-theme-mode="dark"]) & { + color: #cbd5e1 !important; + } + } + + // Active Tab + &.active { + color: var(--primary, #7c3deb); + border-bottom-color: var(--primary, #7c3deb); + font-weight: 700; + + :host-context(.dark) &, + :host-context([data-theme-mode="dark"]) & { + color: #c084fc !important; + border-bottom-color: #c084fc !important; + } + } +} + +.tab-index-badge { + display: flex; + align-items: center; + justify-content: center; + width: 1.25rem; + height: 1.25rem; + flex-shrink: 0; + border-radius: 9999px; + font-size: 0.6875rem; + font-weight: 700; + background-color: #f1f5f9; + color: inherit; + + :host-context(.dark) &, + :host-context([data-theme-mode="dark"]) & { + background-color: rgba(255, 255, 255, 0.08) !important; + } + + .tab-btn.active & { + background: var(--primary, #7c3deb); + background: linear-gradient(135deg, var(--primary, #7c3deb) 0%, #a855f7 100%); + color: #ffffff; + } + + .tab-btn.completed & { + background: var(--primary, #7c3deb); + background: linear-gradient(135deg, var(--primary, #7c3deb) 0%, #a855f7 100%); + color: #ffffff; + } +} + +.tab-label { + overflow: hidden; + text-overflow: ellipsis; + + @media (max-width: 639px) { + display: none; + } +} + +.tabs-content-box { + background-color: #ffffff; + border: 1px dashed #e2e8f0; + + :host-context(.dark) &, + :host-context([data-theme-mode="dark"]) & { + background-color: var(--bg-body, #1a1c1e) !important; + border-color: rgba(255, 255, 255, 0.12) !important; + color: #e2e8f0; + } +} diff --git a/src/app/features/organizations/organization-onboarding/components/onboarding-tabs/onboarding-tabs.ts b/src/app/features/organizations/organization-onboarding/components/onboarding-tabs/onboarding-tabs.ts new file mode 100644 index 00000000..b83873b5 --- /dev/null +++ b/src/app/features/organizations/organization-onboarding/components/onboarding-tabs/onboarding-tabs.ts @@ -0,0 +1,155 @@ +import { + ChangeDetectionStrategy, + Component, + input, + output +} from '@angular/core'; +import { Button } from '../../../../../shared/components/button/button'; + +export interface OnboardingStep { + readonly key: string; + readonly label: string; +} + +@Component({ + selector: 'onboarding-tabs', + standalone: true, + imports: [Button], + templateUrl: './onboarding-tabs.html', + styleUrl: './onboarding-tabs.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class OnboardingTabs { + readonly steps = input.required(); + readonly currentStepIndex = input.required(); + readonly completedStepIndexes = input([]); + + readonly isEditMode = input(false); + + readonly savingDraft = input(false); + readonly finishing = input(false); + readonly navigationDisabled = input(false); + + readonly stepSelected = output(); + readonly backClicked = output(); + readonly nextClicked = output(); + readonly saveDraftClicked = output(); + readonly saveChangesClicked = output(); + readonly finishClicked = output(); + readonly cancelClicked = output(); + + currentStepLabel(): string { + return this.steps()[this.currentStepIndex()]?.label ?? ''; + } + + getProgressPercent(): number { + const total = this.steps().length; + if (total === 0) return 0; + return Math.round(((this.currentStepIndex() + 1) / total) * 100); + } + + getTrackLineLeft(): string { + const total = this.steps().length; + if (total <= 0) return '0%'; + return `${100 / (total * 2)}%`; + } + + getProgressLineWidth(): string { + const total = this.steps().length; + if (total <= 1) return '0%'; + const maxSpan = (100 * (total - 1)) / total; + const progressRatio = Math.min( + 1, + Math.max(0, this.currentStepIndex() / (total - 1)) + ); + return `${maxSpan * progressRatio}%`; + } + + isFirstStep(): boolean { + return this.currentStepIndex() === 0; + } + + isLastStep(): boolean { + return this.currentStepIndex() === this.steps().length - 1; + } + + isCurrentStep(index: number): boolean { + return index === this.currentStepIndex(); + } + + isCompletedStep(index: number): boolean { + return this.completedStepIndexes().includes(index); + } + + isPendingStep(index: number): boolean { + return !this.isCurrentStep(index) && !this.isCompletedStep(index); + } + + canOpenStep(index: number): boolean { + if (this.navigationDisabled()) { + return false; + } + + return index >= 0 && index < this.steps().length; + } + + selectStep(index: number): void { + if (!this.canOpenStep(index)) { + return; + } + + this.stepSelected.emit(index); + } + + requestBack(): void { + if (this.navigationDisabled() || this.isFirstStep()) { + return; + } + + this.backClicked.emit(); + } + + requestNext(): void { + if (this.navigationDisabled() || this.isLastStep()) { + return; + } + + this.nextClicked.emit(); + } + + requestSaveDraft(): void { + if (this.navigationDisabled() || this.savingDraft()) { + return; + } + + this.saveDraftClicked.emit(); + } + + requestSaveChanges(): void { + if (this.navigationDisabled() || this.finishing() || this.savingDraft()) { + return; + } + + this.saveChangesClicked.emit(); + } + + requestFinish(): void { + if ( + this.navigationDisabled() || + this.finishing() || + !this.isLastStep() + ) { + return; + } + + this.finishClicked.emit(); + } + + requestCancel(): void { + if (this.navigationDisabled()) { + return; + } + + this.cancelClicked.emit(); + } +} diff --git a/src/app/features/organizations/organization-onboarding/data-access/organization-onboarding.endpoints.ts b/src/app/features/organizations/organization-onboarding/data-access/organization-onboarding.endpoints.ts index aeaaa533..9fb845d7 100644 --- a/src/app/features/organizations/organization-onboarding/data-access/organization-onboarding.endpoints.ts +++ b/src/app/features/organizations/organization-onboarding/data-access/organization-onboarding.endpoints.ts @@ -3,6 +3,8 @@ import { buildApiUrl } from '../../../../core/config/api-url.util'; export const ORGANIZATION_ONBOARDING_ENDPOINTS = { createDraft: buildApiUrl('masterAdmin', '/v1/organizations'), + getNextCode: buildApiUrl('masterAdmin', '/v1/organizations/next-code'), + getById: (id: string) => buildApiUrl('masterAdmin', `/v1/organizations/${encodeURIComponent(id)}`), diff --git a/src/app/features/organizations/organization-onboarding/data-access/services/organization-onboarding-state.service.ts b/src/app/features/organizations/organization-onboarding/data-access/services/organization-onboarding-state.service.ts index afea86bc..00488d2d 100644 --- a/src/app/features/organizations/organization-onboarding/data-access/services/organization-onboarding-state.service.ts +++ b/src/app/features/organizations/organization-onboarding/data-access/services/organization-onboarding-state.service.ts @@ -33,6 +33,21 @@ const INITIAL_DRAFT_STATE: OnboardingDraftState = { admin: null, }; +/** + * The onboarding API mixes PascalCase (write requests) and camelCase (typed response fields) + * for the same logical data. This raw shape lets restoreServerDraft() defensively read both + * casings from a server draft payload without resorting to `any`. + */ +type RawDraftRecord = Record; + +function firstDefined(...values: readonly unknown[]): unknown { + return values.find(value => value !== null && value !== undefined); +} + +function firstTruthy(...values: readonly unknown[]): unknown { + return values.find(value => !!value); +} + @Injectable() export class OrganizationOnboardingStateService { private readonly isEditModeState = signal(false); @@ -174,40 +189,42 @@ export class OrganizationOnboardingStateService { this.organizationIdState.set(serverDraft.id); this.isEditModeState.set(isEditMode); - const rawRecord = serverDraft as Record; - const orgCode = serverDraft.code ?? rawRecord['Code'] ?? rawRecord['code'] ?? rawRecord['organizationCode'] ?? rawRecord['OrganizationCode'] ?? null; + const rawRecord = serverDraft as unknown as RawDraftRecord; + const orgCode = firstDefined( + serverDraft.code, rawRecord['Code'], rawRecord['code'], rawRecord['organizationCode'], rawRecord['OrganizationCode'] + ) ?? null; - let basicsData = serverDraft.basics ?? rawRecord['Basics'] ?? rawRecord['basics'] ?? ( - rawRecord['name'] || rawRecord['Name'] || rawRecord['organizationName'] || rawRecord['OrganizationName'] || orgCode + let basicsData = (firstDefined(serverDraft.basics, rawRecord['Basics'], rawRecord['basics']) ?? ( + firstTruthy(rawRecord['name'], rawRecord['Name'], rawRecord['organizationName'], rawRecord['OrganizationName'], orgCode) !== undefined ? rawRecord : null - ); + )) as RawDraftRecord | null; if (basicsData) { basicsData = { ...basicsData, - code: basicsData['code'] ?? basicsData['Code'] ?? basicsData['organizationCode'] ?? basicsData['OrganizationCode'] ?? orgCode, - organizationCode: basicsData['organizationCode'] ?? basicsData['OrganizationCode'] ?? basicsData['code'] ?? basicsData['Code'] ?? orgCode, + code: firstDefined(basicsData['code'], basicsData['Code'], basicsData['organizationCode'], basicsData['OrganizationCode'], orgCode), + organizationCode: firstDefined(basicsData['organizationCode'], basicsData['OrganizationCode'], basicsData['code'], basicsData['Code'], orgCode), }; } - const localizationData = serverDraft.localization ?? rawRecord['Localization'] ?? rawRecord['localization'] ?? ( - rawRecord['defaultTimezoneId'] || rawRecord['DefaultTimezoneId'] || rawRecord['defaultCurrencyId'] || rawRecord['DefaultCurrencyId'] || rawRecord['dateFormat'] || rawRecord['DateFormat'] + const localizationData = (firstDefined(serverDraft.localization, rawRecord['Localization'], rawRecord['localization']) ?? ( + firstTruthy(rawRecord['defaultTimezoneId'], rawRecord['DefaultTimezoneId'], rawRecord['defaultCurrencyId'], rawRecord['DefaultCurrencyId'], rawRecord['dateFormat'], rawRecord['DateFormat']) !== undefined ? rawRecord : null - ); + )) as RawDraftRecord | null; - const planLimitsData = serverDraft.planLimits ?? rawRecord['PlanLimits'] ?? rawRecord['planLimits'] ?? rawRecord['plan'] ?? rawRecord['Plan'] ?? ( - rawRecord['planId'] || rawRecord['PlanId'] || rawRecord['licenseType'] || rawRecord['LicenseType'] || rawRecord['maxCompanies'] || rawRecord['MaxCompanies'] + const planLimitsData = (firstDefined(serverDraft.planLimits, rawRecord['PlanLimits'], rawRecord['planLimits'], rawRecord['plan'], rawRecord['Plan']) ?? ( + firstTruthy(rawRecord['planId'], rawRecord['PlanId'], rawRecord['licenseType'], rawRecord['LicenseType'], rawRecord['maxCompanies'], rawRecord['MaxCompanies']) !== undefined ? rawRecord : null - ); + )) as RawDraftRecord | null; - const adminData = serverDraft.admin ?? rawRecord['Admin'] ?? rawRecord['admin'] ?? rawRecord['adminContact'] ?? rawRecord['AdminContact'] ?? ( - rawRecord['adminEmail'] || rawRecord['AdminEmail'] || rawRecord['orgEmail'] || rawRecord['OrgEmail'] || rawRecord['administratorEmail'] || rawRecord['AdministratorEmail'] + const adminData = (firstDefined(serverDraft.admin, rawRecord['Admin'], rawRecord['admin'], rawRecord['adminContact'], rawRecord['AdminContact']) ?? ( + firstTruthy(rawRecord['adminEmail'], rawRecord['AdminEmail'], rawRecord['orgEmail'], rawRecord['OrgEmail'], rawRecord['administratorEmail'], rawRecord['AdministratorEmail']) !== undefined ? rawRecord : null - ); + )) as RawDraftRecord | null; this.onboardingDraftState.set({ basics: basicsData ? { ...basicsData } : null, @@ -221,17 +238,19 @@ export class OrganizationOnboardingStateService { : (serverDraft.completedStepIndexes ?? []); this.onboardingDataState.set({ + // Trust boundary: the server draft payload is only validated at runtime by the API, + // so this cast is an intentional, explicit assumption rather than a type-safety gap. basics: (isEditMode || completedIndexes.includes(0)) && basicsData - ? { ...basicsData } as OrganizationBasicsValue + ? { ...basicsData } as unknown as OrganizationBasicsValue : null, localization: (isEditMode || completedIndexes.includes(1)) && localizationData - ? { ...localizationData } as OrganizationLocalizationValue + ? { ...localizationData } as unknown as OrganizationLocalizationValue : null, planLimits: (isEditMode || completedIndexes.includes(2)) && planLimitsData - ? { ...planLimitsData } as OrganizationPlanLimitsValue + ? { ...planLimitsData } as unknown as OrganizationPlanLimitsValue : null, admin: (isEditMode || completedIndexes.includes(3)) && adminData - ? { ...adminData } as OrganizationAdminValue + ? { ...adminData } as unknown as OrganizationAdminValue : null, }); diff --git a/src/app/features/organizations/organization-onboarding/data-access/services/organization-onboarding.service.ts b/src/app/features/organizations/organization-onboarding/data-access/services/organization-onboarding.service.ts index 55a3ce2a..17fa22d8 100644 --- a/src/app/features/organizations/organization-onboarding/data-access/services/organization-onboarding.service.ts +++ b/src/app/features/organizations/organization-onboarding/data-access/services/organization-onboarding.service.ts @@ -10,6 +10,7 @@ import { FiscalYearConventionValue, NumberFormatValue, OnboardingLookupValue, + OrganizationCodePreviewDto, OrganizationLicenseType, OrganizationPlanDefaults, OrganizationPlanLookupValue, @@ -164,6 +165,24 @@ export class OrganizationOnboardingService { return this.http.post(ORGANIZATION_ONBOARDING_ENDPOINTS.finish(id), {}); } + getNextOrganizationCode(): Observable { + return this.http.get(ORGANIZATION_ONBOARDING_ENDPOINTS.getNextCode).pipe( + map(res => { + if (typeof res === 'string') { + return res; + } + if (res && typeof res === 'object') { + return res.code ?? res.nextCode ?? res.organizationCode ?? res.value ?? null; + } + return null; + }), + catchError(err => { + console.error('Failed to fetch next organization code from server', err); + return of(null); + }) + ); + } + // --- Lookups and Defaults --- searchOrganizationTypes(term: string | null, limit = 10): Observable { diff --git a/src/app/features/organizations/organization-onboarding/models/organization-onboarding.model.ts b/src/app/features/organizations/organization-onboarding/models/organization-onboarding.model.ts index cadf8686..c4575f78 100644 --- a/src/app/features/organizations/organization-onboarding/models/organization-onboarding.model.ts +++ b/src/app/features/organizations/organization-onboarding/models/organization-onboarding.model.ts @@ -205,6 +205,13 @@ export interface OrganizationPlanDefaults { readonly limitsEditable: boolean; } +export interface OrganizationCodePreviewDto { + code?: string | null; + nextCode?: string | null; + organizationCode?: string | null; + value?: string | null; +} + export interface OnboardingStepForm { validate(): boolean; getValue(): TValue; diff --git a/src/app/features/organizations/organization-onboarding/organization-onboarding.html b/src/app/features/organizations/organization-onboarding/organization-onboarding.html index 701ff693..1b85404c 100644 --- a/src/app/features/organizations/organization-onboarding/organization-onboarding.html +++ b/src/app/features/organizations/organization-onboarding/organization-onboarding.html @@ -1,8 +1,63 @@ + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ + +
+
+ @if (useTabsView()) { + + + + } @else { -
- -
- -
- -
- -
- -
- -
- -
+
+ }
(); + + get length(): number { + return this.store.size; + } + + clear(): void { + this.store.clear(); + } + + getItem(key: string): string | null { + return this.store.has(key) ? this.store.get(key)! : null; + } + + key(index: number): string | null { + return [...this.store.keys()][index] ?? null; + } + + removeItem(key: string): void { + this.store.delete(key); + } + + setItem(key: string, value: string): void { + this.store.set(key, value); + } +} + +describe('OrganizationOnboarding', () => { + let mockLocalStorage: InMemoryLocalStorage; + let realLocalStorage: Storage; + + const routerStub = { navigate: vi.fn().mockResolvedValue(true) }; + const toastrStub = { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() }; + const activatedRouteStub = { + snapshot: { queryParamMap: { get: () => null } } + }; + + const createFixture = async (): Promise> => { + TestBed.resetTestingModule(); + await TestBed.configureTestingModule({ + imports: [OrganizationOnboarding], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + { provide: ActivatedRoute, useValue: activatedRouteStub }, + { provide: Router, useValue: routerStub }, + { provide: ToastrService, useValue: toastrStub }, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(OrganizationOnboarding); + fixture.detectChanges(); + return fixture; + }; + + const toggleButton = (fixture: ComponentFixture): HTMLButtonElement => + fixture.nativeElement.querySelector('button[role="switch"]'); + + beforeEach(() => { + mockLocalStorage = new InMemoryLocalStorage(); + realLocalStorage = window.localStorage; + Object.defineProperty(window, 'localStorage', { + value: mockLocalStorage, + configurable: true, + }); + }); + + afterEach(() => { + Object.defineProperty(window, 'localStorage', { + value: realLocalStorage, + configurable: true, + }); + vi.clearAllMocks(); + }); + + it('defaults to the stepper view when no preference is stored', async () => { + const fixture = await createFixture(); + + expect(fixture.componentInstance.useTabsView()).toBe(false); + expect(fixture.debugElement.query(By.directive(OnboardingStepper))).not.toBeNull(); + expect(fixture.debugElement.query(By.directive(OnboardingTabs))).toBeNull(); + }); + + it('falls back to the stepper view when the stored preference is invalid', async () => { + mockLocalStorage.setItem(VIEW_PREFERENCE_STORAGE_KEY, 'not-a-real-value'); + const fixture = await createFixture(); + + expect(fixture.componentInstance.useTabsView()).toBe(false); + expect(fixture.debugElement.query(By.directive(OnboardingStepper))).not.toBeNull(); + }); + + it('restores the tabs view when a valid preference was previously stored', async () => { + mockLocalStorage.setItem(VIEW_PREFERENCE_STORAGE_KEY, 'tabs'); + const fixture = await createFixture(); + + expect(fixture.componentInstance.useTabsView()).toBe(true); + expect(fixture.debugElement.query(By.directive(OnboardingTabs))).not.toBeNull(); + expect(fixture.debugElement.query(By.directive(OnboardingStepper))).toBeNull(); + }); + + it('toggles the rendered child component and persists the new preference', async () => { + const fixture = await createFixture(); + + toggleButton(fixture).click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.useTabsView()).toBe(true); + expect(fixture.debugElement.query(By.directive(OnboardingTabs))).not.toBeNull(); + expect(fixture.debugElement.query(By.directive(OnboardingStepper))).toBeNull(); + expect(mockLocalStorage.getItem(VIEW_PREFERENCE_STORAGE_KEY)).toBe('tabs'); + + toggleButton(fixture).click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.useTabsView()).toBe(false); + expect(fixture.debugElement.query(By.directive(OnboardingStepper))).not.toBeNull(); + expect(mockLocalStorage.getItem(VIEW_PREFERENCE_STORAGE_KEY)).toBe('stepper'); + }); + + it('reflects aria-checked state on the switch control', async () => { + const fixture = await createFixture(); + + expect(toggleButton(fixture).getAttribute('aria-checked')).toBe('false'); + + toggleButton(fixture).click(); + fixture.detectChanges(); + + expect(toggleButton(fixture).getAttribute('aria-checked')).toBe('true'); + }); + + it('persists the preference across a simulated page reload', async () => { + const fixture = await createFixture(); + + toggleButton(fixture).click(); + fixture.detectChanges(); + expect(fixture.componentInstance.useTabsView()).toBe(true); + + // Simulate a reload: tear down the component tree but keep the same + // localStorage-backed store, then bootstrap a fresh instance. + fixture.destroy(); + const reloadedFixture = await createFixture(); + + expect(reloadedFixture.componentInstance.useTabsView()).toBe(true); + expect(reloadedFixture.debugElement.query(By.directive(OnboardingTabs))).not.toBeNull(); + }); + + it('preserves step index, completed steps, and in-progress field values across a view toggle', async () => { + const fixture = await createFixture(); + const stateService = fixture.debugElement.injector.get(OrganizationOnboardingStateService); + + stateService.markStepCompleted(0); + stateService.updateLocalizationDraft({ defaultLanguage: { id: 'en-us', label: 'English' } }); + fixture.detectChanges(); + + const basicsInstance = fixture.debugElement.query( + By.directive(OrganizationBasicsStepComponent) + ).componentInstance as OrganizationBasicsStepComponent; + basicsInstance.form.controls.organizationName.setValue('Acme Corporation'); + basicsInstance.form.controls.shortName.setValue('Acme'); + + toggleButton(fixture).click(); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + expect(fixture.componentInstance.currentStepIndex()).toBe(0); + expect(fixture.componentInstance.completedStepIndexes()).toContain(0); + + const basicsAfterToggle = fixture.debugElement.query( + By.directive(OrganizationBasicsStepComponent) + ).componentInstance as OrganizationBasicsStepComponent; + expect(basicsAfterToggle.form.controls.organizationName.value).toBe('Acme Corporation'); + expect(basicsAfterToggle.form.controls.shortName.value).toBe('Acme'); + + const localizationAfterToggle = fixture.debugElement.query( + By.directive(OrganizationLocalizationStepComponent) + ).componentInstance as OrganizationLocalizationStepComponent; + expect(localizationAfterToggle.form.controls.defaultLanguageId.value).toBe('en-us'); + }); +}); diff --git a/src/app/features/organizations/organization-onboarding/organization-onboarding.ts b/src/app/features/organizations/organization-onboarding/organization-onboarding.ts index f34f3edf..8850229d 100644 --- a/src/app/features/organizations/organization-onboarding/organization-onboarding.ts +++ b/src/app/features/organizations/organization-onboarding/organization-onboarding.ts @@ -11,10 +11,12 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router } from '@angular/router'; import { ToastrService } from 'ngx-toastr'; import { catchError, finalize, forkJoin, Observable, of, switchMap } from 'rxjs'; +import { NgTemplateOutlet } from '@angular/common'; import { OnboardingStep, OnboardingStepper } from './components/onboarding-stepper/onboarding-stepper'; +import { OnboardingTabs } from './components/onboarding-tabs/onboarding-tabs'; import { OrganizationBasicsStepComponent } from './steps/organization-basics/organization-basics'; import { OrganizationLocalizationStepComponent } from './steps/organization-localization/organization-localization'; import { OrganizationPlanLimitsStepComponent } from './steps/organization-plan-limits/organization-plan-limits'; @@ -33,11 +35,15 @@ import { } from './models/organization-onboarding.model'; import { OrganizationProvisioningRequest } from './models/organization-provisioning.model'; +const VIEW_PREFERENCE_STORAGE_KEY = 'org-onboarding:view-preference'; + @Component({ selector: 'app-organization-onboarding', standalone: true, imports: [ + NgTemplateOutlet, OnboardingStepper, + OnboardingTabs, OrganizationBasicsStepComponent, OrganizationLocalizationStepComponent, OrganizationPlanLimitsStepComponent, @@ -62,6 +68,8 @@ export class OrganizationOnboarding { readonly savingDraft = signal(false); readonly finishing = signal(false); + readonly useTabsView = signal(this.readStoredViewPreference()); + private readonly basicsStep = viewChild(OrganizationBasicsStepComponent); private readonly localizationStep = viewChild(OrganizationLocalizationStepComponent); private readonly planLimitsStep = viewChild(OrganizationPlanLimitsStepComponent); @@ -107,6 +115,12 @@ export class OrganizationOnboarding { } } + onToggleView(): void { + this.captureActiveStepDraft(); + this.setUseTabsView(!this.useTabsView()); + this.hydrateAllSteps(); + } + onStepSelected(index: number): void { if (!this.stateService.canOpenStep(index) || index === this.currentStepIndex()) { return; @@ -536,6 +550,35 @@ export class OrganizationOnboarding { }); } + private hydrateAllSteps(): void { + queueMicrotask(() => { + const draft = this.stateService.onboardingDraft(); + + if (draft.basics) this.basicsStep()?.patchValue(draft.basics); + if (draft.localization) this.localizationStep()?.patchValue(draft.localization); + if (draft.planLimits) this.planLimitsStep()?.patchValue(draft.planLimits); + if (draft.admin) this.adminStep()?.patchValue(draft.admin); + }); + } + + private setUseTabsView(useTabsView: boolean): void { + this.useTabsView.set(useTabsView); + + try { + localStorage.setItem(VIEW_PREFERENCE_STORAGE_KEY, useTabsView ? 'tabs' : 'stepper'); + } catch { + // localStorage may be unavailable (e.g. private browsing); the in-memory signal still works for this session. + } + } + + private readStoredViewPreference(): boolean { + try { + return localStorage.getItem(VIEW_PREFERENCE_STORAGE_KEY) === 'tabs'; + } catch { + return false; + } + } + private applyCountryDefaults(): void { const countryIso2 = this.stateService.basics()?.registrationCountry.iso2 ?? null; this.onboardingService.getCountryLocalizationDefaults(countryIso2) diff --git a/src/app/features/organizations/organization-onboarding/steps/organization-basics/organization-basics.ts b/src/app/features/organizations/organization-onboarding/steps/organization-basics/organization-basics.ts index 74f5c80b..9f165f50 100644 --- a/src/app/features/organizations/organization-onboarding/steps/organization-basics/organization-basics.ts +++ b/src/app/features/organizations/organization-onboarding/steps/organization-basics/organization-basics.ts @@ -70,6 +70,22 @@ export class OrganizationBasicsStepComponent implements OnboardingStepForm(null, [Validators.required]), }); + constructor() { + if (!this.stateService?.isEditMode()) { + this.fetchNextOrganizationCode(); + } + } + + private fetchNextOrganizationCode(): void { + this.onboardingService.getNextOrganizationCode() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(code => { + if (code && (this.organizationCodeControl.value === 'Pending generation' || !this.organizationCodeControl.value)) { + this.organizationCodeControl.setValue(code); + } + }); + } + readonly searchOrganizationTypes: AutocompleteSearchFn = (term, limit) => this.onboardingService.searchOrganizationTypes(term, limit).pipe( catchError(() => { diff --git a/src/app/features/organizations/organization-onboarding/steps/organization-plan-limits/organization-plan-limits.html b/src/app/features/organizations/organization-onboarding/steps/organization-plan-limits/organization-plan-limits.html index f66b05a1..ca72906a 100644 --- a/src/app/features/organizations/organization-onboarding/steps/organization-plan-limits/organization-plan-limits.html +++ b/src/app/features/organizations/organization-onboarding/steps/organization-plan-limits/organization-plan-limits.html @@ -114,23 +114,33 @@
+
+ +
+
@if (submitAttempted() && form.errors?.['accessDateRange']) {
- System Access Start Date cannot be later than System Access End Date. + System Access From Date cannot be later than System Access To Date.
} diff --git a/src/app/features/organizations/organization-onboarding/steps/organization-plan-limits/organization-plan-limits.ts b/src/app/features/organizations/organization-onboarding/steps/organization-plan-limits/organization-plan-limits.ts index 05485576..0815bbf7 100644 --- a/src/app/features/organizations/organization-onboarding/steps/organization-plan-limits/organization-plan-limits.ts +++ b/src/app/features/organizations/organization-onboarding/steps/organization-plan-limits/organization-plan-limits.ts @@ -38,7 +38,8 @@ interface OrganizationPlanLimitsFormModel { readonly maximumUsers: number | null; readonly maximumStorageGb: number | null; readonly goLiveDate: string | null; - readonly systemAccessPeriod: DateRangeValue | null; + readonly systemAccessStartDate: string | null; + readonly systemAccessEndDate: string | null; } type ThemeSelectValue = @@ -58,11 +59,11 @@ const accessDateValidator: ValidatorFn = (control: AbstractControl): ValidationE } const errors: ValidationErrors = {}; - const { licenseType, systemAccessPeriod } = value; - const from = systemAccessPeriod?.from ?? null; - const to = systemAccessPeriod?.to ?? null; + const { licenseType, systemAccessStartDate, systemAccessEndDate } = value; + const from = systemAccessStartDate ?? null; + const to = systemAccessEndDate ?? null; - if (licenseType === 'Trial' && (!systemAccessPeriod || !to)) { + if (licenseType === 'Trial' && !to) { errors['trialEndDateRequired'] = true; } @@ -107,7 +108,8 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm(null, [Validators.required, Validators.min(1)]), maximumStorageGb: this.formBuilder.control(null, [Validators.required, Validators.min(1)]), goLiveDate: this.formBuilder.control(null, [Validators.required]), - systemAccessPeriod: this.formBuilder.control(null, [Validators.required]), + systemAccessStartDate: this.formBuilder.control(null, [Validators.required]), + systemAccessEndDate: this.formBuilder.control(null), }, { validators: accessDateValidator }); readonly requiresAccessEndDate = computed(() => this.form.controls.licenseType.value === 'Trial'); @@ -132,12 +134,12 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm { if (licenseType === 'Trial') { - this.form.controls.systemAccessPeriod.addValidators([Validators.required]); + this.form.controls.systemAccessEndDate.addValidators([Validators.required]); } else { - this.form.controls.systemAccessPeriod.removeValidators([Validators.required]); + this.form.controls.systemAccessEndDate.removeValidators([Validators.required]); } - this.form.controls.systemAccessPeriod.updateValueAndValidity({ emitEvent: false }); + this.form.controls.systemAccessEndDate.updateValueAndValidity({ emitEvent: false }); }); this.form.controls.subscriptionPlanId.valueChanges @@ -185,9 +187,8 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm { const value = this.form.getRawValue() as OrganizationPlanLimitsFormModel; - const period = value.systemAccessPeriod; return { subscriptionPlan: this.planSelection() ?? undefined, @@ -217,8 +217,8 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm & Record): void { if (!value) return; - const rawLicenseType = value.licenseType ?? value['LicenseType'] ?? value['licenseType']; + const rawLicenseType = value['licenseType'] ?? value['LicenseType']; const normalizedLicenseType = mapApiToLicenseType(rawLicenseType); - const planId = value.subscriptionPlan?.id ?? value['PlanId'] ?? value['subscriptionPlanId'] ?? value['planId'] ?? null; + const planId = value['subscriptionPlan']?.id ?? value['PlanId'] ?? value['subscriptionPlanId'] ?? value['planId'] ?? null; - const subPlan = value.subscriptionPlan ?? (value['SubscriptionPlan'] || value['plan'] || value['Plan'] ? { + const subPlan = value['subscriptionPlan'] ?? (value['SubscriptionPlan'] || value['plan'] || value['Plan'] ? { id: value['SubscriptionPlan']?.id ?? value['plan']?.id ?? value['Plan']?.id ?? planId, label: value['SubscriptionPlan']?.name ?? value['SubscriptionPlan']?.label ?? value['planName'] ?? value['PlanName'] ?? value['plan']?.name ?? '', code: value['SubscriptionPlan']?.code ?? value['plan']?.code ?? value['Plan']?.code ?? '', } : planId ? { id: planId, label: value['PlanName'] ?? value['planName'] ?? '', code: '' } : null); - const from = value.systemAccessStartDate ?? value['SystemAccessFrom'] ?? value['systemAccessFrom'] ?? value['systemAccessStartDate'] ?? null; - const to = value.systemAccessEndDate ?? value['SystemAccessTo'] ?? value['systemAccessTo'] ?? value['systemAccessEndDate'] ?? null; - const systemAccessPeriod: DateRangeValue | null = (from || to) ? { from, to } : null; + const period = value['systemAccessPeriod'] as DateRangeValue | undefined; + const from = value['systemAccessStartDate'] ?? value['SystemAccessFrom'] ?? value['systemAccessFrom'] ?? period?.from ?? null; + const to = value['systemAccessEndDate'] ?? value['SystemAccessTo'] ?? value['systemAccessTo'] ?? period?.to ?? null; this.form.patchValue({ subscriptionPlanId: planId, licenseType: normalizedLicenseType, - maximumCompanies: value.maximumCompanies ?? value['MaxCompanies'] ?? value['maxCompanies'] ?? null, - maximumUsers: value.maximumUsers ?? value['MaxUsers'] ?? value['maxUsers'] ?? null, - maximumStorageGb: value.maximumStorageGb ?? value['MaxStorageGb'] ?? value['maxStorageGb'] ?? null, - goLiveDate: value.goLiveDate ?? value['GoLiveDate'] ?? value['goLiveDate'] ?? null, - systemAccessPeriod, + maximumCompanies: value['maximumCompanies'] ?? value['MaxCompanies'] ?? value['maxCompanies'] ?? null, + maximumUsers: value['maximumUsers'] ?? value['MaxUsers'] ?? value['maxUsers'] ?? null, + maximumStorageGb: value['maximumStorageGb'] ?? value['MaxStorageGb'] ?? value['maxStorageGb'] ?? null, + goLiveDate: value['goLiveDate'] ?? value['GoLiveDate'] ?? null, + systemAccessStartDate: from, + systemAccessEndDate: to, }, { emitEvent: false }); if (subPlan && subPlan.id) { @@ -265,7 +266,7 @@ export class OrganizationPlanLimitsStepComponent implements OnboardingStepForm
@if(showFilterButton()){ -
+
+ className="!rounded-full shadow-sm !whitespace-nowrap !mb-0" (buttonClicked)="onFilterClick($event)" />
} @if(showAddButton()){ -
+
+ className="!rounded-full shadow-sm !mb-0" (buttonClicked)="onAddClick($event)" />
} @@ -27,7 +27,7 @@
} --> @if (showSearch()) { -
+
diff --git a/src/environments/environment.model.ts b/src/environments/environment.model.ts index c741a1f2..fb64671b 100644 --- a/src/environments/environment.model.ts +++ b/src/environments/environment.model.ts @@ -1,13 +1,3 @@ -export interface AppFirebaseConfig { - apiKey: string; - authDomain: string; - projectId: string; - storageBucket: string; - messagingSenderId: string; - appId: string; - measurementId: string; -} - export interface AppSessionTimeoutConfig { warningAfterMs: number; logoutAfterMs: number; @@ -20,5 +10,4 @@ export interface AppEnvironment { masterAdmin: string; }; sessionTimeout: AppSessionTimeoutConfig; - firebase: AppFirebaseConfig; } diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 828c2a61..b0b2a45f 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -10,13 +10,4 @@ export const environment: AppEnvironment = { warningAfterMs: 25 * 60 * 1000, logoutAfterMs: 30 * 60 * 1000, }, - firebase: { - apiKey: '********************************', - authDomain: '********************************', - projectId: '********************************', - storageBucket: '********************************', - messagingSenderId: '********************************', - appId: '********************************', - measurementId: '********************************', - }, }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 9203fb91..278da4e0 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -16,15 +16,6 @@ export const environment: AppEnvironment = { warningAfterMs: 25 * 60 * 1000, logoutAfterMs: 30 * 60 * 1000, }, - firebase: { - apiKey: '********************************', - authDomain: '********************************', - projectId: '********************************', - storageBucket: '********************************', - messagingSenderId: '********************************', - appId: '********************************', - measurementId: '********************************', - }, }; /* diff --git a/src/styles.scss b/src/styles.scss index 0ccd3fc7..8edb8506 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -54,8 +54,8 @@ body { line-height: 1.25; } -/* Remove unwanted bottom margin from primary-full buttons */ -.ti-btn-primary-full { +/* Remove unwanted bottom margin from buttons */ +.ti-btn { margin-bottom: 0 !important; }