diff --git a/apps/gauzy/src/app/app.component.ts b/apps/gauzy/src/app/app.component.ts index 3f688a1c5c1..28fc8c5e418 100644 --- a/apps/gauzy/src/app/app.component.ts +++ b/apps/gauzy/src/app/app.component.ts @@ -21,6 +21,7 @@ import { ISelectorVisibility, JitsuService, LanguagesService, + NavigationService, SelectorBuilderService, SeoService, Store @@ -47,7 +48,8 @@ export class AppComponent implements OnInit, AfterViewInit { private readonly _router: Router, private readonly _activatedRoute: ActivatedRoute, private readonly _selectorBuilderService: SelectorBuilderService, - private readonly _dateRangePickerBuilderService: DateRangePickerBuilderService + private readonly _dateRangePickerBuilderService: DateRangePickerBuilderService, + private readonly _navigationService: NavigationService ) { this.getActivateRouterDataEvent(); this.getPreferredLanguage(); @@ -200,18 +202,25 @@ export class AppComponent implements OnInit, AfterViewInit { tap( ({ datePicker, - dates + dates, + bookmarkParams }: { datePicker: IDatePickerConfig; dates: IDateRangePicker; selectors: ISelectorVisibility; + bookmarkParams: Record; }) => { + // Date Range Picker if (isNotEmpty(dates)) { this._dateRangePickerBuilderService.setDateRangePicker(dates); } + // Set Date Range Picker Default Unit const datePickerConfig = Object.assign({}, DEFAULT_DATE_PICKER_CONFIG, datePicker); this._dateRangePickerBuilderService.setDatePickerConfig(datePickerConfig); + + // Create query parameters URL builder + this._navigationService.updateQueryParams(bookmarkParams); } ), // Automatically unsubscribe when the component is destroyed diff --git a/apps/gauzy/src/app/pages/dashboard/dashboard-routing.module.ts b/apps/gauzy/src/app/pages/dashboard/dashboard-routing.module.ts index 04f10478c61..ca6ed18cf58 100644 --- a/apps/gauzy/src/app/pages/dashboard/dashboard-routing.module.ts +++ b/apps/gauzy/src/app/pages/dashboard/dashboard-routing.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { PermissionsGuard } from '@gauzy/ui-core/core'; +import { PermissionsGuard, BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { PermissionsEnum } from '@gauzy/contracts'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { DashboardComponent } from './dashboard.component'; @@ -33,7 +33,8 @@ const routes: Routes = [ } }, resolve: { - dates: DateRangePickerResolver + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver } }, { @@ -48,7 +49,8 @@ const routes: Routes = [ } }, resolve: { - dates: DateRangePickerResolver + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver } }, { @@ -60,7 +62,8 @@ const routes: Routes = [ } }, resolve: { - dates: DateRangePickerResolver + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver } }, { @@ -76,7 +79,8 @@ const routes: Routes = [ } }, resolve: { - dates: DateRangePickerResolver + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver } }, { @@ -99,7 +103,8 @@ const routes: Routes = [ } }, resolve: { - dates: DateRangePickerResolver + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver } } ] diff --git a/apps/gauzy/src/app/pages/employees/activity/activity.module.ts b/apps/gauzy/src/app/pages/employees/activity/activity.module.ts index 65a76cc3ed2..696656b6c96 100644 --- a/apps/gauzy/src/app/pages/employees/activity/activity.module.ts +++ b/apps/gauzy/src/app/pages/employees/activity/activity.module.ts @@ -2,7 +2,7 @@ import { Inject, NgModule } from '@angular/core'; import { ROUTES, RouterModule } from '@angular/router'; import { NbCardModule, NbSpinnerModule } from '@nebular/theme'; import { TranslateModule } from '@ngx-translate/core'; -import { PageRouteRegistryService } from '@gauzy/ui-core/core'; +import { BookmarkQueryParamsResolver, PageRouteRegistryService } from '@gauzy/ui-core/core'; import { ActivityItemModule, DateRangePickerResolver, @@ -92,7 +92,10 @@ export class ActivityModule { title: 'ACTIVITY.APPS', // Register the title for the page type: 'apps' // Register the type for the page }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } }); // Register URL Activity Page Routes @@ -111,7 +114,10 @@ export class ActivityModule { title: 'ACTIVITY.VISITED_SITES', // Register the title for the page type: 'urls' // Register the type for the page }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } }); // Set the flag to true diff --git a/apps/gauzy/src/app/pages/employees/activity/screenshot/screenshot-routing.module.ts b/apps/gauzy/src/app/pages/employees/activity/screenshot/screenshot-routing.module.ts index 1873b8741ee..464eca7d3e9 100644 --- a/apps/gauzy/src/app/pages/employees/activity/screenshot/screenshot-routing.module.ts +++ b/apps/gauzy/src/app/pages/employees/activity/screenshot/screenshot-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { ScreenshotComponent } from './screenshot/screenshot.component'; @@ -17,7 +18,10 @@ const routes: Routes = [ isDisableFutureDate: true } }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } } ]; diff --git a/apps/gauzy/src/app/pages/employees/activity/time-activities/time-activities-routing.module.ts b/apps/gauzy/src/app/pages/employees/activity/time-activities/time-activities-routing.module.ts index ea890a10782..31640b16a91 100644 --- a/apps/gauzy/src/app/pages/employees/activity/time-activities/time-activities-routing.module.ts +++ b/apps/gauzy/src/app/pages/employees/activity/time-activities/time-activities-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { TimeActivitiesComponent } from './time-activities/time-activities.component'; @@ -16,7 +17,10 @@ const routes: Routes = [ isSingleDatePicker: true } }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } } ]; diff --git a/apps/gauzy/src/app/pages/employees/timesheet/calendar/calendar-routing.module.ts b/apps/gauzy/src/app/pages/employees/timesheet/calendar/calendar-routing.module.ts index 68b87dc0e31..33cbb9df2dc 100644 --- a/apps/gauzy/src/app/pages/employees/timesheet/calendar/calendar-routing.module.ts +++ b/apps/gauzy/src/app/pages/employees/timesheet/calendar/calendar-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { CalendarComponent } from './calendar/calendar.component'; @@ -12,7 +13,10 @@ const routes: Routes = [ unitOfTime: 'week' } }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } } ]; diff --git a/apps/gauzy/src/app/pages/employees/timesheet/daily/daily-routing.module.ts b/apps/gauzy/src/app/pages/employees/timesheet/daily/daily-routing.module.ts index bfd14652a8e..059d3e2fcff 100644 --- a/apps/gauzy/src/app/pages/employees/timesheet/daily/daily-routing.module.ts +++ b/apps/gauzy/src/app/pages/employees/timesheet/daily/daily-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { DailyComponent } from './daily/daily.component'; @@ -14,7 +15,10 @@ const routes: Routes = [ isSingleDatePicker: true } }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } } ]; diff --git a/apps/gauzy/src/app/pages/employees/timesheet/weekly/weekly-routing.module.ts b/apps/gauzy/src/app/pages/employees/timesheet/weekly/weekly-routing.module.ts index e1b5657eefc..28c102c2531 100644 --- a/apps/gauzy/src/app/pages/employees/timesheet/weekly/weekly-routing.module.ts +++ b/apps/gauzy/src/app/pages/employees/timesheet/weekly/weekly-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { WeeklyComponent } from './weekly/weekly.component'; @@ -13,7 +14,10 @@ const routes: Routes = [ isLockDatePicker: true } }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } } ]; diff --git a/apps/gauzy/src/app/pages/reports/amounts-owed-report/amounts-owed-report-routing.module.ts b/apps/gauzy/src/app/pages/reports/amounts-owed-report/amounts-owed-report-routing.module.ts index 7af94d4a39d..719cfd8f8dd 100644 --- a/apps/gauzy/src/app/pages/reports/amounts-owed-report/amounts-owed-report-routing.module.ts +++ b/apps/gauzy/src/app/pages/reports/amounts-owed-report/amounts-owed-report-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { AmountsOwedReportComponent } from './amounts-owed-report/amounts-owed-report.component'; @@ -12,7 +13,10 @@ const routes: Routes = [ unitOfTime: 'week' } }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } } ]; diff --git a/apps/gauzy/src/app/pages/reports/apps-urls-report/apps-urls-report-routing.module.ts b/apps/gauzy/src/app/pages/reports/apps-urls-report/apps-urls-report-routing.module.ts index dfb8800d79e..3771886456d 100644 --- a/apps/gauzy/src/app/pages/reports/apps-urls-report/apps-urls-report-routing.module.ts +++ b/apps/gauzy/src/app/pages/reports/apps-urls-report/apps-urls-report-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { AppsUrlsReportComponent } from './apps-urls-report/apps-urls-report.component'; @@ -12,7 +13,10 @@ const routes: Routes = [ unitOfTime: 'week' } }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } } ]; diff --git a/apps/gauzy/src/app/pages/reports/client-budgets-report/client-budgets-report-routing.module.ts b/apps/gauzy/src/app/pages/reports/client-budgets-report/client-budgets-report-routing.module.ts index 6c7ff57d5a2..d1c7331c622 100644 --- a/apps/gauzy/src/app/pages/reports/client-budgets-report/client-budgets-report-routing.module.ts +++ b/apps/gauzy/src/app/pages/reports/client-budgets-report/client-budgets-report-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { ClientBudgetsReportComponent } from './client-budgets-report/client-budgets-report.component'; @@ -12,7 +13,10 @@ const routes: Routes = [ unitOfTime: 'week' } }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } } ]; diff --git a/apps/gauzy/src/app/pages/reports/expenses-report/expenses-report-routing.module.ts b/apps/gauzy/src/app/pages/reports/expenses-report/expenses-report-routing.module.ts index 85db03a12ad..a792f72b962 100644 --- a/apps/gauzy/src/app/pages/reports/expenses-report/expenses-report-routing.module.ts +++ b/apps/gauzy/src/app/pages/reports/expenses-report/expenses-report-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { ExpensesReportComponent } from './expenses-report/expenses-report.component'; @@ -12,7 +13,10 @@ const routes: Routes = [ unitOfTime: 'week' } }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } } ]; diff --git a/apps/gauzy/src/app/pages/reports/manual-time/manual-time-routing.module.ts b/apps/gauzy/src/app/pages/reports/manual-time/manual-time-routing.module.ts index e0f91e24b32..99356f820c5 100644 --- a/apps/gauzy/src/app/pages/reports/manual-time/manual-time-routing.module.ts +++ b/apps/gauzy/src/app/pages/reports/manual-time/manual-time-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { ManualTimeComponent } from './manual-time/manual-time.component'; @@ -12,7 +13,10 @@ const routes: Routes = [ unitOfTime: 'week' } }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } } ]; diff --git a/apps/gauzy/src/app/pages/reports/payment-report/payment-report-routing.module.ts b/apps/gauzy/src/app/pages/reports/payment-report/payment-report-routing.module.ts index 5bf6bdc1993..064efd1f2f1 100644 --- a/apps/gauzy/src/app/pages/reports/payment-report/payment-report-routing.module.ts +++ b/apps/gauzy/src/app/pages/reports/payment-report/payment-report-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { PaymentReportComponent } from './payment-report/payment-report.component'; @@ -15,7 +16,10 @@ const routes: Routes = [ employee: false } }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } } ]; diff --git a/apps/gauzy/src/app/pages/reports/project-budgets-report/project-budgets-report-routing.module.ts b/apps/gauzy/src/app/pages/reports/project-budgets-report/project-budgets-report-routing.module.ts index 90ef92cec37..eac2729cd37 100644 --- a/apps/gauzy/src/app/pages/reports/project-budgets-report/project-budgets-report-routing.module.ts +++ b/apps/gauzy/src/app/pages/reports/project-budgets-report/project-budgets-report-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { ProjectBudgetsReportComponent } from './project-budgets-report/project-budgets-report.component'; @@ -12,7 +13,10 @@ const routes: Routes = [ unitOfTime: 'week' } }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } } ]; diff --git a/apps/gauzy/src/app/pages/reports/time-limit-report/time-limit-report-routing.module.ts b/apps/gauzy/src/app/pages/reports/time-limit-report/time-limit-report-routing.module.ts index 3359401d40e..00c43432e41 100644 --- a/apps/gauzy/src/app/pages/reports/time-limit-report/time-limit-report-routing.module.ts +++ b/apps/gauzy/src/app/pages/reports/time-limit-report/time-limit-report-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { TimeLimitReportComponent } from './time-limit-report/time-limit-report.component'; @@ -14,7 +15,10 @@ const routes: Routes = [ unitOfTime: 'week' } }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } } ]; diff --git a/apps/gauzy/src/app/pages/reports/time-reports/time-reports-routing.module.ts b/apps/gauzy/src/app/pages/reports/time-reports/time-reports-routing.module.ts index 7c73c9b7f68..a96a2af0d9d 100644 --- a/apps/gauzy/src/app/pages/reports/time-reports/time-reports-routing.module.ts +++ b/apps/gauzy/src/app/pages/reports/time-reports/time-reports-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { TimeReportsComponent } from './time-reports/time-reports.component'; @@ -12,7 +13,10 @@ const routes: Routes = [ unitOfTime: 'week' } }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } } ]; diff --git a/apps/gauzy/src/app/pages/reports/weekly-time-reports/weekly-time-reports-routing.module.ts b/apps/gauzy/src/app/pages/reports/weekly-time-reports/weekly-time-reports-routing.module.ts index 0fdbb4fad34..9539ae10428 100644 --- a/apps/gauzy/src/app/pages/reports/weekly-time-reports/weekly-time-reports-routing.module.ts +++ b/apps/gauzy/src/app/pages/reports/weekly-time-reports/weekly-time-reports-routing.module.ts @@ -1,5 +1,6 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +import { BookmarkQueryParamsResolver } from '@gauzy/ui-core/core'; import { DateRangePickerResolver } from '@gauzy/ui-core/shared'; import { WeeklyTimeReportsComponent } from './weekly-time-reports/weekly-time-reports.component'; @@ -13,7 +14,10 @@ const routes: Routes = [ isLockDatePicker: true } }, - resolve: { dates: DateRangePickerResolver } + resolve: { + dates: DateRangePickerResolver, + bookmarkParams: BookmarkQueryParamsResolver + } } ]; diff --git a/packages/ui-core/core/src/lib/resolvers/bookmark-query-params.resolver.ts b/packages/ui-core/core/src/lib/resolvers/bookmark-query-params.resolver.ts new file mode 100644 index 00000000000..34945292581 --- /dev/null +++ b/packages/ui-core/core/src/lib/resolvers/bookmark-query-params.resolver.ts @@ -0,0 +1,54 @@ +import { inject } from '@angular/core'; +import { ActivatedRouteSnapshot, ResolveFn } from '@angular/router'; +import { EMPTY, Observable, of } from 'rxjs'; +import { Store } from '../services/store/store.service'; +import { DEFAULT_SELECTOR_VISIBILITY } from '../services/selector-builder/selector-builder.service'; +import { ErrorHandlingService } from '../services/notification/error-handling.service'; + +/** + * The `BookmarkQueryParamsResolver` is responsible for constructing a set of query parameters + * based on selected entities such as organization, employee, project, and team. + * It retrieves the necessary state using injected services (`Store` and `SelectorBuilderService`) + * and dynamically builds the query parameters if the respective selectors are active. + * + * @returns {Observable>} - An observable that emits a set of query parameters + * with entity IDs mapped to their respective keys (e.g., `organizationId`, `employeeId`, etc.). + * If there is an error or no matching selectors, an empty observable is returned. + */ +export const BookmarkQueryParamsResolver: ResolveFn>> = ( + route: ActivatedRouteSnapshot +): Observable> => { + // Get injected services + const _store = inject(Store); + const _errorHandlingService = inject(ErrorHandlingService); + + try { + // Get selectors visibility and selected entities + const selectors = Object.assign({}, DEFAULT_SELECTOR_VISIBILITY, route.data?.selectors); + const { selectedOrganization, selectedEmployee, selectedProject, selectedTeam } = _store; + + // Map selectors to entity IDs + const paramMappings: Record = { + organizationId: selectedOrganization?.id, + employeeId: selectedEmployee?.id, + projectId: selectedProject?.id, + teamId: selectedTeam?.id + }; + + // Create queryParams by filtering out undefined values and matching selectors + const queryParams: Record = Object.entries(paramMappings).reduce((params, [key, value]) => { + if (selectors[key.replace('Id', '')] && value) { + params[key] = value; + } + return params; + }, {}); + + // Return null if organization ID is missing + return of(queryParams); // Allow the route to resolve normally + } catch (error) { + // Handle any synchronous errors and redirect to "new integration" page + console.log(`Error resolving entity query params: ${error}`); + _errorHandlingService.handleError(error); + return EMPTY; // Return an empty observable + } +}; diff --git a/packages/ui-core/core/src/lib/resolvers/index.ts b/packages/ui-core/core/src/lib/resolvers/index.ts index 67043e69a48..70c7d1e6f96 100644 --- a/packages/ui-core/core/src/lib/resolvers/index.ts +++ b/packages/ui-core/core/src/lib/resolvers/index.ts @@ -1,6 +1,7 @@ +export * from './bookmark-query-params.resolver'; export * from './employee-count.resolver'; -export * from './integration.resolver'; export * from './integration-entity-setting.resolver'; export * from './integration-setting.resolver'; +export * from './integration.resolver'; export * from './onboarding.resolver'; export * from './user.resolver'; diff --git a/packages/ui-core/core/src/lib/services/navigation/navigation.service.ts b/packages/ui-core/core/src/lib/services/navigation/navigation.service.ts index a334754e8bc..b4d14f2d0d7 100644 --- a/packages/ui-core/core/src/lib/services/navigation/navigation.service.ts +++ b/packages/ui-core/core/src/lib/services/navigation/navigation.service.ts @@ -8,9 +8,9 @@ import { isNotNullOrUndefinedOrEmpty } from '@gauzy/ui-core/common'; }) export class NavigationService { constructor( - private readonly router: Router, - private readonly activatedRoute: ActivatedRoute, - private readonly location: Location + private readonly _router: Router, + private readonly _activatedRoute: ActivatedRoute, + private readonly _location: Location ) {} /** @@ -23,8 +23,8 @@ export class NavigationService { queryParams: { [key: string]: string | string[] | boolean }, queryParamsHandling: QueryParamsHandling = 'merge' ): Promise { - await this.router.navigate(route, { - relativeTo: this.activatedRoute, + await this._router.navigate(route, { + relativeTo: this._activatedRoute, queryParams, queryParamsHandling }); @@ -40,7 +40,7 @@ export class NavigationService { queryParams: { [key: string]: string | string[] | boolean }, queryParamsHandling: QueryParamsHandling = 'merge' ): Promise { - const currentUrl = this.location.path(); + const currentUrl = this._location.path(); const [currentUrlTree, currentQueryParamsString] = currentUrl.split('?'); // Split current URL to get the path and query params let existingQueryParams: { [key: string]: string | string[] | boolean } = {}; @@ -84,7 +84,7 @@ export class NavigationService { const newUrl = [currentUrlTree, queryParamsString].filter(Boolean).join('?'); // Combine current URL with updated query params // Replace the browser's URL without triggering navigation - this.location.replaceState(newUrl); + this._location.replaceState(newUrl); } /** diff --git a/packages/ui-core/core/src/lib/services/selector-builder/date-range-picker-builder.service.ts b/packages/ui-core/core/src/lib/services/selector-builder/date-range-picker-builder.service.ts index e758b96a843..25f7521f583 100644 --- a/packages/ui-core/core/src/lib/services/selector-builder/date-range-picker-builder.service.ts +++ b/packages/ui-core/core/src/lib/services/selector-builder/date-range-picker-builder.service.ts @@ -5,6 +5,7 @@ import { isNotEmpty } from '@gauzy/ui-core/common'; import { IDateRangePicker } from '@gauzy/contracts'; import { IDatePickerConfig } from './selector-builder-types'; +// Define the default date picker configuration export const DEFAULT_DATE_PICKER_CONFIG: IDatePickerConfig = { unitOfTime: 'week', isLockDatePicker: false, @@ -14,53 +15,64 @@ export const DEFAULT_DATE_PICKER_CONFIG: IDatePickerConfig = { isDisablePastDate: false }; +// Define the default date range picker export const DEFAULT_DATE_RANGE: IDateRangePicker = { startDate: moment().startOf(DEFAULT_DATE_PICKER_CONFIG.unitOfTime).toDate(), endDate: moment().endOf(DEFAULT_DATE_PICKER_CONFIG.unitOfTime).toDate(), isCustomDate: false }; -@Injectable({ - providedIn: 'root' -}) +@Injectable({ providedIn: 'root' }) export class DateRangePickerBuilderService { + public dates$: BehaviorSubject = new BehaviorSubject(DEFAULT_DATE_RANGE); private _datePickerConfig$: BehaviorSubject = new BehaviorSubject(null); public datePickerConfig$: Observable = this._datePickerConfig$.asObservable(); - - public dates$: BehaviorSubject = new BehaviorSubject(DEFAULT_DATE_RANGE); - private _selectedDateRange$: BehaviorSubject = new BehaviorSubject(null); public selectedDateRange$: Observable = this._selectedDateRange$.asObservable(); /** - * Getter & Setter for selected date range + * Sets a new selected date range. + * + * @param range - The new date range to set. */ - get selectedDateRange(): IDateRangePicker { - return this.dates$.getValue(); - } set selectedDateRange(range: IDateRangePicker) { if (isNotEmpty(range)) { this._selectedDateRange$.next(range); + this.dates$.next(range); } } /** - * Override date range picker default configuration + * Gets the currently selected date range. + */ + get selectedDateRange(): IDateRangePicker { + return this.dates$.getValue(); + } + + /** + * Gets the current date picker configuration. + */ + get datePickerConfig(): IDatePickerConfig { + return this._datePickerConfig$.getValue(); + } + + /** + * Sets a new date picker configuration. * - * @param options + * @param config - The new configuration to set. */ - setDatePickerConfig(options: IDatePickerConfig) { - if (isNotEmpty(options)) { - this._datePickerConfig$.next(options); + setDatePickerConfig(config: IDatePickerConfig) { + if (isNotEmpty(config)) { + this._datePickerConfig$.next(config); } } /** - * Override date range picker default values + * Updates the date range picker with new start and end dates. * - * @param dates An object containing the start date, end date, and possibly other properties related to the date range picker. + * @param dates - An object containing the start date and end date. */ - async setDateRangePicker(dates: IDateRangePicker) { + setDateRangePicker(dates: IDateRangePicker) { // Check if dates object is not empty if (isNotEmpty(dates)) { // Update the BehaviorSubject `dates$` with the new dates @@ -75,19 +87,12 @@ export class DateRangePickerBuilderService { */ refreshDateRangePicker(date: moment.Moment) { // Extract the unit of time from the current date picker configuration - const { unitOfTime } = this._datePickerConfig$.getValue(); - + const unitOfTime = this.datePickerConfig.unitOfTime; // Calculate the start and end dates based on the provided date and unit of time - const startDate = moment(date).startOf(unitOfTime); - const endDate = moment(date).endOf(unitOfTime); - - // Update the date range picker with the new start and end dates - this.setDateRangePicker({ - startDate: startDate.toDate(), - endDate: endDate.toDate() - }); + const startDate = moment(date).startOf(unitOfTime).toDate(); + const endDate = moment(date).endOf(unitOfTime).toDate(); - // Maintain the current date picker configuration - this.setDatePickerConfig(this._datePickerConfig$.getValue()); + this.setDateRangePicker({ startDate, endDate }); // Update the date range picker with the new start and end dates + this.setDatePickerConfig(this._datePickerConfig$.getValue()); // Maintain the current date picker configuration } } diff --git a/packages/ui-core/core/src/lib/services/store/store.service.ts b/packages/ui-core/core/src/lib/services/store/store.service.ts index 3d5815c5d5a..e66f6efe6e9 100644 --- a/packages/ui-core/core/src/lib/services/store/store.service.ts +++ b/packages/ui-core/core/src/lib/services/store/store.service.ts @@ -175,38 +175,80 @@ export class Store { ); } + // Getter and Setter for selectedOrganization get selectedOrganization(): IOrganization { - const { selectedOrganization } = this.appQuery.getValue(); - return selectedOrganization; + /** + * Retrieves the currently selected organization from the application's state. + * + * @returns {IOrganization} - The selected organization object. + */ + return this.appQuery.getValue().selectedOrganization; } - set selectedEmployee(employee: ISelectedEmployee) { - this.appStore.update({ - selectedEmployee: employee - }); + set selectedOrganization(organization: IOrganization) { + /** + * Updates the selected organization in the application's state. + * + * @param {IOrganization} organization - The organization object to be set as the selected organization. + */ + this.appStore.update({ selectedOrganization: organization }); } + // Getter and Setter for selectedEmployee get selectedEmployee(): ISelectedEmployee { - const { selectedEmployee } = this.appQuery.getValue(); - return selectedEmployee; + /** + * Retrieves the currently selected employee from the application's state. + * + * @returns {ISelectedEmployee} - The selected employee object. + */ + return this.appQuery.getValue().selectedEmployee; } - set selectedOrganization(organization: IOrganization) { - this.appStore.update({ - selectedOrganization: organization - }); + set selectedEmployee(employee: ISelectedEmployee) { + /** + * Updates the selected employee in the application's state. + * + * @param {ISelectedEmployee} employee - The employee object to be set as the selected employee. + */ + this.appStore.update({ selectedEmployee: employee }); + } + + // Getter and Setter for selectedProject + get selectedProject(): IOrganizationProject { + /** + * Retrieves the currently selected project from the application's state. + * + * @returns {IOrganizationProject} - The selected project object. + */ + return this.appQuery.getValue().selectedProject; } set selectedProject(project: IOrganizationProject) { - this.appStore.update({ - selectedProject: project - }); + /** + * Updates the selected project in the application's state. + * + * @param {IOrganizationProject} project - The project object to be set as the selected project. + */ + this.appStore.update({ selectedProject: project }); + } + + // Getter and Setter for selectedTeam + get selectedTeam(): IOrganizationTeam { + /** + * Retrieves the currently selected team from the application's state. + * + * @returns {IOrganizationTeam} - The selected team object. + */ + return this.appQuery.getValue().selectedTeam; } set selectedTeam(team: IOrganizationTeam) { - this.appStore.update({ - selectedTeam: team - }); + /** + * Updates the selected team in the application's state. + * + * @param {IOrganizationTeam} team - The team object to be set as the selected team. + */ + this.appStore.update({ selectedTeam: team }); } set systemLanguages(languages: ILanguage[]) { diff --git a/packages/ui-core/shared/src/lib/selectors/date-range-picker/arrow/context/arrow.class.ts b/packages/ui-core/shared/src/lib/selectors/date-range-picker/arrow/context/arrow.class.ts index 32c3f1aed06..c5205272cf1 100644 --- a/packages/ui-core/shared/src/lib/selectors/date-range-picker/arrow/context/arrow.class.ts +++ b/packages/ui-core/shared/src/lib/selectors/date-range-picker/arrow/context/arrow.class.ts @@ -1,22 +1,35 @@ import { IArrowStrategy } from '../strategies/arrow-strategy.interface'; + +/** + * The Arrow class is a context that uses a strategy to perform actions. + */ export class Arrow { - // Define strategy aggregation - private strategy: IArrowStrategy; /** - * default constructor + * The strategy instance used to execute actions. */ - constructor() { } + private strategy!: IArrowStrategy; + /** - * set strategy - * @param strategy + * Sets the strategy to be used by the Arrow instance. + * @param strategy - An implementation of IArrowStrategy. */ - set setStrategy(strategy: IArrowStrategy) { + setStrategy(strategy: IArrowStrategy): void { + if (!strategy) { + throw new Error('Strategy cannot be null or undefined.'); + } this.strategy = strategy; } + /** - * @param request + * Executes the action defined by the current strategy. + * @param request - The input request data. + * @param unitOfTime - The unit of time for moment.js operations. + * @returns The result of the strategy action. */ execute(request: any, unitOfTime: moment.unitOfTime.Base): any { + if (!this.strategy) { + throw new Error('Strategy has not been set.'); + } return this.strategy.action(request, unitOfTime); } } diff --git a/packages/ui-core/shared/src/lib/selectors/date-range-picker/date-picker.interface.ts b/packages/ui-core/shared/src/lib/selectors/date-range-picker/date-picker.interface.ts index cbab237da55..199e8904101 100644 --- a/packages/ui-core/shared/src/lib/selectors/date-range-picker/date-picker.interface.ts +++ b/packages/ui-core/shared/src/lib/selectors/date-range-picker/date-picker.interface.ts @@ -1,27 +1,31 @@ // Represents a date range with a start date and end date. export interface IDateRangePicker { - startDate: Date; // The start date of the date range. - endDate: Date; // The end date of the date range. - isCustomDate?: boolean; // Optional flag to indicate if it's a custom date range. + startDate: Date; // The start date of the date range. + endDate: Date; // The end date of the date range. + isCustomDate?: boolean; // Optional flag to indicate if it's a custom date range. } // Represents a time period with a start date and end date using moment.js. export interface TimePeriod { - startDate: moment.Moment; // The start date of the time period. - endDate: moment.Moment; // The end date of the time period. + startDate: moment.Moment; // The start date of the time period. + endDate: moment.Moment; // The end date of the time period. } // Represents a collection of date ranges where each range is indexed by a string key. export interface DateRanges { - [index: string]: [moment.Moment, moment.Moment]; // Key-value pairs of date ranges. + [index: string]: [moment.Moment, moment.Moment]; // Key-value pairs of date ranges. } // Enum defining keys for common date range options. export enum DateRangeKeyEnum { - TODAY = 'Today', - YESTERDAY = 'Yesterday', - CURRENT_WEEK = 'Current week', - LAST_WEEK = 'Last week', - CURRENT_MONTH = 'Current month', - LAST_MONTH = 'Last month' + TODAY = 'Today', + YESTERDAY = 'Yesterday', + CURRENT_WEEK = 'Current Week', + LAST_WEEK = 'Last Week', + CURRENT_MONTH = 'Current Month', + LAST_MONTH = 'Last Month' +} + +export interface DateRangeClicked { + label: DateRangeKeyEnum; } diff --git a/packages/ui-core/shared/src/lib/selectors/date-range-picker/date-range-picker.component.scss b/packages/ui-core/shared/src/lib/selectors/date-range-picker/date-range-picker.component.scss index c9590ebbe3a..32b974c2d1c 100644 --- a/packages/ui-core/shared/src/lib/selectors/date-range-picker/date-range-picker.component.scss +++ b/packages/ui-core/shared/src/lib/selectors/date-range-picker/date-range-picker.component.scss @@ -38,7 +38,7 @@ $button-radius: nb-theme(button-rectangle-border-radius); color: nb-theme(gauzy-text-color-2); } input.single-range { - width: 103px; + width: 103px; } input.double-range { width: 190px; @@ -129,6 +129,7 @@ $button-radius: nb-theme(button-rectangle-border-radius); button { border-radius: $button-radius; color: nb-theme(text-basic-color); + text-transform: capitalize; &.active { background-color: nb-theme(color-primary-active); } @@ -177,4 +178,4 @@ $button-radius: nb-theme(button-rectangle-border-radius); color: nb-theme(gauzy-text-color-1); @include nb-ltr(margin-left, 9px); @include nb-rtl(margin-right, 12px); -} \ No newline at end of file +} diff --git a/packages/ui-core/shared/src/lib/selectors/date-range-picker/date-range-picker.component.ts b/packages/ui-core/shared/src/lib/selectors/date-range-picker/date-range-picker.component.ts index b16930a6b27..bbbca23be54 100644 --- a/packages/ui-core/shared/src/lib/selectors/date-range-picker/date-range-picker.component.ts +++ b/packages/ui-core/shared/src/lib/selectors/date-range-picker/date-range-picker.component.ts @@ -25,7 +25,7 @@ import { distinctUntilChange, isNotEmpty } from '@gauzy/ui-core/common'; import { Arrow } from './arrow/context/arrow.class'; import { Next, Previous } from './arrow/strategies'; import { dayOfWeekAsString, shiftUTCtoLocal } from './date-picker.utils'; -import { DateRangeKeyEnum, DateRanges, TimePeriod } from './date-picker.interface'; +import { DateRangeClicked, DateRangeKeyEnum, DateRanges, TimePeriod } from './date-picker.interface'; import { TimeZoneService } from '../../timesheet/gauzy-filters/timezone-filter'; @UntilDestroy({ checkProperties: true }) @@ -43,90 +43,138 @@ export class DateRangePickerComponent extends TranslationBaseComponent implement public ranges: DateRanges; // Define ngx-daterangepicker-material range configuration private readonly dates$: BehaviorSubject = this._dateRangePickerBuilderService.dates$; // Default selected date picker ranges private readonly range$: Subject = new Subject(); // Local store date picker ranges + // Declaration of arrow variables private arrow: Arrow = new Arrow(); private next: Next = new Next(); private previous: Previous = new Previous(); - // Private field to store the locale configuration - public _locale: LocaleConfig = { - displayFormat: 'DD.MM.YYYY', // default display format could be 'YYYY-MM-DDTHH:mm:ss.SSSSZ' - format: 'DD.MM.YYYY', // default format - direction: 'ltr' // default direction + /** + * Locale configuration for the component. + * Defaults are: + * - displayFormat: 'DD.MM.YYYY' + * - format: 'DD.MM.YYYY' + * - direction: 'ltr' + */ + public locale: LocaleConfig = { + displayFormat: 'DD.MM.YYYY', // Default display format + format: 'DD.MM.YYYY', // Default format + direction: 'ltr' // Default direction }; - // Getter for the locale configuration - get locale(): LocaleConfig { - return this._locale; - } - // Setter for the locale configuration - set locale(locale: LocaleConfig) { - this._locale = locale; - } - // Show or Hide arrows button, show by default + /** ViewChild for the DateRangePickerDirective */ + @ViewChild(DateRangePickerDirective, { static: true }) public dateRangePickerDirective: DateRangePickerDirective; + + /** + * Determines whether to show or hide the arrows button. + * Defaults to showing the arrows. + */ @Input() arrows: boolean = true; - // Private field to store the first day of the week + /** + * Indicates whether the date picker is locked. + */ + @Input() isLockDatePicker: boolean = false; + + /** + * Indicates whether the date picker is in single date selection mode. + */ + @Input() isSingleDatePicker: boolean = false; + + /** + * Indicates whether future dates are disabled in the date picker. + */ + @Input() isDisableFutureDatePicker: boolean = false; + + /** + * Indicates whether past dates are disabled in the date picker. + */ + @Input() isDisablePastDatePicker: boolean = false; + + /** + * The first day of the week. + */ private _firstDayOfWeek: number = moment.localeData().firstDayOfWeek(); - // Getter for the first day of the week + @Input() set firstDayOfWeek(value: WeekDaysEnum) { + // Accept zero (Sunday) as a valid value + if (value !== null && value !== undefined) { + this._firstDayOfWeek = dayOfWeekAsString(value); + } else { + // Default to locale's first day of the week if value is null or undefined + this._firstDayOfWeek = moment.localeData().firstDayOfWeek(); + } + this.locale.firstDay = this._firstDayOfWeek; + } get firstDayOfWeek(): number { return this._firstDayOfWeek; } - // Setter for the first day of the week with input binding - @Input() set firstDayOfWeek(value: WeekDaysEnum) { - if (value) this._firstDayOfWeek = dayOfWeekAsString(value); - this.locale.firstDay = this.firstDayOfWeek; - } - /* - * Getter & Setter + /** + * The time zone to be used. + * Defaults to the user's local time zone. */ private _timeZone: string = moment.tz.guess(); - get timeZone(): string { - return this._timeZone; - } - @Input() set timeZone(value: string) { - if (value) this._timeZone = value; + @Input() set timeZone(value: string | null | undefined) { + // Update the time zone if a valid value is provided + if (value) { + this._timeZone = value; + } - if (this.isSaveDatePicker) { - this.onSavingFilter(this.getSelectorDates()); + // Get the date picker configuration + const datePickerConfig = this._dateRangePickerBuilderService.datePickerConfig; + // Update the date picker based on the current settings + const selectorDates = this.getSelectorDates(); + // If the date picker is set to save date range, save the selected date range + if (datePickerConfig.isSaveDatePicker) { + this.onSavingFilter(selectorDates); } else { - this.selectedDateRange = this.getSelectorDates(); - this.rangePicker = this.selectedDateRange; // Ensure consistency between selectedDateRange and rangePicker + this.selectedDateRange = selectorDates; + this.rangePicker = { ...selectorDates }; // Ensure consistency between selectedDateRange and rangePicker } } + get timeZone(): string { + return this._timeZone; + } - /* - * Getter & Setter for dynamic unitOfTime + /** + * Dynamic unit of time for date operations. + * Defaults to the configuration's unit of time if not provided. */ private _unitOfTime: moment.unitOfTime.Base = DEFAULT_DATE_PICKER_CONFIG.unitOfTime; - get unitOfTime(): moment.unitOfTime.Base { - return this._unitOfTime; - } - @Input() set unitOfTime(value: moment.unitOfTime.Base) { - if (value) this._unitOfTime = value; + @Input() set unitOfTime(value: moment.unitOfTime.Base | null | undefined) { + // Update _unitOfTime if a valid value is provided; otherwise, use default + if (value !== null && value !== undefined) { + this._unitOfTime = value; + } else { + this._unitOfTime = DEFAULT_DATE_PICKER_CONFIG.unitOfTime; + } - if (this.isSaveDatePicker) { - this.onSavingFilter(this.getSelectorDates()); + // Get the date picker configuration + const datePickerConfig = this._dateRangePickerBuilderService.datePickerConfig; + // Update the date picker based on the current settings + const selectorDates = this.getSelectorDates(); + // If the date picker is set to save date range, save the selected date range + if (datePickerConfig.isSaveDatePicker) { + this.onSavingFilter(selectorDates); } else { - this.selectedDateRange = this.getSelectorDates(); - this.rangePicker = this.selectedDateRange; // Ensure consistency between selectedDateRange and rangePicker + this.selectedDateRange = selectorDates; + this.rangePicker = { ...selectorDates }; // Ensure consistency between selectedDateRange and rangePicker } } + get unitOfTime(): moment.unitOfTime.Base { + return this._unitOfTime; + } - /* - * Getter & Setter for dynamic selected date range + /** + * Getter and Setter for dynamic selected date range. */ private _selectedDateRange: IDateRangePicker; - get selectedDateRange(): IDateRangePicker { - return this._selectedDateRange; - } @Input() set selectedDateRange(range: IDateRangePicker) { if (isNotEmpty(range)) { - /** - * IF current route has timesheet filter save state - */ - if (this.isSaveDatePicker) { + // Get the date picker configuration + const datePickerConfig = this._dateRangePickerBuilderService.datePickerConfig; + // If the current route has timesheet filter save state + if (datePickerConfig.isSaveDatePicker) { this._timesheetFilterService.filter = { ...this._timesheetFilterService.filter, ...range @@ -142,9 +190,12 @@ export class DateRangePickerComponent extends TranslationBaseComponent implement } } } + get selectedDateRange(): IDateRangePicker { + return this._selectedDateRange; + } - /* - * Getter & Setter for dynamic selected internal date range + /** + * Getter and Setter for the dynamic selected internal date range. */ private _rangePicker: IDateRangePicker; public get rangePicker(): IDateRangePicker { @@ -156,64 +207,6 @@ export class DateRangePickerComponent extends TranslationBaseComponent implement } } - /* - * Getter & Setter for lock date picker - */ - private _isLockDatePicker: boolean = false; - get isLockDatePicker(): boolean { - return this._isLockDatePicker; - } - @Input() set isLockDatePicker(isLock: boolean) { - this._isLockDatePicker = isLock; - } - - /* - * Getter & Setter for save date picker - */ - private _isSaveDatePicker: boolean = false; - get isSaveDatePicker(): boolean { - return this._isSaveDatePicker; - } - @Input() set isSaveDatePicker(isSave: boolean) { - this._isSaveDatePicker = isSave; - } - - /* - * Getter & Setter for single date picker - */ - private _isSingleDatePicker: boolean = false; - get isSingleDatePicker(): boolean { - return this._isSingleDatePicker; - } - @Input() set isSingleDatePicker(isSingle: boolean) { - this._isSingleDatePicker = isSingle; - } - - /* - * Getter & Setter for disabled future dates - */ - private _isDisableFutureDatePicker: boolean = false; - get isDisableFutureDatePicker(): boolean { - return this._isDisableFutureDatePicker; - } - @Input() set isDisableFutureDatePicker(isDisable: boolean) { - this._isDisableFutureDatePicker = isDisable; - } - - /** - * Getter & Setter for disabled past dates - */ - private _isDisablePastDatePicker: boolean = false; - get isDisablePastDatePicker(): boolean { - return this._isDisablePastDatePicker; - } - @Input() set isDisablePastDatePicker(isDisable: boolean) { - this._isDisablePastDatePicker = isDisable; - } - - /** */ - @ViewChild(DateRangePickerDirective, { static: true }) public dateRangePickerDirective: DateRangePickerDirective; - constructor( public readonly translateService: TranslateService, private readonly _route: ActivatedRoute, @@ -265,21 +258,20 @@ export class DateRangePickerComponent extends TranslationBaseComponent implement ]) ), tap(([organization, datePickerConfig, queryParams, timeZone]) => { - this.organization = organization; - this.futureDateAllowed = organization.futureDateAllowed; - this.timeZone = timeZone; + this.organization = organization; // Update the organization + this.futureDateAllowed = organization.futureDateAllowed; // Update the future date allowed + this.timeZone = timeZone; // Update the time zone - const { isLockDatePicker, isSaveDatePicker } = datePickerConfig; + const { isLockDatePicker } = datePickerConfig; const { isSingleDatePicker, isDisableFutureDate, isDisablePastDate } = datePickerConfig; this.isDisableFutureDatePicker = isDisableFutureDate; this.isDisablePastDatePicker = isDisablePastDate; this.isLockDatePicker = isLockDatePicker; - this.isSaveDatePicker = isSaveDatePicker; this.isSingleDatePicker = isSingleDatePicker; - const { unit_of_time = datePickerConfig.unitOfTime } = queryParams; - this.unitOfTime = unit_of_time; + const { unit_of_time: unitOfTime = datePickerConfig.unitOfTime } = queryParams; + this.unitOfTime = unitOfTime; }), tap(() => { this.createDateRangeMenus(); @@ -304,169 +296,190 @@ export class DateRangePickerComponent extends TranslationBaseComponent implement } /** - * Create Date Range Translated Menus + * Creates the date range translated menus based on the current configuration. */ - createDateRangeMenus() { - this.ranges = { - [DateRangeKeyEnum.TODAY]: [moment(), moment()], - [DateRangeKeyEnum.YESTERDAY]: [moment().subtract(1, 'days'), moment().subtract(1, 'days')], - [DateRangeKeyEnum.CURRENT_WEEK]: [moment().startOf('week'), moment().endOf('week')], - [DateRangeKeyEnum.LAST_WEEK]: [ - moment().subtract(1, 'week').startOf('week'), - moment().subtract(1, 'week').endOf('week') - ], - [DateRangeKeyEnum.CURRENT_MONTH]: [moment().startOf('month'), moment().endOf('month')], - [DateRangeKeyEnum.LAST_MONTH]: [ - moment().subtract(1, 'month').startOf('month'), - moment().subtract(1, 'month').endOf('month') - ] - }; - - // Define the units of time to remove based on conditions - const unitsToRemove = []; + createDateRangeMenus(): void { + this.ranges = {}; - if (this.isLockDatePicker && this.unitOfTime !== 'day') { - unitsToRemove.push(DateRangeKeyEnum.TODAY, DateRangeKeyEnum.YESTERDAY); - } - if (this.isLockDatePicker && this.unitOfTime !== 'week') { - unitsToRemove.push(DateRangeKeyEnum.CURRENT_WEEK, DateRangeKeyEnum.LAST_WEEK); - } - if (this.isLockDatePicker && this.unitOfTime !== 'month') { - unitsToRemove.push(DateRangeKeyEnum.CURRENT_MONTH, DateRangeKeyEnum.LAST_MONTH); - } + // Helper function to add ranges to the ranges object + const addRange = (key: DateRangeKeyEnum, startDate: moment.Moment, endDate: moment.Moment) => { + this.ranges[key] = [startDate, endDate]; + }; - // Remove date ranges based on unitsToRemove - unitsToRemove.forEach((unit) => { - delete this.ranges[unit]; + // Determine which units of time are allowed + const allowedUnits = this.isLockDatePicker ? [this.unitOfTime] : ['day', 'week', 'month']; + + // Add date ranges based on the allowed units of time + allowedUnits.forEach((unit) => { + switch (unit) { + case 'day': + addRange(DateRangeKeyEnum.TODAY, moment(), moment()); + addRange(DateRangeKeyEnum.YESTERDAY, moment().subtract(1, 'days'), moment().subtract(1, 'days')); + break; + case 'week': + addRange(DateRangeKeyEnum.CURRENT_WEEK, moment().startOf('week'), moment().endOf('week')); + addRange( + DateRangeKeyEnum.LAST_WEEK, + moment().subtract(1, 'week').startOf('week'), + moment().subtract(1, 'week').endOf('week') + ); + break; + case 'month': + addRange(DateRangeKeyEnum.CURRENT_MONTH, moment().startOf('month'), moment().endOf('month')); + addRange( + DateRangeKeyEnum.LAST_MONTH, + moment().subtract(1, 'month').startOf('month'), + moment().subtract(1, 'month').endOf('month') + ); + break; + } }); } /** - * Allowed/Disallowed future max date strategy. + * Updates the maximum selectable date based on the future date strategy. + * If future dates are allowed, `maxDate` is set to `null` (no maximum limit). + * If future dates are disallowed, `maxDate` is set to today. + * Additionally, if the selected end date is in the future, it is adjusted to today. */ - private setFutureStrategy() { + private setFutureStrategy(): void { if (this.hasFutureStrategy()) { + // Future dates are allowed; no maximum date limit this.maxDate = null; } else { - this.maxDate = moment().format(); + // Future dates are disallowed; set maxDate to today + const today = moment(); + this.maxDate = today.format(); + + // If the selected end date is today or in the future, adjust it to today if (this.isSameOrAfterDay(this.selectedDateRange.endDate)) { this.selectedDateRange = { ...this.selectedDateRange, - endDate: moment().toDate() + endDate: today.toDate() }; } } } /** - * Sets the strategy for allowing/disallowing past dates. + * Updates the minimum selectable date based on the past date strategy. + * If past dates are disallowed, `minDate` is set to today. + * If past dates are allowed, `minDate` is set to `null` (no minimum limit). */ - private setPastStrategy() { + private setPastStrategy(): void { if (this.hasPastStrategy()) { - // If there is a past strategy, set the minimum date to the current date + // Past dates are disallowed; set minDate to today this.minDate = moment().format(); } else { - // If there is no past strategy, set the minimum date to null, allowing past dates + // Past dates are allowed; no minimum date limit this.minDate = null; } } /** - * Retrieves the next selected range if not disabled. + * Advances the selected date range to the next period if not disabled. + * Updates the selected date range and synchronizes the range picker. + * Also updates the query parameters without navigating away. */ - async nextRange() { + async nextRange(): Promise { if (this.isNextDisabled()) { return; } - this.arrow.setStrategy = this.next; + // Set the strategy to 'next' on the arrow object + this.arrow.setStrategy(this.next); + + // Execute the strategy to get the next range const nextRange = this.arrow.execute(this.rangePicker, this.unitOfTime); - this.selectedDateRange = { ...this.selectedDateRange, ...nextRange }; - this.rangePicker = this.selectedDateRange; // Ensure consistency between selectedDateRange and rangePicker + + // Update the selected date range and ensure consistency with the range picker + this.selectedDateRange = { ...nextRange }; + this.rangePicker = { ...nextRange }; + + // Update future strategy settings if necessary this.setFutureStrategy(); } /** - * Retrieves the previous selected range. + * Moves the selected date range to the previous period. + * Updates the selected date range and synchronizes the range picker. + * Also updates the query parameters without navigating away. */ - async previousRange() { - this.arrow.setStrategy = this.previous; + previousRange(): void { + // Set the strategy to 'previous' on the arrow object + this.arrow.setStrategy(this.previous); + + // Execute the strategy to get the previous range const previousRange = this.arrow.execute(this.rangePicker, this.unitOfTime); - this.selectedDateRange = { ...this.selectedDateRange, ...previousRange }; - this.rangePicker = this.selectedDateRange; // Ensure consistency between selectedDateRange and rangePicker - } - /** - * Navigates to the current route with specified query parameters, while preserving existing ones. - * - * @param queryParams The query parameters to be attached. - */ - async navigateWithQueryParams(): Promise { - const { startDate, endDate, isCustomDate } = this.selectedDateRange; - - // Create new moment objects for formatted dates to avoid mutation - const formattedStartDate = moment(startDate).clone().format('YYYY-MM-DD'); - const formattedEndDate = moment(endDate).clone().format('YYYY-MM-DD'); - - // Updates the query parameters of the current route without navigating away. - await this._navigationService.updateQueryParams({ - date: formattedStartDate, - date_end: formattedEndDate, - unit_of_time: this.unitOfTime, - is_custom_date: isCustomDate - }); + // Update the selected date range and ensure consistency with the range picker + this.selectedDateRange = { ...previousRange }; + this.rangePicker = { ...previousRange }; } /** - * Checks if the Next Button should be disabled. - * @returns True if the Next Button should be disabled, false otherwise. + * Determines whether the "Next" button should be disabled. + * The "Next" button is disabled if: + * - There is no selected date range. + * - The selected date range lacks a start or end date. + * - There is no future strategy available and the end date is today or in the past. + * + * @returns {boolean} True if the Next Button should be disabled, false otherwise. */ isNextDisabled(): boolean { - if (!this.selectedDateRange) { - return true; - } - - const { startDate, endDate } = this.selectedDateRange; - if (!startDate || !endDate) { + // Check if selectedDateRange, startDate, and endDate are all defined + if (!this.selectedDateRange?.startDate || !this.selectedDateRange?.endDate) { return true; } - return !this.hasFutureStrategy() && this.isSameOrAfterDay(endDate); + // Determine if there is no future strategy and the end date is today or after + return !this.hasFutureStrategy() && this.isSameOrAfterDay(this.selectedDateRange.endDate); } /** - * Listens to the event on ngx-daterangepicker-material. - * @param event The updated time period. + * Listens to the date update event from ngx-daterangepicker-material. + * Updates the selected date range and synchronizes the range picker. + * Also updates the query parameters without navigating away. + * + * @param event - The updated time period. */ - onDatesUpdated(event: TimePeriod) { + onDatesUpdated(event: TimePeriod): void { if (!this.dateRangePickerDirective) { return; } const { startDate, endDate } = shiftUTCtoLocal(event); - if (startDate && endDate) { - const range = {} as IDateRangePicker; - const start = this.isLockDatePicker ? moment(startDate).startOf(this.unitOfTime) : startDate; - const end = this.isLockDatePicker ? moment(startDate).endOf(this.unitOfTime) : endDate; - - range.startDate = start.toDate(); - range.endDate = end.toDate(); - range.isCustomDate = this.isCustomDate({ - startDate: start, - endDate: end - }); - this.selectedDateRange = range; - this.rangePicker = this.selectedDateRange; // Ensure consistency between selectedDateRange and rangePicker + // Return early if either date is missing + if (!startDate || !endDate) { + return; } + + // Ensure start and end are moment objects + const startMoment = this.isLockDatePicker ? moment(startDate).startOf(this.unitOfTime) : moment(startDate); + const endMoment = this.isLockDatePicker ? moment(startDate).endOf(this.unitOfTime) : moment(endDate); + + // Construct the range object directly + const range: IDateRangePicker = { + startDate: startMoment.toDate(), + endDate: endMoment.toDate(), + isCustomDate: this.isCustomDate({ startDate: startMoment, endDate: endMoment }) + }; + + // Update the selected date range and ensure consistency with the range picker + this.selectedDateRange = { ...range }; + this.rangePicker = { ...range }; } /** - * Listens to the range click event on ngx-daterangepicker-material. - * @param range The clicked range object. + * Handles the range click event from ngx-daterangepicker-material. + * Updates the `unitOfTime` based on the selected range label. + * + * @param {DateRangeClicked} range - The clicked range object. */ - rangeClicked(range: any) { - const unitOfTimeMap: { [key in DateRangeKeyEnum]: string } = { + rangeClicked(range: DateRangeClicked) { + // Define the mapping outside the method to avoid recreating it on every call + const unitOfTimeMap: Record = { [DateRangeKeyEnum.TODAY]: 'day', [DateRangeKeyEnum.YESTERDAY]: 'day', [DateRangeKeyEnum.CURRENT_WEEK]: 'week', @@ -478,116 +491,171 @@ export class DateRangePickerComponent extends TranslationBaseComponent implement const unitOfTime = unitOfTimeMap[range.label]; if (unitOfTime) { this.unitOfTime = unitOfTime; + } else { + console.warn(`Unrecognized range label: ${range.label}`); } } /** - * Checks if the provided date range is a custom date range. + * Determines if the provided date range is a custom date range, + * meaning it does not match any predefined ranges. * - * @param dateRange The date range to check. + * @param dateRange - The date range to check. * @returns True if the date range is custom, false otherwise. */ - isCustomDate(dateRange: any): boolean { - if (!this.dateRangePickerDirective) { - return true; // If dateRangePickerDirective is not available, consider it as a custom range + isCustomDate(dateRange: { startDate: moment.Moment; endDate: moment.Moment }): boolean { + // If the date range picker directive or its ranges are not available, consider it a custom range + if (!this.dateRangePickerDirective?.ranges) { + return true; } - const ranges = this.dateRangePickerDirective.ranges; - for (const range in ranges) { - if (this.ranges[range]) { - const [rangeStartDate, rangeEndDate] = this.ranges[range]; + const predefinedRanges = this.dateRangePickerDirective.ranges; + const formattedStartDate = moment(dateRange.startDate).format('YYYY-MM-DD'); + const formattedEndDate = moment(dateRange.endDate).format('YYYY-MM-DD'); - // Create new moment objects for formatted dates to avoid mutation - const formattedStartDate = moment(dateRange.startDate).clone().format('YYYY-MM-DD'); - const formattedEndDate = moment(dateRange.endDate).clone().format('YYYY-MM-DD'); + // Iterate over the predefined ranges to check for a match + for (const rangeLabel in predefinedRanges) { + if (predefinedRanges.hasOwnProperty(rangeLabel)) { + const [rangeStartDate, rangeEndDate] = predefinedRanges[rangeLabel]; - const isStartDateEqual = formattedStartDate === rangeStartDate.format('YYYY-MM-DD'); - const isEndDateEqual = formattedEndDate === rangeEndDate.format('YYYY-MM-DD'); + // Format the predefined range dates for comparison + const predefinedStartDate = rangeStartDate.format('YYYY-MM-DD'); + const predefinedEndDate = rangeEndDate.format('YYYY-MM-DD'); - // ignore times when comparing dates if time picker is not enabled - if (isStartDateEqual && isEndDateEqual) { - return false; // If the range matches any predefined range, it's not custom + // Compare the formatted dates + if (formattedStartDate === predefinedStartDate && formattedEndDate === predefinedEndDate) { + // The date range matches a predefined range; it's not custom + return false; } } } - return true; // If no predefined range matches, it's a custom range + + // No matching predefined range found; it's a custom date range + return true; } /** - * When date range picker wants to save dates in local storage + * Saves the selected date range to the timesheet filter service and updates the query parameters. * - * @param range + * @param range - The selected date range. */ - onSavingFilter(range: IDateRangePicker) { + onSavingFilter(range: IDateRangePicker): void { + const datePickerConfig = this._dateRangePickerBuilderService.datePickerConfig; + + // Proceed only if saving the date picker is enabled + if (!datePickerConfig.isSaveDatePicker) { + return; + } + + // Subscribe to the filter$ Observable and take the first emitted value this._timesheetFilterService.filter$ - .pipe( - take(1), - filter(() => !!this.isSaveDatePicker), - tap((filters: ITimeLogFilters) => { - const { startDate = range.startDate } = filters; - const hasFutureStrategy = this.hasFutureStrategy(); - const date = !hasFutureStrategy && this.isSameOrAfterDay(startDate) ? moment() : moment(startDate); - const start = moment(date).startOf(this.unitOfTime); - const end = moment(date).endOf(this.unitOfTime); - - this.selectedDateRange = { - startDate: start.toDate(), - endDate: end.toDate(), - isCustomDate: this.isCustomDate({ startDate: start, endDate: end }) - }; - this.rangePicker = this.selectedDateRange; // Ensure consistency between selectedDateRange and rangePicker - }), - untilDestroyed(this) - ) - .subscribe(); + .pipe(take(1), untilDestroyed(this)) + .subscribe((filters: ITimeLogFilters) => { + // Use the startDate from filters if available; otherwise, use the startDate from the range parameter + const startDate = filters.startDate || range.startDate; + + // Determine if future dates are allowed + const hasFutureStrategy = this.hasFutureStrategy(); + + // Calculate the date based on the future strategy and the start date + const dateMoment = + !hasFutureStrategy && this.isSameOrAfterDay(startDate) ? moment() : moment(startDate); + + // Get the start and end of the unit of time (e.g., day, week, month) + const start = dateMoment.clone().startOf(this.unitOfTime); + const end = dateMoment.clone().endOf(this.unitOfTime); + + // Update the selected date range and ensure consistency with the range picker + this.selectedDateRange = { + startDate: start.toDate(), + endDate: end.toDate(), + isCustomDate: this.isCustomDate({ startDate: start, endDate: end }) + }; + this.rangePicker = { ...this.selectedDateRange }; // Ensure consistency between selectedDateRange and rangePicker + }); } /** - * Is same or after today + * Checks if the provided date is the same as or after today. * - * @param date - * @returns {Boolean} + * @param date - The date to compare. + * @returns True if the date is today or in the future, false otherwise. */ isSameOrAfterDay(date: string | Date): boolean { - return moment(moment(date)).isSameOrAfter(moment(), 'day'); + return moment(date).isSameOrAfter(moment(), 'day'); } /** - * Checks if there is a future strategy or not. - * @returns {Boolean} True if there is a future strategy, false otherwise. + * Determines whether future dates are allowed based on the current strategy. + * + * @returns True if future dates are allowed, false otherwise. */ private hasFutureStrategy(): boolean { return !this.isDisableFutureDatePicker && this.futureDateAllowed; } /** - * Determines whether there is a strategy to disable past dates. - * @returns {Boolean} True if there is a strategy to disable past dates, otherwise false. + * Determines whether past dates are disallowed based on the current strategy. + * + * @returns True if past dates are disallowed, false otherwise. */ private hasPastStrategy(): boolean { return this.isDisablePastDatePicker; } /** - * Open Date Picker On Calender Click + * Opens the date picker when the calendar icon is clicked. + * + * @param event - The mouse event triggered by clicking the calendar icon. */ openDatepicker(event: MouseEvent): void { - this.dateRangePickerDirective.toggle(event); + if (this.dateRangePickerDirective) { + this.dateRangePickerDirective.toggle(event); + } else { + console.warn('DateRangePickerDirective is not initialized.'); + } } /** - * Gets the selector default dates from the BehaviorSubject. + * Retrieves the default date range picker configuration from the dates BehaviorSubject. * * @returns The default date range picker configuration. */ private getSelectorDates(): IDateRangePicker { const { startDate, endDate, isCustomDate } = this.dates$.getValue(); + return { startDate: moment(startDate).toDate(), endDate: moment(endDate).toDate(), - isCustomDate + isCustomDate: isCustomDate ?? false }; } + /** + * Navigates to the current route with specified query parameters, while preserving existing ones. + * + * @param queryParams The query parameters to be attached. + */ + async navigateWithQueryParams(): Promise { + const selectors = this._selectorBuilderService.getSelectors(); + + // Check if the date selector exists + if (selectors.date) { + const { startDate, endDate, isCustomDate } = this.selectedDateRange; + + // Create new moment objects for formatted dates to avoid mutation + const formattedStartDate = moment(startDate).clone().format('YYYY-MM-DD'); + const formattedEndDate = moment(endDate).clone().format('YYYY-MM-DD'); + + // Updates the query parameters of the current route without navigating away. + await this._navigationService.updateQueryParams({ + date: formattedStartDate, + date_end: formattedEndDate, + unit_of_time: this.unitOfTime, + is_custom_date: isCustomDate + }); + } + } + ngOnDestroy(): void {} } diff --git a/packages/ui-core/shared/src/lib/selectors/employee/employee.component.html b/packages/ui-core/shared/src/lib/selectors/employee/employee.component.html index 96181d0e350..478bb8825ab 100644 --- a/packages/ui-core/shared/src/lib/selectors/employee/employee.component.html +++ b/packages/ui-core/shared/src/lib/selectors/employee/employee.component.html @@ -3,7 +3,7 @@ [addTag]="(hasEditEmployee$ | async) && addTag ? createNew : null" [clearable]="isClearable()" [disabled]="disabled" - [(items)]="people" + [(items)]="employees" (change)="selectEmployee($event); select.blur()" (clear)="select.blur()" [(ngModel)]="selectedEmployee" diff --git a/packages/ui-core/shared/src/lib/selectors/employee/employee.component.ts b/packages/ui-core/shared/src/lib/selectors/employee/employee.component.ts index 73067aad197..f71ed2d7eb0 100644 --- a/packages/ui-core/shared/src/lib/selectors/employee/employee.component.ts +++ b/packages/ui-core/shared/src/lib/selectors/employee/employee.component.ts @@ -17,6 +17,7 @@ import { filter, debounceTime, tap, switchMap } from 'rxjs/operators'; import { CrudActionEnum, DEFAULT_TYPE, + ID, IDateRangePicker, IEmployee, IOrganization, @@ -43,82 +44,37 @@ import { ALL_EMPLOYEES_SELECTED } from './default-employee'; changeDetection: ChangeDetectionStrategy.OnPush }) export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit { - /* - * Getter & Setter for dynamic clearable option - */ - _clearable: boolean = true; - get clearable(): boolean { - return this._clearable; - } - @Input() set clearable(value: boolean) { - this._clearable = value; - } - - /* - * Getter & Setter for dynamic add tag option - */ - _addTag: boolean = true; - get addTag(): boolean { - return this._addTag; - } - @Input() set addTag(value: boolean) { - this._addTag = value; - } - - private _skipGlobalChange: boolean = false; - get skipGlobalChange(): boolean { - return this._skipGlobalChange; - } - @Input() set skipGlobalChange(value: boolean) { - this._skipGlobalChange = value; - } - - /* - * Getter & Setter for dynamic disabled element - */ - private _disabled: boolean = false; - get disabled(): boolean { - return this._disabled; - } - @Input() set disabled(value: boolean) { - this._disabled = value; - } - - /* - * Getter & Setter for dynamic placeholder - */ - private _placeholder: string; - get placeholder(): string { - return this._placeholder; - } - @Input() set placeholder(value: string) { - this._placeholder = value; - } + public hasEditEmployee$: Observable; + public organization: IOrganization; + public employees: ISelectedEmployee[] = []; + public subject$: Subject = new Subject(); /** + * Input properties for component customization. * + * @property clearable - Whether the component allows clearing the selection (default: true). + * @property addTag - Whether adding new tags is allowed (default: true). + * @property skipGlobalChange - Whether to skip global change handling (default: false). + * @property disabled - Whether the component is disabled (default: false). + * @property placeholder - The placeholder text for the component. + * @property defaultSelected - Whether the default option is selected (default: true). + * @property showAllEmployeesOption - Whether to show the "All Employees" option (default: true). */ - private _defaultSelected: boolean = true; - get defaultSelected(): boolean { - return this._defaultSelected; - } - @Input() set defaultSelected(value: boolean) { - this._defaultSelected = value; - } + @Input() clearable: boolean = true; + @Input() addTag: boolean = true; + @Input() skipGlobalChange: boolean = false; + @Input() disabled: boolean = false; + @Input() placeholder: string; + @Input() defaultSelected: boolean = true; + @Input() showAllEmployeesOption: boolean = true; /** + * Manages the selected date range. * - */ - private _showAllEmployeesOption: boolean = true; - get showAllEmployeesOption(): boolean { - return this._showAllEmployeesOption; - } - @Input() set showAllEmployeesOption(value: boolean) { - this._showAllEmployeesOption = value; - } - - /** + * The `selectedDateRange` setter updates the date range and triggers an update via `subject$.next` + * with the selected organization and date range. * + * @property selectedDateRange - The currently selected date range. */ private _selectedDateRange?: IDateRangePicker; get selectedDateRange(): IDateRangePicker { @@ -130,7 +86,11 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, } /** + * Manages the selected employee. + * + * The `selectedEmployee` setter updates the selected employee and logs the change for debugging. * + * @property selectedEmployee - The currently selected employee. */ private _selectedEmployee: ISelectedEmployee; get selectedEmployee(): ISelectedEmployee { @@ -138,15 +98,15 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, } @Input() set selectedEmployee(employee: ISelectedEmployee) { this._selectedEmployee = employee; + + // If skipGlobalChange is false, update the query parameters + if (!this.skipGlobalChange) { + this.setAttributesToParams({ employeeId: employee?.id }); + } } @Output() selectionChanged: EventEmitter = new EventEmitter(); - public hasEditEmployee$: Observable; - public organization: IOrganization; - people: ISelectedEmployee[] = []; - subject$: Subject = new Subject(); - constructor( private readonly _router: Router, private readonly _navigationService: NavigationService, @@ -238,37 +198,32 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, } /** - * After create new employee pushed on header selector - * @param employees + * Adds newly created employees to the header selector. + * @param employees - The array of employees to add. */ - createEmployee(employees: IEmployee[]) { - const people: ISelectedEmployee[] = this.people || []; - if (Array.isArray(people)) { - employees.forEach((employee: IEmployee) => { - people.push({ - id: employee.id, - firstName: employee.user.firstName, - lastName: employee.user.lastName, - fullName: employee.user.name, - imageUrl: employee.user.imageUrl, - timeFormat: employee.user.timeFormat, - timeZone: employee.user.timeZone - }); - }); - this.people = [...people].filter(isNotEmpty); - } + createEmployee(employees: IEmployee[]): void { + this.employees = [ + ...(this.employees || []), + ...employees.map((employee: IEmployee) => ({ + id: employee.id, + firstName: employee.user.firstName, + lastName: employee.user.lastName, + fullName: employee.user.name, + imageUrl: employee.user.imageUrl, + timeFormat: employee.user.timeFormat, + timeZone: employee.user.timeZone + })) + ].filter(isNotEmpty); } /** - * After delete remove employee from header selector - * @param employee + * Removes a deleted employee from the header selector. + * @param employee - The employee to remove. */ - deleteEmployee(employee: IEmployee) { - let people: ISelectedEmployee[] = this.people || []; - if (Array.isArray(people) && people.length) { - people = people.filter((item: ISelectedEmployee) => item.id !== employee.id); - } - this.people = [...people].filter(isNotEmpty); + deleteEmployee(employee: IEmployee): void { + this.employees = (this.employees || []) + .filter((item: ISelectedEmployee) => item.id !== employee.id) + .filter(isNotEmpty); } /** @@ -282,7 +237,10 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, */ searchEmployee(term: string, item: any): boolean { // Split the search term by commas to handle multiple names - const searchTerms = term.toLowerCase().split(',').map((s) => s.trim()); + const searchTerms = term + .toLowerCase() + .split(',') + .map((s) => s.trim()); // Combine the employee's firstName and lastName for easier comparison const fullName = `${item.firstName || ''} ${item.lastName || ''}`.toLowerCase(); @@ -325,17 +283,18 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, } /** - * Selects an employee by their ID and performs necessary actions based on the selection - * @param employeeId The ID of the employee to select + * Selects an employee by their ID and performs necessary actions based on the selection. + * + * @param employeeId - The ID of the employee to select. */ - async selectEmployeeById(employeeId: string) { + async selectEmployeeById(employeeId: ID): Promise { try { - const employee = this.people.find((employee: ISelectedEmployee) => employeeId === employee.id); + const employee = this.employees.find((emp: ISelectedEmployee) => emp.id === employeeId); if (employee) { await this.selectEmployee(employee); } } catch (error) { - console.error('Error while selecting employee by ID:', error); + console.error('Error selecting employee by ID:', error); } } @@ -368,9 +327,9 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, */ private onSelectEmployee() { try { - if (!this.selectedEmployee && isNotEmpty(this.people)) { + if (!this.selectedEmployee && isNotEmpty(this.employees)) { // Ensure selected employee doesn't get reset when already set elsewhere - this.selectEmployee(this.people[0]); + this.selectEmployee(this.employees[0]); } if (!this.defaultSelected && this.selectedEmployee === ALL_EMPLOYEES_SELECTED) { @@ -390,7 +349,7 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, loadWorkingEmployeesIfRequired = async (organization: IOrganization, selectedDateRange: IDateRangePicker) => { //If no organization, then something is wrong if (!organization) { - this.people = []; + this.employees = []; return; } this._selectedDateRange = selectedDateRange; @@ -405,7 +364,7 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, */ private getEmployees = async (organization: IOrganization, selectedDateRange: IDateRangePicker) => { if (!organization) { - this.people = []; + this.employees = []; return; } const { tenantId } = this._store.user; @@ -413,7 +372,7 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, const { items } = await this._employeesService.getWorking(organizationId, tenantId, selectedDateRange, true); - this.people = [ + this.employees = [ ...items.map((employee: IEmployee) => { return { id: employee.id, @@ -433,12 +392,12 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, //Insert All Employees Option if (this.showAllEmployeesOption && this._store.hasPermission(PermissionsEnum.CHANGE_SELECTED_EMPLOYEE)) { - this.people.unshift(ALL_EMPLOYEES_SELECTED); + this.employees.unshift(ALL_EMPLOYEES_SELECTED); } //Set selected employee if no employee selected if (items.length > 0 && !this._store.selectedEmployee) { - this._store.selectedEmployee = this.people[0] || ALL_EMPLOYEES_SELECTED; + this._store.selectedEmployee = this.employees[0] || ALL_EMPLOYEES_SELECTED; } }; @@ -482,8 +441,8 @@ export class EmployeeSelectorComponent implements OnInit, OnDestroy, OnChanges, }; ngOnDestroy() { - if (this.people.length > 0 && !this._store.selectedEmployee && !this.skipGlobalChange) { - this._store.selectedEmployee = this.people[0] || ALL_EMPLOYEES_SELECTED; + if (this.employees.length > 0 && !this._store.selectedEmployee && !this.skipGlobalChange) { + this._store.selectedEmployee = this.employees[0] || ALL_EMPLOYEES_SELECTED; } } } diff --git a/packages/ui-core/shared/src/lib/selectors/organization/organization.component.ts b/packages/ui-core/shared/src/lib/selectors/organization/organization.component.ts index fb3e46adfa1..aec75ba3bbb 100644 --- a/packages/ui-core/shared/src/lib/selectors/organization/organization.component.ts +++ b/packages/ui-core/shared/src/lib/selectors/organization/organization.component.ts @@ -1,11 +1,11 @@ import { Component, OnInit, OnDestroy, AfterViewInit, Input } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { filter, map, tap } from 'rxjs/operators'; +import { catchError, distinctUntilChanged, filter, map, tap } from 'rxjs'; import { Observable } from 'rxjs/internal/Observable'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { uniq } from 'underscore'; -import { IOrganization, CrudActionEnum, PermissionsEnum } from '@gauzy/contracts'; -import { distinctUntilChange, isNotEmpty } from '@gauzy/ui-core/common'; +import { IOrganization, CrudActionEnum, PermissionsEnum, ID } from '@gauzy/contracts'; +import { isNotEmpty } from '@gauzy/ui-core/common'; import { NavigationService, OrganizationEditStore, @@ -13,6 +13,7 @@ import { ToastrService, UsersOrganizationsService } from '@gauzy/ui-core/core'; +import { of } from 'rxjs'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -21,41 +22,34 @@ import { styleUrls: ['./organization.component.scss'] }) export class OrganizationSelectorComponent implements AfterViewInit, OnInit, OnDestroy { - organizations: IOrganization[] = []; - selectedOrganization: IOrganization; - isOpen: boolean = false; + public organizations: IOrganization[] = []; + public selectedOrganization: IOrganization; + public isOpen: boolean = false; public hasEditOrganization$: Observable; - /* - * Getter & Setter for dynamic add tag option + /** + * Input properties for component customization. + * + * @property addTag - Whether adding new tags is allowed (default: true). */ - _addTag: boolean = true; - get addTag(): boolean { - return this._addTag; - } - @Input() set addTag(value: boolean) { - this._addTag = value; - } + @Input() addTag: boolean = true; constructor( - private readonly router: Router, - private readonly toastrService: ToastrService, - private readonly store: Store, - private readonly userOrganizationService: UsersOrganizationsService, + private readonly _router: Router, + private readonly _toastrService: ToastrService, + private readonly _store: Store, + private readonly _userOrganizationService: UsersOrganizationsService, private readonly _organizationEditStore: OrganizationEditStore, - private readonly activatedRoute: ActivatedRoute, + private readonly _activatedRoute: ActivatedRoute, private readonly _navigationService: NavigationService ) {} ngOnInit() { - this.hasEditOrganization$ = this.store.userRolePermissions$.pipe( - map(() => this.store.hasPermission(PermissionsEnum.ALL_ORG_EDIT)) - ); - + this.initializePermissions(); this.loadSelectedOrganization(); this.loadOrganizations().then(() => { - this.activatedRoute.queryParams + this._activatedRoute.queryParams .pipe( filter((query) => !!query.organizationId), tap(({ organizationId }) => this.selectOrganizationById(organizationId)), @@ -65,71 +59,148 @@ export class OrganizationSelectorComponent implements AfterViewInit, OnInit, OnD }); } - selectOrganization(organization: IOrganization) { - if (organization) { - this.store.selectedOrganization = organization; - this.store.organizationId = organization.id; - this.store.selectedEmployee = null; + /** + * Initializes the observable that determines if the user has edit permissions for organizations. + */ + private initializePermissions(): void { + this.hasEditOrganization$ = this._store.userRolePermissions$.pipe( + map(() => this._store.hasPermission(PermissionsEnum.ALL_ORG_EDIT)), + catchError((error) => { + console.error('Error checking permissions:', error); + return of(false); + }), + untilDestroyed(this) + ); + } - this.updateQueryParams({ organizationId: organization.id }); + /** + * Selects an organization and updates the store and query parameters accordingly. + * + * @param organization - The organization to select. + * @param isResetEmployeeStore - Whether to reset the selected employee. Defaults to true. + */ + public selectOrganization( + organization: IOrganization | null | undefined, + isResetEmployeeStore: boolean = true + ): void { + if (!organization) { + this._toastrService.warning('No organization provided to select.'); + console.warn('No organization provided to select.'); + return; + } + + // Update the store with the selected organization details + this._store.selectedOrganization = organization; + this._store.organizationId = organization.id; + + // Reset the selected employee store if required + if (isResetEmployeeStore) { + this._store.selectedEmployee = null; + console.info('Selected employee store has been reset.'); } + + console.log(`Selected Organization: ${organization.name}`); + + // Update the query parameters in the URL + this._navigationService.updateQueryParams({ organizationId: organization.id }); } /** * Updates query parameters while preserving specified parameters. + * * @param queryParams New query parameters to be added or updated. */ private async updateQueryParams(queryParams: { [key: string]: any }): Promise { await this._navigationService.updateQueryParams(queryParams); } + /** + * Loads and initializes the list of organizations for the current user. + * Retrieves organizations associated with the user, ensures uniqueness, + * and sets the selected organization in the store based on predefined logic. + */ private async loadOrganizations(): Promise { - const { tenantId } = this.store.user; - const { userId } = this.store; + try { + // Retrieve the user's ID and tenant ID + const { id: userId, tenantId } = this._store.user; - const { items = [] } = await this.userOrganizationService.getAll( - [ + // Define the relations to be included in the query + const relations = [ 'organization', 'organization.contact', 'organization.featureOrganizations', 'organization.featureOrganizations.feature' - ], - { userId, tenantId } - ); + ]; + + // Fetch all organizations associated with the user + const { items = [] } = await this._userOrganizationService.getAll(relations, { userId, tenantId }); - const organizations = items.map(({ organization }) => organization); - this.organizations = uniq(organizations, (item) => item.id); + // Extract organizations from the fetched items + const fetchedOrganizations: IOrganization[] = items.map(({ organization }) => organization); + // Remove duplicate organizations based on their ID + this.organizations = uniq(fetchedOrganizations, 'id'); + + // Select and set the active organization + this.selectAndSetOrganization(); + } catch (error) { + // Handle errors during organization loading + console.error('Failed to load organizations:', error); + } + } + + /** + * Selects and sets the active organization based on stored ID, default, or the first available. + */ + private selectAndSetOrganization(): void { + // Check if there are organizations available if (this.organizations.length > 0) { - const defaultOrganization = this.organizations.find( - (organization: IOrganization) => organization.isDefault - ); - const [firstOrganization] = this.organizations; + // Select the organization based on the following priority: + // 1. Organization with the stored ID + // 2. Default organization + // 3. First organization in the list + this._store.selectedOrganization = + this.organizations.find((org: IOrganization) => org.id === this._store.organizationId) || + this.organizations.find((org: IOrganization) => org.isDefault) || + this.organizations[0] || + null; - if (this.store.organizationId) { - const organization = this.organizations.find( - (organization: IOrganization) => organization.id === this.store.organizationId - ); - this.store.selectedOrganization = organization || defaultOrganization || firstOrganization; + // Log the selected organization if it exists + if (this._store.selectedOrganization) { + // Update the query parameters in the URL + this.updateQueryParams({ organizationId: this._store.selectedOrganization.id }); } else { - // set default organization as selected - this.store.selectedOrganization = defaultOrganization || firstOrganization; + // Handle the unlikely case where organizations exist but no selection was made + console.warn('No valid organization found to select.'); + this.resetStore(); } + } else { + // Handle the case where no organizations are available + this.resetStore(); + console.warn('No organizations found for the user. Store has been reset.'); } } - private loadSelectedOrganization() { - this.store.selectedOrganization$ + /** + * Loads the currently selected organization from the store and updates local state. + */ + private loadSelectedOrganization(): void { + this._store.selectedOrganization$ .pipe( - distinctUntilChange(), - filter((organization: IOrganization) => !!organization), + distinctUntilChanged((prev, curr) => prev?.id === curr?.id), + filter((organization: IOrganization | null) => !!organization), tap((organization: IOrganization) => { this.selectedOrganization = organization; - this.store.featureOrganizations = organization.featureOrganizations || []; + this._store.featureOrganizations = organization.featureOrganizations || []; }), untilDestroyed(this) ) - .subscribe(); + .subscribe({ + error: (error) => { + console.error('Error loading selected organization:', error); + this._toastrService.error('Failed to load selected organization.', error); + } + }); } ngAfterViewInit() { @@ -156,46 +227,95 @@ export class OrganizationSelectorComponent implements AfterViewInit, OnInit, OnD }); } - /* - * After created new organization pushed on dropdown + /** + * Adds a new organization to the dropdown list. + * + * @param organization - The organization to add. */ - createOrganization(organization: IOrganization) { - const organizations: IOrganization[] = this.organizations || []; - if (Array.isArray(organizations)) { - organizations.push(organization); - this.organizations = [...organizations].filter(isNotEmpty); - } + public createOrganization(organization: IOrganization): void { + // Initialize the organizations array if it's undefined or null + const updatedOrganizations: IOrganization[] = [...(this.organizations ?? []), organization]; + + // Filter out any empty or invalid entries, if necessary + this.organizations = updatedOrganizations.filter(isNotEmpty); } - /* - * After updated existing organization changed in the dropdown + /** + * Updates an existing organization in the dropdown list. + * + * @param organization - The organization with updated data. */ - updateOrganization(organization: IOrganization) { - let organizations: IOrganization[] = this.organizations || []; - if (Array.isArray(organizations) && organizations.length) { - organizations = organizations.map((item: IOrganization) => { - if (item.id === organization.id) { - return Object.assign({}, item, organization); - } - return item; - }); + public updateOrganization(organization: IOrganization): void { + // Check if the organization exists + const exists = this.organizations.some((org) => org.id === organization.id); + if (!exists) { + console.log('updated organization not found: ', organization?.name); + return; } - this.store.selectedOrganization = organization; - this.organizations = [...organizations].filter(isNotEmpty); + try { + // Update the organizations array immutably by mapping through existing organizations + const updatedOrganizations = this.organizations.map((org) => + org.id === organization.id ? { ...org, ...organization } : org + ); + + // Update the store with the selected organization details + this.selectOrganization(organization); + + // Assign the filtered and updated organizations list + this.organizations = updatedOrganizations.filter(isNotEmpty); + } catch (error) { + console.error('Error updating organization:', error); + this._toastrService.error('Failed to update organization.', error); + } } - /* - * After deleted organization removed on dropdown + /** + * Deletes an organization from the dropdown list. + * + * @param organization - The organization to delete. */ - deleteOrganization(organization: IOrganization) { - let organizations: IOrganization[] = this.organizations || []; - if (Array.isArray(organizations) && organizations.length) { - organizations = organizations.filter((item: IOrganization) => item.id !== organization.id); + public deleteOrganization(organization: IOrganization): void { + if (!organization) { + console.warn('No organization provided to delete.'); + return; } - this.organizations = [...organizations].filter(isNotEmpty); + // Check if the organization exists + const exists = this.organizations.some((org) => org.id === organization.id); + if (!exists) { + console.warn(`Delete failed: Organization with ID ${organization.id} not found.`); + return; + } + + try { + // Remove the organization immutably by filtering it out + const updatedOrganizations = this.organizations.filter((org) => org.id !== organization.id); + + // Assign the filtered and updated organizations list + this.organizations = updatedOrganizations.filter(isNotEmpty); + + // Check if the deleted organization was the selected one + if (this._store.selectedOrganization?.id === organization.id) { + if (updatedOrganizations.length > 0) { + // Select a random organization from the updated list + const organization = this.getRandomOrganization(updatedOrganizations); + this.selectOrganization(organization); + } else { + // No organizations left; reset the store + this.resetStore(); + console.warn('All organizations have been deleted. Store has been reset.'); + } + } + + console.log(`Organization with ID ${organization.id} deleted successfully.`); + } catch (error) { + // Handle any errors that occur during deletion + console.error(`Failed to delete organization with ID ${organization.id}`, error); + this._toastrService.error(`Failed to delete organization "${organization.name}".`, error); + } } + /** * event fired on model change. */ @@ -204,45 +324,81 @@ export class OrganizationSelectorComponent implements AfterViewInit, OnInit, OnD } /** - * Create new employee from ng-select tag + * Creates a new organization entry and navigates to the organization's page to open the add dialog. * - * @param name - * @returns + * @param name - The name of the new organization to be created. + * @returns A promise that resolves if the organization creation process is successful or returns early if permissions or required data are missing. */ - createNew = async (name: string) => { - if (!this.store.hasPermission(PermissionsEnum.ALL_ORG_EDIT)) { + createNew = async (name: string): Promise => { + // Check if the user has the required permissions + if (!this._store.hasPermission(PermissionsEnum.ALL_ORG_EDIT)) { return; } + + // Ensure that both the selected organization and name are provided if (!this.selectedOrganization || !name) { return; } + try { - this.router.navigate(['/pages/organizations/'], { - queryParams: { - openAddDialog: true - }, - state: { - name: name, - officialName: name - } + // Navigate to the organization's page and open the add dialog with the provided name + await this._router.navigate(['/pages/organizations/'], { + queryParams: { openAddDialog: true }, + state: { name, officialName: name } }); } catch (error) { - this.toastrService.error(error); + // Display an error message in case of any navigation failure + this._toastrService.error(error); } }; - onClickOutside(event) { + /** + * Closes the component when a click occurs outside of it. + * + * @param event - The click event. + */ + onClickOutside(event: Event): void { if (this.isOpen && !event) this.isOpen = false; } - selectOrganizationById(organizationId: string) { + /** + * Selects an organization by its ID. + * + * @param organizationId - The ID of the organization to select. + */ + selectOrganizationById(organizationId: ID): void { const organization = this.organizations.find( - (organization: IOrganization) => organizationId === organization.id + (organization: IOrganization) => organization.id === organizationId ); if (organization) { - this.selectOrganization(organization); + this.selectOrganization(organization, false); } } + /** + * Resets the store's selected organization, organization ID, and selected employee. + */ + private resetStore(): void { + this._store.selectedOrganization = null; + this._store.organizationId = null; + this._store.selectedEmployee = null; + console.info('Store reset: selectedOrganization, organizationId, and selectedEmployee have been cleared.'); + } + + /** + * Selects a random organization from the provided list. + * + * @param organizations - The list of organizations to select from. + * @returns A randomly selected organization. + */ + private getRandomOrganization(organizations: IOrganization[]): IOrganization { + if (organizations.length === 0) { + return null; + } + + const randomIndex = Math.floor(Math.random() * organizations.length); + return organizations[randomIndex]; + } + ngOnDestroy() {} } diff --git a/packages/ui-core/shared/src/lib/selectors/project/project/project.component.ts b/packages/ui-core/shared/src/lib/selectors/project/project/project.component.ts index 2ef8565289d..ff9ab777777 100644 --- a/packages/ui-core/shared/src/lib/selectors/project/project/project.component.ts +++ b/packages/ui-core/shared/src/lib/selectors/project/project/project.component.ts @@ -1,5 +1,9 @@ import { Component, OnInit, OnDestroy, Input, forwardRef, AfterViewInit, Output, EventEmitter } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { combineLatest, from, map, Observable, of, Subject, switchMap } from 'rxjs'; +import { catchError, filter, tap } from 'rxjs/operators'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { IOrganization, IOrganizationProject, @@ -8,10 +12,6 @@ import { ID, IOrganizationProjectsFindInput } from '@gauzy/contracts'; -import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { map, Observable, Subject, switchMap } from 'rxjs'; -import { filter, tap } from 'rxjs/operators'; -import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { distinctUntilChange, isEmpty, isNotEmpty } from '@gauzy/ui-core/common'; import { ErrorHandlingService, @@ -44,103 +44,145 @@ export class ProjectSelectorComponent implements OnInit, OnDestroy, AfterViewIni public organization: IOrganization; public subject$: Subject = new Subject(); - @Input() shortened = false; - @Input() disabled = false; - @Input() multiple = false; - @Input() label = null; + private _projectId: ID | ID[]; // Internal storage for the project IDs. + private _employeeId: ID; // Internal storage for the employee ID. + private _organizationContactId: ID; // Internal storage for the organization contact ID. + + /** + * Determines whether the component should be displayed in a shortened form. + * This might control the size, visibility of certain elements, or compactness of the UI. + * + * @default false + */ + @Input() shortened: boolean = false; + + /** + * Determines whether the component is disabled and non-interactive. + * When set to `true`, user interactions (like clicking or selecting) are disabled. + * + * @default false + */ + @Input() disabled: boolean = false; + + /** + * Allows multiple selections if set to `true`. + * This could enable features like multi-select dropdowns or checkboxes. + * + * @default false + */ + @Input() multiple: boolean = false; + + /** + * The label text to be displayed alongside the component. + * This could be used for accessibility purposes or to provide context to the user. + * + * @default null + */ + @Input() label: string | null = null; + + /** + * The placeholder text to be displayed in the project selector. + * Provides guidance to the user on what action to take or what information to provide. + * + */ + @Input() placeholder: string | null = null; + + /** + * Determines whether to skip triggering global change detection. + * Useful for optimizing performance by preventing unnecessary change detection cycles. + * + * @default false + */ + @Input() skipGlobalChange: boolean = false; /** - * Sets the project ID for this component and triggers change and touch events. - * @param val - The project ID or array of project IDs to be set. + * Enables the default selection behavior. + * When `true`, the component may automatically select a default project upon initialization. + * + * @default true */ - private _projectId: ID | ID[]; - set projectId(val: ID | ID[]) { - this._projectId = val; - this.onChange(val); - this.onTouched(val); + @Input() defaultSelected: boolean = true; + + /** + * Determines whether to display the "Show All" option in the selector. + * Allows users to view and select all available projects if enabled. + * + * @default true + */ + @Input() showAllOption: boolean = true; + + /** + * Sets the project ID and triggers change and touch events. + * + * @param value - The project ID or array of project IDs to be set. + */ + @Input() + public set projectId(value: ID | ID[]) { + this._projectId = value; + this.onChange(value); + this.onTouched(); } - get projectId(): ID | ID[] { + + /** + * Gets the current project ID + * + * @returns The current project ID or array of project IDs. + */ + public get projectId(): ID | ID[] { return this._projectId; } /** - * Sets the employee ID for this component and triggers an update. + * Sets the employee ID and triggers change and touch events. + * * @param value - The ID of the employee to be set. */ - private _employeeId: ID; - @Input() public set employeeId(value: ID) { + @Input() + public set employeeId(value: ID) { this._employeeId = value; this.subject$.next(true); } - public get employeeId() { + + /** + * Gets the current employee ID + * + * @returns The current employee ID or array of employee IDs. + */ + public get employeeId(): ID | undefined { return this._employeeId; } /** - * Sets the organization contact ID for this component and triggers an update. + * Sets the organization contact ID and triggers change and touch events. + * * @param value - The ID of the organization contact to be set. */ - private _organizationContactId: ID; - @Input() public set organizationContactId(value: ID) { + @Input() + public set organizationContactId(value: ID) { this._organizationContactId = value; this.subject$.next(true); } - public get organizationContactId(): ID { - return this._organizationContactId; - } /** - * Sets the placeholder text for this component. - * @param value - The placeholder text to be displayed. + * Gets the current organization contact ID + * + * @returns The current organization contact ID or array of organization contact ID. */ - private _placeholder: string; - @Input() set placeholder(value: string) { - this._placeholder = value; - } - get placeholder(): string { - return this._placeholder; + public get organizationContactId(): ID | undefined { + return this._organizationContactId; } - /** - * Sets the flag to determine whether to skip triggering global change detection. - * @param value - A boolean indicating whether to skip global change detection. - */ - private _skipGlobalChange: boolean = false; - @Input() set skipGlobalChange(value: boolean) { - this._skipGlobalChange = value; - } - get skipGlobalChange(): boolean { - return this._skipGlobalChange; - } + @Output() onChanged = new EventEmitter(); /** - * Sets the default selection flag for this component. - * @param value - A boolean indicating whether to enable the default selection. + * Callback function to notify changes in the form control. */ - private _defaultSelected: boolean = true; - @Input() set defaultSelected(value: boolean) { - this._defaultSelected = value; - } - get defaultSelected(): boolean { - return this._defaultSelected; - } + private onChange: (value: ID | ID[]) => void = () => {}; /** - * Sets the flag to determine whether to display the "Show All" option. - * @param value - A boolean indicating whether to show the "Show All" option. + * Callback function to notify touch events in the form control. */ - private _showAllOption: boolean = true; - @Input() set showAllOption(value: boolean) { - this._showAllOption = value; - } - get showAllOption(): boolean { - return this._showAllOption; - } - - @Output() onChanged = new EventEmitter(); - - onChange: any = () => {}; - onTouched: any = () => {}; + private onTouched: () => void = () => {}; constructor( private readonly _organizationProjects: OrganizationProjectsService, @@ -154,35 +196,19 @@ export class ProjectSelectorComponent implements OnInit, OnDestroy, AfterViewIni ) {} ngOnInit(): void { - this.hasAddProject$ = this._store.userRolePermissions$.pipe( - map(() => this._store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_PROJECT_ADD)) - ); - this.subject$ - .pipe( - switchMap(() => this.getProjects()), - tap(() => { - if (this._activatedRoute.snapshot.queryParams.projectId) { - this.selectProjectById(this._activatedRoute.snapshot.queryParams.projectId); - } - }), - untilDestroyed(this) - ) - .subscribe(); - - this._activatedRoute.queryParams - .pipe( - filter((query) => !!query.projectId), - tap(({ projectId }) => this.selectProjectById(projectId)), - untilDestroyed(this) - ) - .subscribe(); + this.initializePermissions(); + this.initializeProjectSelection(); + this.initializeOrganizationSelection(); + // Handle organization changes and trigger project fetch this._store.selectedOrganization$ .pipe( distinctUntilChange(), filter((organization: IOrganization) => !!organization), - tap((organization: IOrganization) => (this.organization = organization)), - tap(() => this.subject$.next(true)), + tap((organization: IOrganization) => { + this.organization = organization; + this.subject$.next(true); // Triggers project fetch when organization changes + }), untilDestroyed(this) ) .subscribe(); @@ -212,46 +238,130 @@ export class ProjectSelectorComponent implements OnInit, OnDestroy, AfterViewIni }); } + /** + * Initializes the observable that determines if the user has edit permissions for projects. + */ + private initializePermissions(): void { + this.hasAddProject$ = this._store.userRolePermissions$.pipe( + map(() => this._store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_PROJECT_ADD)), + catchError((error) => { + console.error('Error checking permissions:', error); + return of(false); + }), + untilDestroyed(this) + ); + } + + /** + * Handles the combined stream to fetch projects and select the appropriate project + * based on route parameters and subject emissions. + */ + private initializeProjectSelection(): void { + combineLatest([this.subject$, this._activatedRoute.queryParams]) + .pipe( + // Switch to a new observable each time the source observables emit + switchMap(([_, queryParams]) => + // Fetch projects and handle errors during retrieval + from(this.getProjects()).pipe( + // Return the projectId from queryParams on success + map(() => queryParams.projectId), + // Handle any errors that occur during project fetching + catchError((error) => { + console.error('Error fetching projects:', error); + return of(null); // Return a null value to prevent project selection on error + }) + ) + ), + // After fetching, select the project if projectId exists + tap((projectId: ID | null) => { + if (projectId) { + this.selectProjectById(projectId); + } else { + console.warn('Project ID is missing or projects could not be retrieved.'); + } + }), + // Automatically unsubscribe when the component is destroyed + untilDestroyed(this) + ) + .subscribe(); + } + + /** + * Handles changes in the selected organization. + * + * Updates the local organization property and triggers a project fetch. + */ + private initializeOrganizationSelection(): void { + this._store.selectedOrganization$ + .pipe( + // Emit only when the selected organization changes + distinctUntilChange(), + // Proceed only if the organization is defined + filter((organization: IOrganization) => !!organization), + // Update the organization property and trigger a project fetch + tap((organization: IOrganization) => { + this.organization = organization; + this.subject$.next(true); // Triggers the combined stream + }), + untilDestroyed(this) + ) + .subscribe(); + } + /** * Retrieves projects based on specified parameters. * If an employee ID is provided, retrieves projects associated with that employee. * Otherwise, retrieves all projects for the organization. Optionally inserts an "All Projects" option. */ async getProjects(): Promise { - if (!this.organization) return; + // Return early if the organization is not defined + if (!this.organization) { + console.warn('Organization is not defined.'); + return; + } const { id: organizationId, tenantId } = this.organization; + + // Construct query options const queryOptions: IOrganizationProjectsFindInput = { + ...(this.organizationContactId && { organizationContactId: this.organizationContactId }), organizationId, - tenantId, - ...(this.organizationContactId && { organizationContactId: this.organizationContactId }) + tenantId }; - // Retrieve projects based on the presence of employeeId - this.projects = this.employeeId - ? await this._organizationProjects.getAllByEmployee(this.employeeId, queryOptions) - : (await this._organizationProjects.getAll([], queryOptions)).items || []; - - // Optionally add "All Projects" selection - if (this.showAllOption) { - this.projects.unshift(ALL_PROJECT_SELECTED); - this.selectProject(ALL_PROJECT_SELECTED); + try { + // Retrieve projects based on whether employeeId is provided + this.projects = this.employeeId + ? await this._organizationProjects.getAllByEmployee(this.employeeId, queryOptions) + : (await this._organizationProjects.getAll([], queryOptions)).items || []; + + // Optionally add "All Projects" option + if (this.showAllOption) { + this.projects.unshift(ALL_PROJECT_SELECTED); + this.selectProject(ALL_PROJECT_SELECTED); + } + } catch (error) { + console.error('Error retrieving projects:', error); + this._errorHandlingService.handleError(error); } } /** * Writes a value to the component, handling single or multiple selection modes. - * @param {string | string[]} value - The value(s) to write, either a single string or an array of strings. + * + * @param {ID | ID[]} value - The value(s) to write, either a single ID or IDs. */ - writeValue(value: string | string[]): void { + writeValue(value: ID | ID[]): void { this._projectId = this.multiple ? (Array.isArray(value) ? value : [value]) : value; } /** - * Registers a callback function to be called when the rating changes. - * @param {(rating: number) => void} fn - The callback function to register. + * Registers a callback function to be called when the control's value changes. + * This method is used by Angular forms to bind the model to the view. + * + * @param fn - The callback function to register for the 'onChange' event. */ - registerOnChange(fn: (rating: number) => void): void { + registerOnChange(fn: (value: ID | ID[]) => void): void { this.onChange = fn; } @@ -265,6 +375,7 @@ export class ProjectSelectorComponent implements OnInit, OnDestroy, AfterViewIni /** * Sets the disabled state of the component. + * * @param {boolean} isDisabled - The disabled state to set. */ setDisabledState(isDisabled: boolean): void { @@ -273,54 +384,75 @@ export class ProjectSelectorComponent implements OnInit, OnDestroy, AfterViewIni /** * Creates a new project with the given name. + * * @param {string} name - The name of the new project. */ createNew = async (name: string): Promise => { - // Return early if organization is not defined - if (!this.organization) return; + // Return early if organization or project name is not defined + if (!this.organization || !name) { + console.warn('Organization or project name is missing.'); + return; + } try { // Destructure tenantId and organizationId from organization const { id: organizationId, tenantId } = this.organization; + // Include member if employeeId or store user's employeeId is provided + const memberId = this.employeeId || this._store.user.employee?.id; + // Create the project const project = await this._organizationProjects.create({ name, - organizationId, - tenantId, + ...(memberId && { memberIds: [memberId] }), ...(this.organizationContactId && { organizationContactId: this.organizationContactId }), - memberIds: [this.employeeId || this._store.user.employee?.id].filter(Boolean) // Filter out falsy values + organizationId, + tenantId }); // Handle the created project and update projectId this.createOrganizationProject(project); - // Update projectId + // Set the newly created project's ID this.projectId = project.id; // Show success message this._toastrService.success('NOTES.ORGANIZATIONS.EDIT_ORGANIZATIONS_PROJECTS.ADD_PROJECT', { name }); } catch (error) { - // Handle the error - console.log('Error while creating new project: ', error); + // Log and handle the error + console.error('Error while creating new project: ', error); this._errorHandlingService.handleError(error); } }; /** * Adds a newly created organization project to the dropdown list. + * * @param {IOrganizationProject} project - The project to add. */ createOrganizationProject(project: IOrganizationProject): void { - this.projects = [...(this.projects || []), project].filter(isNotEmpty); + if (!project) { + console.warn('Invalid project provided'); + return; + } + + // Ensure projects array is initialized, then add the new project + this.projects = (this.projects ?? []).concat(project).filter(isNotEmpty); } /** * Updates an existing organization project in the dropdown list. + * * @param {IOrganizationProject} project - The project with updated details. */ updateOrganizationProject(project: IOrganizationProject): void { - this.projects = (this.projects || []) + if (!project || !project.id) { + console.warn('Invalid project or missing project ID'); + return; + } + + // Map through projects to update the matching project + this.projects = (this.projects ?? []) .map((item) => (item.id === project.id ? { ...item, ...project } : item)) .filter(isNotEmpty); } @@ -330,24 +462,33 @@ export class ProjectSelectorComponent implements OnInit, OnDestroy, AfterViewIni * @param {IOrganizationProject} project - The project to remove. */ deleteOrganizationProject(project: IOrganizationProject): void { - this.projects = (this.projects || []).filter((item) => item.id !== project.id).filter(isNotEmpty); + if (!project || !project.id) { + console.warn('Invalid project or missing project ID'); + return; + } + + // Filter out the project with the matching ID + this.projects = (this.projects ?? []).filter((item) => item.id !== project.id).filter(isNotEmpty); } /** * Selects the specified project, updates relevant parameters, and emits the change event. + * * @param {IOrganizationProject} project - The project to select. */ selectProject(project: IOrganizationProject): void { - const selectedProject = project || ALL_PROJECT_SELECTED; + const selectedProject = project ?? ALL_PROJECT_SELECTED; + // Update global store and parameters if global changes are allowed if (!this.skipGlobalChange) { this._store.selectedProject = selectedProject; this.setAttributesToParams({ projectId: selectedProject.id }); } + // Update local state and emit the change event this.selectedProject = selectedProject; this.projectId = selectedProject.id; - this.onChanged.emit(project); + this.onChanged.emit(selectedProject); } /** @@ -360,58 +501,66 @@ export class ProjectSelectorComponent implements OnInit, OnDestroy, AfterViewIni /** * Selects a project by its ID and triggers further processing if found. + * * @param {ID} projectId - The unique identifier of the project to select. */ selectProjectById(projectId: ID): void { - const project = this.projects.find((project) => project.id === projectId); - if (project) this.selectProject(project); + if (!projectId) { + console.warn('Invalid project ID provided.'); + return; + } + + const project = this.projects?.find((project) => project.id === projectId); + + if (project) { + this.selectProject(project); + } else { + console.warn(`Project with ID ${projectId} not found.`); + } } /** - * Display clearable option in project selector + * Determines if the project selector should display a clearable option. * - * @returns + * @returns {boolean} - Returns true if the project is clearable, false otherwise. */ isClearable(): boolean { - if (this.selectedProject === ALL_PROJECT_SELECTED) { - return false; - } - return true; + return this.selectedProject !== ALL_PROJECT_SELECTED; } /** - * GET Shortened Name + * Returns a shortened version of the name, with truncation applied to both first and last names. * - * @param name - * @returns + * @param {string} name - The full name to be shortened. + * @param {number} [limit=20] - The maximum character limit for the shortened name. + * @returns {string | undefined} - The shortened name, or undefined if the name is empty. */ - getShortenedName(name: string, limit = 20) { + getShortenedName(name: string, limit = 20): string | undefined { if (isEmpty(name)) { return; } + const chunks = name.split(/\s+/); - const [firstName, lastName] = [chunks.shift(), chunks.join(' ')]; + const firstName = chunks.shift(); + const lastName = chunks.join(' '); + // If both first and last names exist, truncate both if (firstName && lastName) { return ( - this._truncatePipe.transform(firstName, limit / 2, false, '') + + this._truncatePipe.transform(firstName, Math.floor(limit / 2), false, '') + ' ' + - this._truncatePipe.transform(lastName, limit / 2, false, '.') - ); - } else { - return ( - this._truncatePipe.transform(firstName, limit) || - this._truncatePipe.transform(lastName, limit) || - '[error: bad name]' + this._truncatePipe.transform(lastName, Math.floor(limit / 2), false, '.') ); } + + // Fallback to truncating either firstName or lastName if available + return this._truncatePipe.transform(firstName || lastName, limit) || '[error: bad name]'; } /** - * Clear Selector Value - * + * Clears the selected project value if the "Show All" option is disabled. */ - clearSelection() { + clearSelection(): void { if (!this.showAllOption) { this.projectId = null; } diff --git a/packages/ui-core/shared/src/lib/selectors/team/team/team.component.ts b/packages/ui-core/shared/src/lib/selectors/team/team/team.component.ts index 7bfedc3ffb4..9dd640486d8 100644 --- a/packages/ui-core/shared/src/lib/selectors/team/team/team.component.ts +++ b/packages/ui-core/shared/src/lib/selectors/team/team/team.component.ts @@ -1,12 +1,20 @@ import { Component, OnInit, OnDestroy, Input, forwardRef, Output, EventEmitter } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { IOrganization, IOrganizationTeam, CrudActionEnum, PermissionsEnum, IPagination } from '@gauzy/contracts'; +import { + IOrganization, + IOrganizationTeam, + CrudActionEnum, + PermissionsEnum, + ID, + IOrganizationTeamFindInput +} from '@gauzy/contracts'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { map, Observable, Subject, switchMap } from 'rxjs'; -import { filter, tap } from 'rxjs/operators'; +import { combineLatest, map, Observable, of, Subject, switchMap } from 'rxjs'; +import { catchError, filter, tap } from 'rxjs/operators'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { distinctUntilChange, isEmpty, isNotEmpty } from '@gauzy/ui-core/common'; import { + ErrorHandlingService, NavigationService, OrganizationTeamStore, OrganizationTeamsService, @@ -30,130 +38,167 @@ import { ALL_TEAM_SELECTED } from './default-team'; ] }) export class TeamSelectorComponent implements OnInit, OnDestroy { - teams: IOrganizationTeam[] = []; - selectedTeam: IOrganizationTeam; - hasAddTeam$: Observable; - public organization: IOrganization; - subject$: Subject = new Subject(); - onChange: any = () => {}; - onTouched: any = () => {}; + public subject$: Subject = new Subject(); + public hasAddTeam$: Observable; + public teams: IOrganizationTeam[] = []; + public selectedTeam: IOrganizationTeam; - @Input() shortened = false; - @Input() disabled = false; - @Input() multiple = false; - @Input() label = null; + private _teamId: ID | ID[]; // Internal storage for the team ID. + private _employeeId: ID; // Internal storage for the employee ID. + private _organizationContactId: ID; // Internal storage for the organization contact ID. - private _teamId: string | string[]; - get teamId(): string | string[] { - return this._teamId; - } - set teamId(val: string | string[]) { - this._teamId = val; - this.onChange(val); - this.onTouched(val); + /** + * Determines whether the component should be displayed in a shortened form. + * This might control the size, visibility of certain elements, or compactness of the UI. + * + * @default false + */ + @Input() shortened: boolean = false; + + /** + * Determines whether the component is disabled and non-interactive. + * When set to `true`, user interactions (like clicking or selecting) are disabled. + * + * @default false + */ + @Input() disabled: boolean = false; + + /** + * Allows multiple selections if set to `true`. + * This could enable features like multi-select dropdowns or checkboxes. + * + * @default false + */ + @Input() multiple: boolean = false; + + /** + * The label text to be displayed alongside the component. + * This could be used for accessibility purposes or to provide context to the user. + * + * @default null + */ + @Input() label: string | null = null; + + /** + * The placeholder text to be displayed in the team selector. + * Provides guidance to the user on what action to take or what information to provide. + * + */ + @Input() placeholder: string | null = null; + + /** + * Determines whether to skip triggering global change detection. + * Useful for optimizing performance by preventing unnecessary change detection cycles. + * + * @default false + */ + @Input() skipGlobalChange: boolean = false; + + /** + * Enables the default selection behavior. + * When `true`, the component may automatically select a default team upon initialization. + * + * @default true + */ + @Input() defaultSelected: boolean = true; + + /** + * Determines whether to display the "Show All" option in the selector. + * Allows users to view and select all available teams if enabled. + * + * @default true + */ + @Input() showAllOption: boolean = true; + + /** + * Sets the team ID and triggers change and touch events. + * + * @param value - The team ID or array of team IDs to be set. + */ + @Input() + public set teamId(value: ID | ID[]) { + this._teamId = value; + this.onChange(value); + this.onTouched(); } - private _employeeId: string; - public get employeeId() { - return this._employeeId; + /** + * Gets the current team ID + * + * @returns The current team ID or array of team IDs. + */ + public get teamId(): ID | ID[] { + return this._teamId; } - @Input() public set employeeId(value) { + + /** + * Sets the employee ID and triggers change and touch events. + * + * @param value - The ID of the employee to be set. + */ + @Input() + public set employeeId(value: ID) { this._employeeId = value; this.subject$.next(true); } - private _organizationContactId: string; - public get organizationContactId(): string { - return this._organizationContactId; - } - @Input() public set organizationContactId(value: string) { - this._organizationContactId = value; - this.subject$.next(true); + /** + * Gets the current employee ID + * + * @returns The current employee ID or array of employee IDs. + */ + public get employeeId(): ID | undefined { + return this._employeeId; } - /* - * Getter & Setter for dynamic placeholder + /** + * Sets the organization contact ID and triggers change and touch events. + * + * @param value - The ID of the organization contact to be set. */ - private _placeholder: string; - get placeholder(): string { - return this._placeholder; - } - @Input() set placeholder(value: string) { - this._placeholder = value; + @Input() + public set organizationContactId(value: ID) { + this._organizationContactId = value; + this.subject$.next(true); } - /* - * Getter & Setter for skip global change + /** + * Gets the current organization contact ID + * + * @returns The current organization contact ID or array of organization contact ID. */ - private _skipGlobalChange: boolean = false; - get skipGlobalChange(): boolean { - return this._skipGlobalChange; - } - @Input() set skipGlobalChange(value: boolean) { - this._skipGlobalChange = value; + public get organizationContactId(): ID | undefined { + return this._organizationContactId; } - private _defaultSelected: boolean = true; - get defaultSelected(): boolean { - return this._defaultSelected; - } - @Input() set defaultSelected(value: boolean) { - this._defaultSelected = value; - } + @Output() onChanged = new EventEmitter(); - private _showAllOption: boolean = true; - get showAllOption(): boolean { - return this._showAllOption; - } - @Input() set showAllOption(value: boolean) { - this._showAllOption = value; - } + /** + * Callback function to notify changes in the form control. + */ + private onChange: (value: ID | ID[]) => void = () => {}; - @Output() - onChanged = new EventEmitter(); + /** + * Callback function to notify touch events in the form control. + */ + private onTouched: () => void = () => {}; constructor( private readonly _activatedRoute: ActivatedRoute, private readonly _organizationTeamsService: OrganizationTeamsService, - private readonly store: Store, - private readonly toastrService: ToastrService, + private readonly _store: Store, + private readonly _toastrService: ToastrService, + private readonly _errorHandlingService: ErrorHandlingService, private readonly _organizationTeamStore: OrganizationTeamStore, private readonly _truncatePipe: TruncatePipe, private readonly _navigationService: NavigationService ) {} ngOnInit(): void { - this.hasAddTeam$ = this.store.userRolePermissions$.pipe( - map(() => this.store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TEAM_ADD)) - ); - this.subject$ - .pipe( - switchMap(() => this.getTeams()), - tap(() => { - if (this._activatedRoute.snapshot.queryParams.teamId) { - this.selectTeamById(this._activatedRoute.snapshot.queryParams.teamId); - } - }), - untilDestroyed(this) - ) - .subscribe(); - this._activatedRoute.queryParams - .pipe( - filter((query) => !!query.teamId), - tap(({ teamId }) => this.selectTeamById(teamId)), - untilDestroyed(this) - ) - .subscribe(); - this.store.selectedOrganization$ - .pipe( - distinctUntilChange(), - filter((organization: IOrganization) => !!organization), - tap((organization: IOrganization) => (this.organization = organization)), - tap(() => this.subject$.next(true)), - untilDestroyed(this) - ) - .subscribe(); + this.initializePermissions(); + this.initializeTeamSelection(); + this.initializeOrganizationSelection(); } ngAfterViewInit(): void { @@ -181,153 +226,232 @@ export class TeamSelectorComponent implements OnInit, OnDestroy { } /** - * Retrieves teams based on the current organization and employee. + * Initializes the observable that determines if the user has edit permissions for teams. + */ + private initializePermissions(): void { + this.hasAddTeam$ = this._store.userRolePermissions$.pipe( + map(() => this._store.hasAnyPermission(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TEAM_ADD)), + catchError((error) => { + console.error('Error checking permissions:', error); + return of(false); + }), + untilDestroyed(this) + ); + } + + /** + * Handles the combined stream to fetch teams and select the appropriate team + * + * based on route parameters and subject emissions. + */ + private initializeTeamSelection(): void { + combineLatest([this.subject$, this._activatedRoute.queryParams]) + .pipe( + // Switch to a new observable each time the source observables emit + switchMap(([_, queryParams]) => + // Fetch teams and then pass the teamId from queryParams + this.getTeams().then(() => queryParams.teamId) + ), + // After fetching, select the team if teamId exists + tap((teamId: ID) => { + if (teamId) { + this.selectTeamById(teamId); + } + }), + // Automatically unsubscribe when the component is destroyed + untilDestroyed(this) + ) + .subscribe(); + } + + /** + * Handles changes in the selected organization. + * + * Updates the local organization property and triggers a team fetch. + */ + private initializeOrganizationSelection(): void { + this._store.selectedOrganization$ + .pipe( + distinctUntilChange(), + filter((organization: IOrganization) => !!organization), + tap((organization: IOrganization) => (this.organization = organization)), + tap(() => this.subject$.next(true)), + untilDestroyed(this) + ) + .subscribe(); + } + + /** + * Retrieves teams based on specified parameters. + * If an employee ID is provided, retrieves teams associated with that employee. + * Otherwise, retrieves all teams for the organization. Optionally inserts an "All Projects" option. */ async getTeams() { + // Return early if the organization is not defined if (!this.organization) { + console.warn('Organization is not defined.'); return; } - const { tenantId } = this.store.user; - const { id: organizationId } = this.organization; - - let teamsResponse: IPagination; - - if (this.employeeId) { - // Fetch teams based on the current employee - teamsResponse = await this._organizationTeamsService.getMyTeams({ - organizationId, - tenantId, - members: { employeeId: this.employeeId } - }); - } else { - // Fetch all teams for the organization - teamsResponse = await this._organizationTeamsService.getAll([], { - organizationId, - tenantId, - ...(this.organizationContactId && { organizationContactId: this.organizationContactId }) - }); - } + const { id: organizationId, tenantId } = this.organization; - // Update teams list - this.teams = teamsResponse.items || []; + // Construct query options + const queryOptions: IOrganizationTeamFindInput = { + ...(this.organizationContactId && { organizationContactId: this.organizationContactId }), + organizationId, + tenantId + }; - //Insert All Employees Option - if (this.showAllOption) { - this.teams.unshift(ALL_TEAM_SELECTED); - this.selectTeam(ALL_TEAM_SELECTED); + try { + // Retrieve teams based on whether employeeId is provided + this.teams = this.employeeId + ? ( + await this._organizationTeamsService.getMyTeams({ + ...queryOptions, + members: { employeeId: this.employeeId } + }) + ).items || [] + : (await this._organizationTeamsService.getAll([], queryOptions)).items || []; + + // Optionally add "All Projects" option + if (this.showAllOption) { + this.teams.unshift(ALL_TEAM_SELECTED); + this.selectTeam(ALL_TEAM_SELECTED); + } + } catch (error) { + console.error('Error retrieving teams:', error); + this._errorHandlingService.handleError(error); } } - writeValue(value: string | string[]) { - if (this.multiple) { - this._teamId = value instanceof Array ? value : [value]; - } else { - this._teamId = value; - } + /** + * Writes a value to the component, handling single or multiple selection modes. + * + * @param {ID | ID[]} value - The value(s) to write, either a single ID or IDs. + */ + writeValue(value: ID | ID[]): void { + this._teamId = this.multiple ? (Array.isArray(value) ? value : [value]) : value; } - registerOnChange(fn: (rating: number) => void): void { + /** + * Registers a callback function to be called when the control's value changes. + * This method is used by Angular forms to bind the model to the view. + * + * @param fn - The callback function to register for the 'onChange' event. + */ + registerOnChange(fn: (value: ID | ID[]) => void): void { this.onChange = fn; } + /** + * Registers a callback function to be called when the component is touched. + * @param {() => void} fn - The callback function to register. + */ registerOnTouched(fn: () => void): void { this.onTouched = fn; } + /** + * Sets the disabled state of the component. + * + * @param {boolean} isDisabled - The disabled state to set. + */ setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } /** * Creates a new team with the given name. + * * @param {string} name - The name of the new team. */ - createNew = async (name: string) => { - // Check if organization is defined + createNew = async (name: string): Promise => { + // Return early if organization is not defined if (!this.organization) { + console.warn('Organization is not defined.'); return; } try { const { id: organizationId, tenantId } = this.organization; + // Include member if employeeId or store user's employeeId is provided + const memberId = this.employeeId || this._store.user.employee?.id; + // Construct request object with common parameters const request = { name, + ...(memberId && { memberIds: [memberId] }), + ...(this.organizationContactId && { organizationContactId: this.organizationContactId }), organizationId, - tenantId, - ...(this.organizationContactId && { organizationContactId: this.organizationContactId }) + tenantId }; - // Include member if employeeId or store user's employeeId is provided - const employeeId = this.store.user.employee?.id; - - if (this.employeeId || employeeId) { - const member: Record = { - id: this.employeeId || employeeId - }; - request['members'] = [member]; - } - - // Create the team + // Handle the created team and update teamId const team = await this._organizationTeamsService.create(request); - // Call method to handle the created team + // Handle the created team this.createOrganizationTeam(team); - // Update teamId + // Set the newly created team's ID this.teamId = team.id; // Show success message - this.toastrService.success('NOTES.ORGANIZATIONS.EDIT_ORGANIZATIONS_TEAM.ADD_NEW_TEAM', { name }); + this._toastrService.success('NOTES.ORGANIZATIONS.EDIT_ORGANIZATIONS_TEAM.ADD_NEW_TEAM', { name }); } catch (error) { - // Show error message - this.toastrService.error(error); + // Log and show error message + console.error('Error while creating new team: ', error); + this._errorHandlingService.handleError(error); } }; - /* - * After created new organization team pushed on dropdown + /** + * Adds a newly created organization team to the dropdown list. + * + * @param team - The new organization team to add. */ - createOrganizationTeam(team: IOrganizationTeam) { - const teams: IOrganizationTeam[] = this.teams || []; - if (Array.isArray(teams)) { - teams.push(team); + createOrganizationTeam(team: IOrganizationTeam): void { + if (!team) { + console.warn('Invalid team or empty team provided.'); + return; // Ensure the team is valid and not empty before proceeding. } - this.teams = [...teams].filter(isNotEmpty); + + this.teams = [...(this.teams || []), team].filter(isNotEmpty); } - /* - * After updated existing organization team changed in the dropdown + /** + * Updates an existing organization team in the dropdown. + * + * @param team - The updated organization team. */ - updateOrganizationTeam(team: IOrganizationTeam) { - let teams: IOrganizationTeam[] = this.teams || []; - if (Array.isArray(teams) && teams.length) { - teams = teams.map((item: IOrganizationTeam) => { - if (item.id === team.id) { - return Object.assign({}, item, team); - } - return item; - }); + updateOrganizationTeam(team: IOrganizationTeam): void { + if (!team || !team.id) { + console.warn('Invalid team or empty team provided.'); + return; // Ensure the team and its ID are valid before proceeding. } - this.teams = [...teams].filter(isNotEmpty); + + this.teams = (this.teams || []) + .map((item: IOrganizationTeam) => (item.id === team.id ? { ...item, ...team } : item)) + .filter(isNotEmpty); } - /* - * After deleted organization team removed on dropdown + /** + * Removes a deleted organization team from the dropdown. + * + * @param team - The organization team to remove. */ - deleteOrganizationTeam(team: IOrganizationTeam) { - let teams: IOrganizationTeam[] = this.teams || []; - if (Array.isArray(teams) && teams.length) { - teams = teams.filter((item: IOrganizationTeam) => item.id !== team.id); + deleteOrganizationTeam(team: IOrganizationTeam): void { + if (!team || !team.id) { + console.warn('Invalid team or empty team provided.'); + return; // Ensure the team and its ID are valid before proceeding. } - this.teams = [...teams].filter(isNotEmpty); + + this.teams = (this.teams || []).filter((item: IOrganizationTeam) => item.id !== team.id).filter(isNotEmpty); } selectTeam(team: IOrganizationTeam): void { if (!this.skipGlobalChange) { - this.store.selectedTeam = team || ALL_TEAM_SELECTED; + this._store.selectedTeam = team || ALL_TEAM_SELECTED; this.setAttributesToParams({ teamId: team?.id }); } this.selectedTeam = team || ALL_TEAM_SELECTED; @@ -343,7 +467,12 @@ export class TeamSelectorComponent implements OnInit, OnDestroy { await this._navigationService.updateQueryParams(params); } - selectTeamById(teamId: string) { + /** + * Selects a team by its ID. + * + * @param teamId - The ID of the team to select. + */ + selectTeamById(teamId: ID): void { const team = this.teams.find((team: IOrganizationTeam) => teamId === team.id); if (team) { this.selectTeam(team); @@ -351,50 +480,47 @@ export class TeamSelectorComponent implements OnInit, OnDestroy { } /** - * Display clearable option in team selector + * Determines if the "clear" option should be displayed in the team selector. * - * @returns + * @returns True if the "clear" option should be displayed, false otherwise. */ isClearable(): boolean { - if (this.selectedTeam === ALL_TEAM_SELECTED) { - return false; - } - return true; + return this.selectedTeam !== ALL_TEAM_SELECTED; } /** - * GET Shortened Name + * Returns a shortened version of the name, with truncation applied to both first and last names. * - * @param name - * @returns + * @param {string} name - The full name to be shortened. + * @param {number} [limit=20] - The maximum character limit for the shortened name. + * @returns {string | undefined} - The shortened name, or undefined if the name is empty. */ - getShortenedName(name: string, limit = 20) { + getShortenedName(name: string, limit = 20): string | undefined { if (isEmpty(name)) { return; } + const chunks = name.split(/\s+/); - const [firstName, lastName] = [chunks.shift(), chunks.join(' ')]; + const firstName = chunks.shift(); + const lastName = chunks.join(' '); + // If both first and last names exist, truncate both if (firstName && lastName) { return ( - this._truncatePipe.transform(firstName, limit / 2, false, '') + + this._truncatePipe.transform(firstName, Math.floor(limit / 2), false, '') + ' ' + - this._truncatePipe.transform(lastName, limit / 2, false, '.') - ); - } else { - return ( - this._truncatePipe.transform(firstName, limit) || - this._truncatePipe.transform(lastName, limit) || - '[error: bad name]' + this._truncatePipe.transform(lastName, Math.floor(limit / 2), false, '.') ); } + + // Fallback to truncating either firstName or lastName if available + return this._truncatePipe.transform(firstName || lastName, limit) || '[error: bad name]'; } /** - * Clear Selector Value - * + * Clears the selected team value if the "Show All" option is disabled. */ - clearSelection() { + clearSelection(): void { if (!this.showAllOption) { this.teamId = null; }