diff --git a/apps/desktop-timer/src/app/app.module.ts b/apps/desktop-timer/src/app/app.module.ts index a6a9d67a73c..559691994ed 100644 --- a/apps/desktop-timer/src/app/app.module.ts +++ b/apps/desktop-timer/src/app/app.module.ts @@ -22,6 +22,7 @@ import { LanguageInterceptor, LanguageModule, LoggerService, + NgxDesktopThemeModule, NgxLoginModule, NoAuthGuard, OrganizationInterceptor, @@ -34,14 +35,14 @@ import { SetupModule, SplashScreenModule, Store, + TaskTableModule, TenantInterceptor, TimeTrackerModule, TimeoutInterceptor, TokenInterceptor, UnauthorizedInterceptor, UpdaterModule, - serverConnectionFactory, - NgxDesktopThemeModule + serverConnectionFactory } from '@gauzy/desktop-ui-lib'; import { environment as gauzyEnvironment } from '@gauzy/ui-config'; import { @@ -117,7 +118,8 @@ if (environment.SENTRY_DSN) { NbDatepickerModule.forRoot(), AboutModule, ActivityWatchModule, - RecapModule + RecapModule, + TaskTableModule ], providers: [ AppService, @@ -198,7 +200,7 @@ if (environment.SENTRY_DSN) { }, { provide: APP_INITIALIZER, - useFactory: () => () => { }, + useFactory: () => () => {}, deps: [Sentry.TraceService], multi: true }, @@ -214,4 +216,4 @@ if (environment.SENTRY_DSN) { bootstrap: [AppComponent], exports: [] }) -export class AppModule { } +export class AppModule {} diff --git a/packages/desktop-ui-lib/src/index.ts b/packages/desktop-ui-lib/src/index.ts index b13d8ba14cb..0a457bf941c 100644 --- a/packages/desktop-ui-lib/src/index.ts +++ b/packages/desktop-ui-lib/src/index.ts @@ -12,8 +12,8 @@ export * from './lib/image-viewer/image-viewer.component'; export * from './lib/image-viewer/image-viewer.module'; export * from './lib/integrations'; export * from './lib/interceptors'; -export * from './lib/language/language.module'; export * from './lib/language/language-electron.service'; +export * from './lib/language/language.module'; export * from './lib/ngx-translate'; export * from './lib/recap/recap-routing.module'; export * from './lib/recap/recap.module'; @@ -31,13 +31,14 @@ export * from './lib/setup/setup.module'; export * from './lib/setup/setup.service'; export * from './lib/splash-screen/splash-screen.component'; export * from './lib/splash-screen/splash-screen.module'; +export * from './lib/theme'; export * from './lib/time-tracker/organization-selector/user-organization.service'; +export * from './lib/time-tracker/task-table/task-table.module'; export * from './lib/time-tracker/time-tracker.component'; export * from './lib/time-tracker/time-tracker.module'; export * from './lib/time-tracker/time-tracker.service'; export * from './lib/updater/updater.component'; export * from './lib/updater/updater.module'; -export * from './lib/theme'; /** * Auth Module */ diff --git a/packages/desktop-ui-lib/src/lib/language/language-electron.service.ts b/packages/desktop-ui-lib/src/lib/language/language-electron.service.ts index f46ca4ae13b..6e02340edaf 100644 --- a/packages/desktop-ui-lib/src/lib/language/language-electron.service.ts +++ b/packages/desktop-ui-lib/src/lib/language/language-electron.service.ts @@ -20,22 +20,13 @@ export class LanguageElectronService { ) {} public initialize(callback?: T) { - this.electronService.ipcRenderer.on('preferred_language_change', (event, language: LanguagesEnum) => { - this.ngZone.run(() => { - this.languageSelectorService.setLanguage(language, this.translateService); - TimeTrackerDateManager.locale(language); - if (callback) { - callback; - } - }); - }); + this.onLanguageChange(callback); from(this.electronService.ipcRenderer.invoke('PREFERRED_LANGUAGE')) .pipe( tap((language: LanguagesEnum) => { this.languageSelectorService.setLanguage(language, this.translateService); TimeTrackerDateManager.locale(language); - console.log('PREFERRED_LANGUAGE', language); if (callback) { callback; } @@ -44,4 +35,16 @@ export class LanguageElectronService { ) .subscribe(); } + + public onLanguageChange(callback?: T) { + this.electronService.ipcRenderer.on('preferred_language_change', (event, language: LanguagesEnum) => { + this.ngZone.run(() => { + this.languageSelectorService.setLanguage(language, this.translateService); + TimeTrackerDateManager.locale(language); + if (callback) { + callback; + } + }); + }); + } } diff --git a/packages/desktop-ui-lib/src/lib/recap/monthly/features/monthly-statistic/monthly-statistic.component.scss b/packages/desktop-ui-lib/src/lib/recap/monthly/features/monthly-statistic/monthly-statistic.component.scss index 7f65afa2f18..6c2a324983c 100644 --- a/packages/desktop-ui-lib/src/lib/recap/monthly/features/monthly-statistic/monthly-statistic.component.scss +++ b/packages/desktop-ui-lib/src/lib/recap/monthly/features/monthly-statistic/monthly-statistic.component.scss @@ -25,6 +25,7 @@ nb-card { line-height: 44px; letter-spacing: 0; text-wrap: nowrap; + margin: 0; } .h2 { diff --git a/packages/desktop-ui-lib/src/lib/recap/recap-routing.module.ts b/packages/desktop-ui-lib/src/lib/recap/recap-routing.module.ts index 813785a74c3..0b78d9fbe8d 100644 --- a/packages/desktop-ui-lib/src/lib/recap/recap-routing.module.ts +++ b/packages/desktop-ui-lib/src/lib/recap/recap-routing.module.ts @@ -4,14 +4,30 @@ export const recapRoutes: Routes = [ { path: '', pathMatch: 'full', - redirectTo: 'daily' + redirectTo: 'tasks' }, { path: 'daily', + loadComponent: () => import('./features/recap/recap.component').then((m) => m.RecapComponent), loadChildren: () => import('./recap-children-routing.module').then((m) => m.recapChildRoutes) }, + { + path: 'weekly', + loadComponent: () => + import('./weekly/features/weekly-recap/weekly-recap.component').then((m) => m.WeeklyRecapComponent) + }, + { + path: 'monthly', + loadComponent: () => + import('./monthly/features/monthly-recap/monthly-recap.component').then((m) => m.MonthlyRecapComponent) + }, + { + path: 'tasks', + loadChildren: () => + import('../time-tracker/task-table/task-table.routing.module').then((m) => m.taskTableRoutes) + }, { path: '**', - redirectTo: 'daily' + redirectTo: 'tasks' } ]; diff --git a/packages/desktop-ui-lib/src/lib/recap/shared/features/activity-report/activity-report.component.scss b/packages/desktop-ui-lib/src/lib/recap/shared/features/activity-report/activity-report.component.scss index c161f695528..5de4f56ae24 100644 --- a/packages/desktop-ui-lib/src/lib/recap/shared/features/activity-report/activity-report.component.scss +++ b/packages/desktop-ui-lib/src/lib/recap/shared/features/activity-report/activity-report.component.scss @@ -6,7 +6,7 @@ padding-bottom: 10px; background-color: var(--gauzy-card-1); border-radius: var(--border-radius); - height: calc(100vh - 22.625rem); + height: calc(100vh - 23.625rem); overflow-y: auto; .table-row-custom { @@ -96,7 +96,7 @@ } .main-report-wrapper { - height: calc(100vh - 8rem); + height: calc(100vh - 9rem); margin-bottom: 0 !important; .main-report-body { diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-progress/weekly-progress.component.scss b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-progress/weekly-progress.component.scss index 5d2650006c6..c29abe60153 100644 --- a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-progress/weekly-progress.component.scss +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-progress/weekly-progress.component.scss @@ -34,7 +34,7 @@ display: flex; flex-direction: column; gap: 1rem; - padding: 1rem; + padding: 14px; border-radius: var(--border-radius); background-color: var(--gauzy-card-3); height: 100%; diff --git a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-statistic/weekly-statistic.component.scss b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-statistic/weekly-statistic.component.scss index 7f65afa2f18..6c2a324983c 100644 --- a/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-statistic/weekly-statistic.component.scss +++ b/packages/desktop-ui-lib/src/lib/recap/weekly/features/weekly-statistic/weekly-statistic.component.scss @@ -25,6 +25,7 @@ nb-card { line-height: 44px; letter-spacing: 0; text-wrap: nowrap; + margin: 0; } .h2 { diff --git a/packages/desktop-ui-lib/src/lib/services/task-cache.service.ts b/packages/desktop-ui-lib/src/lib/services/task-cache.service.ts index 31d73d9f27c..557c5a405e9 100644 --- a/packages/desktop-ui-lib/src/lib/services/task-cache.service.ts +++ b/packages/desktop-ui-lib/src/lib/services/task-cache.service.ts @@ -14,5 +14,6 @@ export class TaskCacheService extends AbstractCacheService { ) { super(_storageService, _store); this.prefix = TaskCacheService.name.toString(); + this.duration = 24 * 3600 * 1000; // 1 day } } diff --git a/packages/desktop-ui-lib/src/lib/shared/+state/selector.service.ts b/packages/desktop-ui-lib/src/lib/shared/+state/selector.service.ts index 38ad453551b..622c9e0b5d7 100644 --- a/packages/desktop-ui-lib/src/lib/shared/+state/selector.service.ts +++ b/packages/desktop-ui-lib/src/lib/shared/+state/selector.service.ts @@ -41,4 +41,8 @@ export abstract class SelectorService { public get hasPermission$(): Observable { return this.selectorQuery.hasPermission$; } + + public get selected$(): Observable { + return this.selectorQuery.selected$; + } } diff --git a/packages/desktop-ui-lib/src/lib/shared/features/task-selector/+state/task-selector.service.ts b/packages/desktop-ui-lib/src/lib/shared/features/task-selector/+state/task-selector.service.ts index 9a9b6455c87..35a53ab31eb 100644 --- a/packages/desktop-ui-lib/src/lib/shared/features/task-selector/+state/task-selector.service.ts +++ b/packages/desktop-ui-lib/src/lib/shared/features/task-selector/+state/task-selector.service.ts @@ -101,7 +101,7 @@ export class TaskSelectorService extends SelectorService { } } - private merge(tasks: ITask[], statistics: ITasksStatistics[]): (ITask & ITasksStatistics)[] { + public merge(tasks: ITask[], statistics: ITasksStatistics[]): (ITask & ITasksStatistics)[] { let arr: (ITask & ITasksStatistics)[] = []; arr = arr.concat(statistics, tasks); return arr.reduce((result, current) => { diff --git a/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts b/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts index 300e0579e55..8b55930d6ff 100644 --- a/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts +++ b/packages/desktop-ui-lib/src/lib/tasks/tasks.component.ts @@ -1,6 +1,5 @@ -import { Component, EventEmitter, Input, OnInit, Output, Inject } from '@angular/core'; +import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; import { FormControl, UntypedFormGroup, Validators } from '@angular/forms'; -import { TimeTrackerService } from '../time-tracker/time-tracker.service'; import { IEmployee, IOrganizationContact, @@ -16,13 +15,17 @@ import { TaskStatusEnum } from '@gauzy/contracts'; import { NbDialogRef, NbToastrService } from '@nebular/theme'; -import * as moment from 'moment'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateService } from '@ngx-translate/core'; -import { CkEditorConfig, ColorAdapter } from '../utils'; -import { Store, TagService } from '../services'; +import * as moment from 'moment'; +import { concatMap, from, map, Observable, tap } from 'rxjs'; import { GAUZY_ENV } from '../constants'; -import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { tap, from, Observable, map } from 'rxjs'; +import { Store, TagService } from '../services'; +import { ClientSelectorService } from '../shared/features/client-selector/+state/client-selector.service'; +import { ProjectSelectorService } from '../shared/features/project-selector/+state/project-selector.service'; +import { TeamSelectorService } from '../shared/features/team-selector/+state/team-selector.service'; +import { TimeTrackerService } from '../time-tracker/time-tracker.service'; +import { CkEditorConfig, ColorAdapter } from '../utils'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -31,13 +34,17 @@ import { tap, from, Observable, map } from 'rxjs'; styleUrls: ['./tasks.component.scss'] }) export class TasksComponent implements OnInit { - @Input() userData: IUserOrganization; - @Input() employee: IEmployee; - @Input() hasProjectPermission: boolean; + @Input() userData: IUserOrganization = this.store.user as any; + @Input() employee: IEmployee = this.store.user.employee; + @Input() hasProjectPermission: boolean = this.projectSelectorService.hasPermission; @Input() selected: { projectId: IOrganizationProject['id']; teamId: IOrganizationTeam['id']; contactId: IOrganizationContact['id']; + } = { + projectId: this.projectSelectorService.selectedId, + teamId: this.teamSelectorService.selectedId, + contactId: this.clientSelectorService.selectedId }; @Output() isAddTask: EventEmitter = new EventEmitter(); @Output() newTaskCallback: EventEmitter<{ @@ -93,20 +100,43 @@ export class TasksComponent implements OnInit { private readonly _environment: any, private store: Store, private _dialogRef: NbDialogRef, - private _tagService: TagService + private _tagService: TagService, + private readonly clientSelectorService: ClientSelectorService, + private readonly teamSelectorService: TeamSelectorService, + private readonly projectSelectorService: ProjectSelectorService ) { this.isSaving = false; } - private async _projects(user: IUserOrganization): Promise { + private async _projects(value?: { + organizationContactId?: string; + organizationTeamId?: string; + projectId?: string; + }): Promise { try { - this.projects = await this.timeTrackerService.getProjects({ - organizationContactId: this.selected.contactId, - organizationTeamId: this.selected.teamId, - ...user - }); + const { organizationId, user, tenantId } = this.store; + const employeeId = user?.employee?.id; + + if (!employeeId) { + throw new Error('Employee ID is missing.'); + } + + const filterParams = { + organizationId, + tenantId, + employeeId, + ...(value?.organizationContactId && { organizationContactId: value.organizationContactId }), + ...(value?.organizationTeamId && { organizationTeamId: value.organizationTeamId }) + }; + + this.projects = await this.timeTrackerService.getProjects(filterParams); + + // Clear the form's projectId if the selected project does not exist in the fetched list + if (value?.projectId && !this.projects.some(({ id }) => id === value.projectId)) { + this.form.patchValue({ projectId: null }); + } } catch (error) { - console.error('[error]', "can't get employee project::" + error.message); + console.error('[Projects Fetch Error]', `Unable to fetch employee projects: ${error.message}`); } } @@ -118,18 +148,22 @@ export class TasksComponent implements OnInit { } } - private async _employees(user: IUserOrganization): Promise { + private async _employees(): Promise { try { - const employee = await this.timeTrackerService.getEmployees(user); + const { organizationId, user, tenantId } = this.store; + const employeeId = user.employee.id; + const employee = await this.timeTrackerService.getEmployees({ employeeId, organizationId, tenantId }); this.employees = [employee]; } catch (error) { console.error('[error]', 'while get employees::' + error.message); } } - private async _clients(user: IUserOrganization): Promise { + private async _clients(): Promise { try { - this.contacts = await this.timeTrackerService.getClient(user); + const { organizationId, user, tenantId } = this.store; + const employeeId = user.employee.id; + this.contacts = await this.timeTrackerService.getClient({ organizationId, employeeId, tenantId }); } catch (error) { console.error('[error]', 'while get contacts::' + error.message); } @@ -167,14 +201,15 @@ export class TasksComponent implements OnInit { } ngOnInit() { + const { organizationId, tenantId } = this.store; this.editorConfig.editorplaceholder = this.translate.instant('FORM.PLACEHOLDERS.DESCRIPTION'); this.taskStatuses = this.store.statuses; from( Promise.allSettled([ - this._projects(this.userData), + this._projects(), this._tags(), - this._employees(this.userData), - this._clients(this.userData), + this._employees(), + this._clients(), this._teams(), this._sizes(), this._priorities() @@ -194,7 +229,7 @@ export class TasksComponent implements OnInit { estimateHours: new FormControl(null, [Validators.min(0), Validators.max(23)]), estimateMinutes: new FormControl(null, [Validators.min(0), Validators.max(59)]), members: new FormControl([]), - organizationId: new FormControl(this.userData.organizationId), + organizationId: new FormControl(organizationId), project: new FormControl(null), projectId: new FormControl(this.selected.projectId), status: new FormControl(TaskStatusEnum.OPEN), @@ -202,7 +237,7 @@ export class TasksComponent implements OnInit { size: new FormControl(null), tags: new FormControl([]), teams: new FormControl([]), - tenantId: new FormControl(this.userData.tenantId), + tenantId: new FormControl(tenantId), title: new FormControl(null, Validators.required), taskStatus: new FormControl(null), taskPriority: new FormControl(null), @@ -210,6 +245,12 @@ export class TasksComponent implements OnInit { organizationContactId: new FormControl(this.selected.contactId), organizationTeamId: new FormControl(this.selected.teamId) }); + this.form.valueChanges + .pipe( + concatMap((values) => this._projects(values)), + untilDestroyed(this) + ) + .subscribe(); this.hasAddTagPermission$ = this.store.userRolePermissions$.pipe( map(() => this.store.hasPermissions(PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_TAGS_ADD)) ); @@ -232,6 +273,7 @@ export class TasksComponent implements OnInit { taskSize, organizationTeamId } = this.form.value; + const { user } = this.store; const days = estimateDays ? estimateDays * 24 * 3600 : 0; const hours = estimateHours ? estimateHours * 3600 : 1; const minutes = estimateMinutes ? estimateMinutes * 60 : 0; @@ -252,7 +294,7 @@ export class TasksComponent implements OnInit { teams }); - await this.timeTrackerService.saveNewTask(this.userData, this.form.value); + await this.timeTrackerService.saveNewTask(user, this.form.value); this.close({ isSuccess: true, message: this.translate.instant('TOASTR.MESSAGE.CREATED') @@ -269,30 +311,7 @@ export class TasksComponent implements OnInit { } public addProject = async (name: string) => { - try { - const data = this.userData as any; - const { tenantId, organizationContactId } = data; - const { organizationId } = data; - - const request = { - name, - organizationId, - tenantId, - ...(organizationContactId ? { contactId: organizationContactId } : {}) - }; - - request['members'] = [...this.employees]; - - const project = await this.timeTrackerService.createNewProject(request, data); - - this.projects = this.projects.concat([project]); - this.toastrService.success( - this.translate.instant('TIMER_TRACKER.TOASTR.PROJECT_ADDED'), - this._environment.DESCRIPTION - ); - } catch (error) { - this.toastrService.danger(error); - } + await this.projectSelectorService.addProject(name); }; public background(bgColor: string) { diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/+state/task-table.query.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/+state/task-table.query.ts new file mode 100644 index 00000000000..25df945dfbc --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/+state/task-table.query.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { Query } from '@datorama/akita'; +import { ITask } from '@gauzy/contracts'; +import { Observable } from 'rxjs'; +import { ITaskTableState, TaskTableStore } from './task-table.store'; + +@Injectable({ providedIn: 'root' }) +export class TaskTableQuery extends Query { + public readonly data$: Observable = this.select((state) => state.data); + public readonly total$: Observable = this.select((state) => state.total); + constructor(protected store: TaskTableStore) { + super(store); + } + + public get tasks(): ITask[] { + return this.getValue().data; + } + + public get total(): number { + return this.getValue().total; + } + + public get page(): number { + return this.getValue().page; + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/+state/task-table.store.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/+state/task-table.store.ts new file mode 100644 index 00000000000..fb1044bf85b --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/+state/task-table.store.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { Store, StoreConfig } from '@datorama/akita'; +import { ITask } from '@gauzy/contracts'; + +export interface ITaskTableState { + data: ITask[]; + total: number; + page: number; +} + +export function createInitialState(): ITaskTableState { + return { + data: [], + page: 1, + total: 0 + }; +} + +@StoreConfig({ name: '_taskTable' }) +@Injectable({ providedIn: 'root' }) +export class TaskTableStore extends Store { + constructor() { + super(createInitialState()); + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/+state/action-button.query.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/+state/action-button.query.ts new file mode 100644 index 00000000000..9ccc1261b86 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/+state/action-button.query.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { Query } from '@datorama/akita'; +import { Observable } from 'rxjs'; +import { ActionButton, ActionButtonStore, IActionButtonState } from './action-button.store'; + +@Injectable({ providedIn: 'root' }) +export class ActionButtonQuery extends Query { + public readonly toggle$: Observable = this.select((state) => state.toggle); + public readonly action$: Observable = this.select((state) => state.action); + constructor(protected store: ActionButtonStore) { + super(store); + } + + public get toggle(): boolean { + return this.getValue().toggle; + } + + public get action(): ActionButton { + return this.getValue().action; + } + +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/+state/action-button.store.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/+state/action-button.store.ts new file mode 100644 index 00000000000..1ce2686d050 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/+state/action-button.store.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core'; +import { Store, StoreConfig } from '@datorama/akita'; + +export enum ActionButton { + ADD = 'adding', + VIEW = 'viewing', + EDIT = 'editing', + DELETE = 'deleting', + NONE = 'none' +} + +export interface IActionButtonState { + toggle: boolean; + action: ActionButton; +} + +export function createInitialState(): IActionButtonState { + return { + toggle: false, + action: ActionButton.NONE + }; +} + +@StoreConfig({ name: '_actionButton' }) +@Injectable({ providedIn: 'root' }) +export class ActionButtonStore extends Store { + constructor() { + super(createInitialState()); + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/action-button.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/action-button.component.html new file mode 100644 index 00000000000..0cad83c8255 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/action-button.component.html @@ -0,0 +1,19 @@ +
+
+ +
+
+ + + +
+
diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/action-button.component.scss b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/action-button.component.scss new file mode 100644 index 00000000000..36cffcc1276 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/action-button.component.scss @@ -0,0 +1,16 @@ +@import 'gauzy/_gauzy-table'; + +.button-container, +.button-hide { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 0.25rem; +} + +.button-add, +.button-hide { + padding: 0.25rem; + background-color: var(--gauzy-card-2); + border-radius: var(--button-rectangle-border-radius); +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/action-button.component.spec.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/action-button.component.spec.ts new file mode 100644 index 00000000000..0db81a61606 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/action-button.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActionButtonComponent } from './action-button.component'; + +describe('ActionButtonComponent', () => { + let component: ActionButtonComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ActionButtonComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(ActionButtonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/action-button.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/action-button.component.ts new file mode 100644 index 00000000000..d238e6aefb9 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/action-button/action-button.component.ts @@ -0,0 +1,83 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ITask } from '@gauzy/contracts'; +import { NbDialogService } from '@nebular/theme'; +import { filter, Observable, of, tap } from 'rxjs'; +import { ElectronService } from '../../../electron/services'; +import { ToastrNotificationService } from '../../../services'; +import { TaskSelectorService } from '../../../shared/features/task-selector/+state/task-selector.service'; +import { TasksComponent } from '../../../tasks/tasks.component'; +import { TaskDetailComponent } from '../../task-render/task-detail/task-detail.component'; +import { ActionButtonQuery } from './+state/action-button.query'; +import { ActionButton, ActionButtonStore } from './+state/action-button.store'; + +@Component({ + selector: 'ngx-action-button', + templateUrl: './action-button.component.html', + styleUrls: ['./action-button.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ActionButtonComponent { + constructor( + private readonly toastrService: ToastrNotificationService, + private readonly electronService: ElectronService, + private readonly taskSelectorService: TaskSelectorService, + private readonly actionButtonStore: ActionButtonStore, + private readonly actionButtonQuery: ActionButtonQuery, + private readonly dialogService: NbDialogService + ) {} + public add() { + this.actionButtonStore.update({ action: ActionButton.ADD }); + this.dialogService + .open(TasksComponent, { + backdropClass: 'backdrop-blur' + }) + .onClose.pipe( + tap(() => this.onFinishAdding()), + filter(Boolean), + tap((result) => this.onCreateNewTask(result)) + ) + .subscribe(); + } + public view() { + this.actionButtonStore.update({ action: ActionButton.EDIT }); + this.dialogService + .open(TaskDetailComponent, { + context: { + task: this.taskSelectorService.selected as any + }, + backdropClass: 'backdrop-blur' + }) + .onClose.pipe(tap(() => this.actionButtonStore.update({ action: ActionButton.NONE, toggle: false }))) + .subscribe(); + } + public edit() { + // TODO + } + public delete() { + // TODO + } + + public onFinishAdding() { + this.actionButtonStore.update({ action: ActionButton.NONE, toggle: false }); + this.electronService.ipcRenderer.send('refresh-timer'); + } + + public onCreateNewTask({ isSuccess, message }): void { + if (isSuccess) { + this.toastrService.success(message); + this.electronService.ipcRenderer.send('refresh-timer'); + } else { + this.toastrService.error(message); + } + } + public get task$(): Observable { + return this.taskSelectorService.selected$; + } + + public get showHiddenButton$(): Observable { + // return combineLatest([this.task$, this.actionButtonQuery.toggle$]).pipe( + // map(([task, toggle]) => !!task && toggle) + // ); + return of(false); // TODO + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/+state/search-term.query.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/+state/search-term.query.ts new file mode 100644 index 00000000000..7137496149e --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/+state/search-term.query.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { Query } from '@datorama/akita'; +import { Observable } from 'rxjs'; +import { ISearchTermState, SearchTermStore } from './search-term.store'; + +@Injectable({ providedIn: 'root' }) +export class SearchTermQuery extends Query { + public readonly value$: Observable = this.select((state) => state.value); + constructor(protected store: SearchTermStore) { + super(store); + } + + public get value(): string { + return this.getValue().value; + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/+state/search-term.store.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/+state/search-term.store.ts new file mode 100644 index 00000000000..461d1458a85 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/+state/search-term.store.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { Store, StoreConfig } from '@datorama/akita'; + +export interface ISearchTermState { + value: string; +} + +export function createInitialState(): ISearchTermState { + return { + value: '' + }; +} + +@StoreConfig({ name: '_searchTerm' }) +@Injectable({ providedIn: 'root' }) +export class SearchTermStore extends Store { + constructor() { + super(createInitialState()); + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/search.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/search.component.html new file mode 100644 index 00000000000..3e34fccc3b4 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/search.component.html @@ -0,0 +1,13 @@ + + + + diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/search.component.scss b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/search.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/search.component.spec.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/search.component.spec.ts new file mode 100644 index 00000000000..826a633794a --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/search.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SearchComponent } from './search.component'; + +describe('SearchComponent', () => { + let component: SearchComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SearchComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(SearchComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/search.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/search.component.ts new file mode 100644 index 00000000000..02e641bad87 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/search/search.component.ts @@ -0,0 +1,30 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { Observable } from 'rxjs'; +import { TimeTrackerQuery } from '../../+state/time-tracker.query'; +import { SearchTermQuery } from './+state/search-term.query'; +import { SearchTermStore } from './+state/search-term.store'; + +@Component({ + selector: 'ngx-search', + templateUrl: './search.component.html', + styleUrls: ['./search.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SearchComponent { + constructor( + private readonly searchTermStore: SearchTermStore, + private readonly searchTermQuery: SearchTermQuery, + private readonly timeTrackerQuery: TimeTrackerQuery + ) {} + public onSearch(searchTerm: string) { + this.searchTermStore.update({ value: searchTerm }); + } + + public get searchTerm$(): Observable { + return this.searchTermQuery.value$; + } + + public get disabled$(): Observable { + return this.timeTrackerQuery.disabled$; + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.html new file mode 100644 index 00000000000..053de80b41b --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.html @@ -0,0 +1,34 @@ + + +
+
+ +
+
+ +
+
+
+
+
+ + +
+
+ +
+
+ +
+ +
+
+
+
+
diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.scss b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.scss new file mode 100644 index 00000000000..acbd8ef2959 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.scss @@ -0,0 +1,40 @@ +@import 'themes'; + +nb-card { + margin: 0 !important; + height: calc(100vh - 9rem); + background-color: var(--gauzy-card-2); + border-top-left-radius: 0; + + nb-card-body { + padding: 1rem !important; + display: flex; + flex-direction: column; + gap: 1rem; + overflow: unset; + } +} + +.grow { + flex-grow: 10; +} + +:host .custom-table { + height: calc(100vh - 14.375rem); + display: flex; + flex-direction: column; + @include nb-rtl(padding-left, 3px); + @include nb-ltr(padding-right, 3px); + + .table-scroll-container { + max-height: unset !important; + @include nb-rtl(padding-left, 12px !important); + @include nb-ltr(padding-right, 12px !important); + } + + .pagination-container { + margin-top: 12px; + @include nb-rtl(padding-left, 1rem); + @include nb-ltr(padding-right, 1rem); + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.spec.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.spec.ts new file mode 100644 index 00000000000..57fdee4f6c5 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TaskTableComponent } from './task-table.component'; + +describe('TaskTableComponent', () => { + let component: TaskTableComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TaskTableComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(TaskTableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.ts new file mode 100644 index 00000000000..ed2fa7d8981 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/table/task-table.component.ts @@ -0,0 +1,330 @@ +import { HttpClient } from '@angular/common/http'; +import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; +import { ITask, ITaskStatus, ITaskUpdateInput, TaskStatusEnum } from '@gauzy/contracts'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { TranslateService } from '@ngx-translate/core'; +import { Angular2SmartTableComponent, Cell } from 'angular2-smart-table'; +import { combineLatest, concatMap, tap } from 'rxjs'; +import { TaskTableStore } from '../+state/task-table.store'; +import { API_PREFIX } from '../../../constants'; +import { ElectronService } from '../../../electron/services'; +import { LanguageElectronService } from '../../../language/language-electron.service'; +import { Store, TaskCacheService, ToastrNotificationService } from '../../../services'; +import { ProjectSelectorService } from '../../../shared/features/project-selector/+state/project-selector.service'; +import { TaskSelectorService } from '../../../shared/features/task-selector/+state/task-selector.service'; +import { TeamSelectorService } from '../../../shared/features/team-selector/+state/team-selector.service'; +import { CachedServerDataSource } from '../../../utils/smart-table/cached-server.data-source'; +import { TaskDurationComponent, TaskProgressComponent } from '../../task-render'; +import { TaskRenderCellComponent } from '../../task-render/task-render-cell/task-render-cell.component'; +import { TaskStatusComponent } from '../../task-render/task-status/task-status.component'; +import { TimeTrackerService } from '../../time-tracker.service'; +import { ActionButtonStore } from '../action-button/+state/action-button.store'; +import { SearchTermQuery } from '../search/+state/search-term.query'; + +@UntilDestroy({ checkProperties: true }) +@Component({ + selector: 'ngx-task-table', + templateUrl: './task-table.component.html', + styleUrls: ['./task-table.component.scss'] +}) +export class TaskTableComponent implements OnInit, AfterViewInit { + private _smartTable: Angular2SmartTableComponent; + public smartTableSource: CachedServerDataSource; + public smartTableSettings: any; + + @ViewChild('smartTable') + public set smartTable(content: Angular2SmartTableComponent) { + if (content) { + this._smartTable = content; + } + } + public get smartTable(): Angular2SmartTableComponent { + return this._smartTable; + } + + constructor( + private readonly translateService: TranslateService, + private readonly timeTrackerService: TimeTrackerService, + private readonly toastrNotifier: ToastrNotificationService, + private readonly languageElectronService: LanguageElectronService, + private readonly electronService: ElectronService, + private readonly taskSelectorService: TaskSelectorService, + private readonly taskTableStore: TaskTableStore, + private readonly httpClient: HttpClient, + private readonly teamSelectorService: TeamSelectorService, + private readonly projectSelectorService: ProjectSelectorService, + private readonly actionButtonStore: ActionButtonStore, + private readonly searchTermQuery: SearchTermQuery, + private readonly taskCacheService: TaskCacheService, + private readonly store: Store + ) {} + ngOnInit(): void { + combineLatest([ + this.teamSelectorService.selected$, + this.projectSelectorService.selected$, + this.searchTermQuery.value$ + ]) + .pipe( + tap(() => this.setSmartTableSource()), + untilDestroyed(this) + ) + .subscribe(); + this.loadSmartTableSettings(); + this.setSmartTableSource(); + } + + ngAfterViewInit(): void { + this.languageElectronService.onLanguageChange(() => { + this.loadSmartTableSettings(); + }); + this.onChangedSource(); + } + + public refreshTimer(): void { + this.electronService.ipcRenderer.send('refresh-timer'); + } + + public handleRowSelection(selectionEvent): void { + if (this.isNoRowSelected(selectionEvent)) { + this.clearSelectedTaskAndRefresh(); + } else { + const selectedRow = selectionEvent.data; + this.handleSelectedTaskChange(selectedRow.id); + } + } + + private handleSelectedTaskChange(selectedTaskId: string): void { + if (this.isDifferentTask(selectedTaskId)) { + this.taskSelectorService.selected = selectedTaskId; + } + } + + private isDifferentTask(selectedTaskId: string): boolean { + return this.taskSelectorService.selectedId !== selectedTaskId; + } + + private clearSelectedTaskAndRefresh(): void { + this.taskSelectorService.selected = null; + } + + private isNoRowSelected({ isSelected }): boolean { + this.actionButtonStore.update({ toggle: isSelected }); + return !isSelected; + } + + private loadSmartTableSettings(): void { + this.smartTableSettings = { + columns: { + title: { + title: this.translateService.instant('TIMER_TRACKER.TASK'), + type: 'custom', + renderComponent: TaskRenderCellComponent, + width: '40%', + componentInitFunction: (instance: TaskRenderCellComponent, cell: Cell) => { + instance.rowData = cell.getRow().getData(); + } + }, + duration: { + title: this.translateService.instant('TIMESHEET.DURATION'), + type: 'custom', + renderComponent: TaskDurationComponent, + componentInitFunction: (instance: TaskDurationComponent, cell: Cell) => { + instance.rowData = cell.getRow().getData(); + } + }, + taskProgress: { + title: this.translateService.instant('MENU.IMPORT_EXPORT.PROGRESS'), + type: 'custom', + renderComponent: TaskProgressComponent, + width: '192px', + componentInitFunction: (instance: TaskProgressComponent, cell: Cell) => { + instance.rowData = cell.getRow().getData(); + instance.updated.subscribe({ + next: async (estimate: number) => { + const { tenantId, organizationId } = this.store; + const id = instance.task.id; + const title = instance.task.title; + const status = instance.task.status; + const taskUpdateInput: ITaskUpdateInput = { + organizationId, + tenantId, + estimate, + status, + title + }; + await this.timeTrackerService.updateTask(id, taskUpdateInput); + this.toastrNotifier.success(this.translateService.instant('TOASTR.MESSAGE.UPDATED')); + this.refreshTimer(); + }, + error: (err: any) => { + console.warn(err); + } + }); + } + }, + taskStatus: { + title: this.translateService.instant('SM_TABLE.STATUS'), + type: 'custom', + renderComponent: TaskStatusComponent, + componentInitFunction: (instance: TaskStatusComponent, cell: Cell) => { + instance.rowData = cell.getRow().getData(); + instance.updated.subscribe({ + next: async (taskStatus: ITaskStatus) => { + const { tenantId, organizationId } = this.store; + const id = instance.task.id; + const title = instance.task.title; + const status = taskStatus.name as TaskStatusEnum; + const taskUpdateInput: ITaskUpdateInput = { + organizationId, + tenantId, + status, + title, + taskStatus + }; + await this.timeTrackerService.updateTask(id, taskUpdateInput); + this.toastrNotifier.success(this.translateService.instant('TOASTR.MESSAGE.UPDATED')); + this.refreshTimer(); + }, + error: (err: any) => { + console.warn(err); + } + }); + } + } + }, + hideSubHeader: true, + actions: false, + noDataMessage: this.translateService.instant('SM_TABLE.NO_DATA.TASK'), + pager: { + display: false, + perPage: 10 + } + }; + } + + private setSmartTableSource(): void { + const { tenantId, organizationId, user } = this.store; + const employeeId = user?.employee?.id; + + // Validate essential IDs + if (!tenantId || !organizationId || !employeeId) { + console.error('Missing essential data: tenantId, organizationId, or employeeId'); + return; + } + + // Prepare request parameters for filtering + const requestFilters = { + tenantId, + organizationId, + ...(this.projectSelectorService.selectedId && { projectId: this.projectSelectorService.selectedId }), + ...(this.teamSelectorService.selectedId && { teams: [this.teamSelectorService.selectedId] }), + members: { id: employeeId }, + ...(this.searchTermQuery.value && { + title: this.searchTermQuery.value, + prefix: this.searchTermQuery.value + }) + }; + + // Initialize the smart table data source + const source = new CachedServerDataSource( + this.httpClient, + { + endPoint: `${API_PREFIX}/tasks/pagination`, + relations: [ + 'members', + 'members.user', + 'project', + 'tags', + 'teams', + 'teams.members', + 'teams.members.employee', + 'teams.members.employee.user', + 'creator', + 'organizationSprint', + 'taskStatus', + 'taskSize', + 'taskPriority' + ], + join: { + alias: 'task', + leftJoin: { + members: 'task.members', + user: 'members.user' + } + }, + order: { updatedAt: 'DESC' }, + where: requestFilters + }, + this.taskCacheService + ); + + // Register operator to handle task responses + source.registerOperatorFunction(concatMap((res: ITask[]) => this.mergeTaskWithStatistics(res))); + + // Assign source to the component property + this.smartTableSource = source; + } + + private async mergeTaskWithStatistics(tasks: ITask[]): Promise { + // Early exit if no tasks + if (!tasks?.length) { + this.taskTableStore.update({ data: [] }); + return []; + } + + const { organizationId, tenantId, user } = this.store; + const employeeId = user?.employee?.id; + const request = { + organizationId, + employeeId, + tenantId, + projectId: this.projectSelectorService.selectedId, + organizationTeamId: this.teamSelectorService.selectedId + }; + + try { + const taskIds = tasks.map((task) => task.id); + const statistics = await this.timeTrackerService.getTasksStatistics({ ...request, taskIds }); + const mergedItems = this.taskSelectorService.merge(tasks, statistics); + this.taskTableStore.update({ data: mergedItems }); + return mergedItems; + } catch (error) { + console.error('Error fetching task statistics:', error); + this.taskTableStore.update({ data: tasks }); + return tasks; // Fallback to original tasks in case of error + } + } + + private onChangedSource(): void { + this.smartTable.source + .onChanged() + .pipe( + tap(() => { + this.clearItem(); + this.handleRowAutoSelection(); + }), + untilDestroyed(this) + ) + .subscribe(); + } + + private handleRowAutoSelection(): void { + const { selectedId } = this.taskSelectorService; + if (!selectedId) return; + const row = this.findRowById(selectedId); + if (row) { + this.smartTable.grid.selectRow(row); + } + } + + private findRowById(id: string): any { + return this.smartTable.grid.getRows().find((row) => row.getData().id === id); + } + + private clearItem(): void { + if (this.smartTable && this.smartTable.grid) { + this.smartTable.grid.dataSet['willSelect'] = 'indexed'; + this.smartTable.grid.dataSet.deselectAll(); + } + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/task-table.module.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/task-table.module.ts new file mode 100644 index 00000000000..0a7bbc0e7fa --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/task-table.module.ts @@ -0,0 +1,58 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { + NbButtonModule, + NbCardModule, + NbFormFieldModule, + NbIconModule, + NbInputModule, + NbTooltipModule +} from '@nebular/theme'; +import { Angular2SmartTableModule } from 'angular2-smart-table'; +import { LanguageModule } from '../../language/language.module'; +import { TaskCacheService } from '../../services'; +import { ProjectSelectorModule } from '../../shared/features/project-selector/project-selector.module'; +import { TaskSelectorModule } from '../../shared/features/task-selector/task-selector.module'; +import { TeamSelectorModule } from '../../shared/features/team-selector/team-selector.module'; +import { NoDataMessageModule } from '../no-data-message/no-data-message.module'; +import { PaginationModule } from '../pagination/pagination.module'; +import { TaskTableQuery } from './+state/task-table.query'; +import { TaskTableStore } from './+state/task-table.store'; +import { ActionButtonQuery } from './action-button/+state/action-button.query'; +import { ActionButtonStore } from './action-button/+state/action-button.store'; +import { ActionButtonComponent } from './action-button/action-button.component'; +import { SearchTermQuery } from './search/+state/search-term.query'; +import { SearchTermStore } from './search/+state/search-term.store'; +import { SearchComponent } from './search/search.component'; +import { TaskTableComponent } from './table/task-table.component'; + +@NgModule({ + declarations: [TaskTableComponent, SearchComponent, ActionButtonComponent], + exports: [TaskTableComponent], + imports: [ + CommonModule, + PaginationModule, + NbButtonModule, + NbTooltipModule, + NbIconModule, + NbCardModule, + NbFormFieldModule, + NoDataMessageModule, + NbInputModule, + TaskSelectorModule, + ProjectSelectorModule, + TeamSelectorModule, + LanguageModule.forChild(), + Angular2SmartTableModule + ], + providers: [ + ActionButtonStore, + SearchTermStore, + TaskTableStore, + ActionButtonQuery, + SearchTermQuery, + TaskTableQuery, + TaskCacheService + ] +}) +export class TaskTableModule {} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/task-table/task-table.routing.module.ts b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/task-table.routing.module.ts new file mode 100644 index 00000000000..53f6e130653 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/time-tracker/task-table/task-table.routing.module.ts @@ -0,0 +1,17 @@ +import { Routes } from '@angular/router'; + +export const taskTableRoutes: Routes = [ + { + path: '', + pathMatch: 'full', + redirectTo: 'table' + }, + { + path: 'table', + loadComponent: () => import('./table/task-table.component').then((m) => m.TaskTableComponent) + }, + { + path: '**', + redirectTo: 'table' + } +]; diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html index eb139a46457..8b0ea407d94 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.html @@ -136,74 +136,8 @@ -
- - -
- - -
- -
-
- -
-
-
-
- - -
-
- -
-
- -
- - -
-
-
-
-
-
-
- - - - - - - - - -
+
+
diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.scss b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.scss index 7e5eebcb0a2..9849c3ab14d 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.scss +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.scss @@ -720,3 +720,24 @@ img { margin: 0; } } + +.content-active { + background-color: var(--gauzy-card-2); + border-radius: var(--border-radius); + padding: 1rem; + width: 900px; +} + +::ng-deep nb-route-tabset .route-tab .tab-link { + border-radius: var(--border-radius) var(--border-radius) 0 0; + svg { + fill: var(--text-primary-color); + } + span { + display: inline-block; + text-transform: initial; + &:first-letter { + text-transform: uppercase; + } + } +} diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts index 99bfde90faa..3cc1efdc863 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts @@ -15,8 +15,6 @@ import { IOrganization, IOrganizationTeam, ITask, - ITasksStatistics, - ITaskStatus, ITaskUpdateInput, ITimeLog, ITimeSlotTimeLogs, @@ -24,10 +22,9 @@ import { TaskStatusEnum } from '@gauzy/contracts'; import { compressImage, distinctUntilChange } from '@gauzy/ui-core/common'; -import { NbDialogRef, NbDialogService, NbIconLibraries, NbToastrService } from '@nebular/theme'; +import { NbDialogRef, NbDialogService, NbIconLibraries, NbRouteTab, NbToastrService } from '@nebular/theme'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { TranslateService } from '@ngx-translate/core'; -import { Angular2SmartTableComponent, Cell, LocalDataSource } from 'angular2-smart-table'; import * as moment from 'moment'; import 'moment-duration-format'; import { @@ -75,12 +72,8 @@ import { ProjectSelectorService } from '../shared/features/project-selector/+sta import { TaskSelectorService } from '../shared/features/task-selector/+state/task-selector.service'; import { TeamSelectorService } from '../shared/features/team-selector/+state/team-selector.service'; import { hasAllPermissions } from '../shared/utils/permission.util'; -import { TasksComponent } from '../tasks/tasks.component'; import { TimeTrackerQuery } from './+state/time-tracker.query'; import { IgnitionState, TimeTrackerStore } from './+state/time-tracker.store'; -import { TaskDurationComponent, TaskProgressComponent } from './task-render'; -import { TaskRenderCellComponent } from './task-render/task-render-cell/task-render-cell.component'; -import { TaskStatusComponent } from './task-render/task-status/task-status.component'; import { IRemoteTimer } from './time-tracker-status/interfaces'; import { TimeTrackerStatusService } from './time-tracker-status/time-tracker-status.service'; import { TimeTrackerService } from './time-tracker.service'; @@ -165,7 +158,6 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { smartTableSettings: object; tableData = []; isTrackingEnabled = true; - isAddTask = false; sound: any = null; constructor( @@ -238,14 +230,6 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { .subscribe(); } - private get _sourceData(): LocalDataSource { - return this._sourceData$.getValue(); - } - - private get _hasTaskPermission(): boolean { - return this.taskSelectorService.hasPermission; - } - private get _isOffline(): boolean { return this._isOffline$.getValue(); } @@ -266,27 +250,12 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { return this.taskSelectorService.selected; } - private _taskTable: Angular2SmartTableComponent; - - @ViewChild('taskTable') set taskTable(content: Angular2SmartTableComponent) { - if (content) { - this._taskTable = content; - this._onChangedSource(); - } - } - private _timeRun$: BehaviorSubject = new BehaviorSubject('00:00:00'); public get timeRun$(): Observable { return this._timeRun$.asObservable(); } - private _sourceData$: BehaviorSubject; - - public get sourceData$(): Observable { - return this._sourceData$.asObservable(); - } - private _isOffline$: BehaviorSubject = new BehaviorSubject(false); public get isOffline$(): Observable { @@ -367,29 +336,6 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { } } - private merge(tasks: ITask[], statistics: ITasksStatistics[]): (ITask & ITasksStatistics)[] { - let arr: (ITask & ITasksStatistics)[] = []; - arr = arr.concat(statistics, tasks); - return arr.reduce((result, current) => { - const existing = result.find((item: any) => item.id === current.id); - if (existing) { - const updatedAtMoment = moment(existing?.updatedAt, moment.ISO_8601).utc(true); - Object.assign( - existing, - current, - updatedAtMoment.isAfter(current?.updatedAt) - ? { - updatedAt: updatedAtMoment.toISOString() - } - : {} - ); - } else { - result.push(current); - } - return result.filter((task) => !!task?.id); - }, []); - } - private countDuration(count, isForcedSync?: boolean): void { if (!this.start || isForcedSync) { this._lastTotalWorkedToday$.next(count.todayDuration); @@ -466,31 +412,6 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { return isPassed; } - private _onChangedSource(): void { - this._taskTable.source.onChangedSource - .pipe( - tap(() => this._clearItem()), - tap(() => { - if (this.selectedTask) { - this._taskTable.grid.dataSet.getRows().map((row) => { - if (row.getData().id === this.taskSelectorService.selectedId) { - return this._taskTable.grid.dataSet.selectRow(row); - } - }); - } - }), - untilDestroyed(this) - ) - .subscribe(); - } - - private _clearItem(): void { - if (this._taskTable && this._taskTable.grid) { - this._taskTable.grid.dataSet['willSelect'] = 'indexed'; - this._taskTable.grid.dataSet.deselectAll(); - } - } - private async _mappingScreenshots(args: any[]): Promise { try { let screenshots = await Promise.all( @@ -611,97 +532,6 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { } } - private _loadSmartTableSettings(): void { - this.smartTableSettings = { - columns: { - title: { - title: this._translateService.instant('TIMER_TRACKER.TASK'), - type: 'custom', - renderComponent: TaskRenderCellComponent, - width: '40%', - componentInitFunction: (instance: TaskRenderCellComponent, cell: Cell) => { - instance.rowData = cell.getRow().getData(); - } - }, - duration: { - title: this._translateService.instant('TIMESHEET.DURATION'), - type: 'custom', - renderComponent: TaskDurationComponent, - componentInitFunction: (instance: TaskDurationComponent, cell: Cell) => { - instance.rowData = cell.getRow().getData(); - } - }, - taskProgress: { - title: this._translateService.instant('MENU.IMPORT_EXPORT.PROGRESS'), - type: 'custom', - renderComponent: TaskProgressComponent, - width: '192px', - componentInitFunction: (instance: TaskProgressComponent, cell: Cell) => { - instance.rowData = cell.getRow().getData(); - instance.updated.subscribe({ - next: async (estimate: number) => { - const { tenantId, organizationId } = this._store; - const id = instance.task.id; - const title = instance.task.title; - const status = instance.task.status; - const taskUpdateInput: ITaskUpdateInput = { - organizationId, - tenantId, - estimate, - status, - title - }; - await this.timeTrackerService.updateTask(id, taskUpdateInput); - this._toastrNotifier.success(this._translateService.instant('TOASTR.MESSAGE.UPDATED')); - this.refreshTimer(); - }, - error: (err: any) => { - console.warn(err); - } - }); - } - }, - taskStatus: { - title: this._translateService.instant('SM_TABLE.STATUS'), - type: 'custom', - renderComponent: TaskStatusComponent, - componentInitFunction: (instance: TaskStatusComponent, cell: Cell) => { - instance.rowData = cell.getRow().getData(); - instance.updated.subscribe({ - next: async (taskStatus: ITaskStatus) => { - const { tenantId, organizationId } = this._store; - const id = instance.task.id; - const title = instance.task.title; - const status = taskStatus.name as TaskStatusEnum; - const taskUpdateInput: ITaskUpdateInput = { - organizationId, - tenantId, - status, - title, - taskStatus - }; - await this.timeTrackerService.updateTask(id, taskUpdateInput); - this._toastrNotifier.success(this._translateService.instant('TOASTR.MESSAGE.UPDATED')); - this.refreshTimer(); - }, - error: (err: any) => { - console.warn(err); - } - }); - } - } - }, - hideSubHeader: true, - actions: false, - noDataMessage: this._translateService.instant('SM_TABLE.NO_DATA.TASK'), - pager: { - display: true, - perPage: 10, - page: 1 - } - }; - } - private async loadStatuses(): Promise { if (!this._store.organizationId && !this._store.tenantId) { return; @@ -721,21 +551,6 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { } ngOnInit(): void { - this._sourceData$ = new BehaviorSubject(new LocalDataSource(this.tableData)); - - this._sourceData.setSort([{ field: 'updatedAt', direction: 'desc' }]); - - this.taskSelectorService - .getAll$() - .pipe( - tap(async (tasks) => { - this.tableData = tasks; - await this._sourceData.load(this.tableData); - }), - untilDestroyed(this) - ) - .subscribe(); - this._lastTotalWorkedToday$ .pipe( tap((todayDuration: number) => { @@ -927,8 +742,6 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { untilDestroyed(this) ) .subscribe(); - - this._loadSmartTableSettings(); } public xor(a: boolean, b: boolean): boolean { @@ -1506,7 +1319,7 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { }) ); - this._languageElectronService.initialize(asyncScheduler.schedule(() => this._loadSmartTableSettings(), 150)); + this._languageElectronService.initialize(); this.electronService.ipcRenderer.on('sleep_remote_lock', (event, state: boolean) => { this._ngZone.run(async () => { @@ -1771,8 +1584,6 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { public descriptionChange(e): void { if (e) this.errors.note = false; - this.clearSelectedTaskAndRefresh(); - this._clearItem(); this.electronService.ipcRenderer.send('update_project_on', { note: this.noteService.note }); @@ -2003,54 +1814,6 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { this.electronService.ipcRenderer.send('expand', !this.isExpand); } - public handleRowSelection(selectionEvent): void { - if (this.isNoRowSelected(selectionEvent)) { - this.clearSelectedTaskAndRefresh(); - } else { - const selectedRow = selectionEvent.data; - this.handleSelectedTaskChange(selectedRow.id); - } - } - - private isNoRowSelected({ isSelected }): boolean { - return !isSelected; - } - - private clearSelectedTaskAndRefresh(): void { - this.taskSelectorService.selected = null; - } - - private handleSelectedTaskChange(selectedTaskId): void { - if (this.isDifferentTask(selectedTaskId)) { - this.taskSelectorService.selected = selectedTaskId; - } - } - - private isDifferentTask(selectedTaskId): boolean { - return this.taskSelectorService.selectedId !== selectedTaskId; - } - - public onSearch(query: string = ''): void { - if (query) { - this._sourceData.setFilter( - [ - { - field: 'title', - search: query - }, - { - field: 'taskNumber', - search: query - } - ], - false - ); - } else { - this._sourceData.reset(); - this._sourceData.refresh(); - } - } - public async getScreenshot(arg, isThumb: boolean | null = false): Promise { try { let thumbSize = this.determineScreenshot(arg.screenSize); @@ -2347,52 +2110,6 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { } } - public addTask(): void { - this.isAddTask = !this._isOffline && this._hasTaskPermission; - if (!this.isAddTask) { - return; - } - this.dialogService - .open(TasksComponent, { - context: { - employee: this.userData, - hasProjectPermission: this.projectSelectorService.hasPermission, - selected: { - teamId: this.teamSelectorService.selectedId, - projectId: this.projectSelectorService.selectedId, - contactId: this.clientSelectorService.selectedId - }, - userData: this.argFromMain - }, - backdropClass: 'backdrop-blur' - }) - .onClose.pipe( - tap(() => this.closeAddTask()), - filter((result) => !!result), - tap((result) => this.callbackNewTask(result)), - untilDestroyed(this) - ) - .subscribe(); - } - - public closeAddTask(): void { - this.isAddTask = false; - this.electronService.ipcRenderer.send('refresh-timer'); - } - - public callbackNewTask(e): void { - if (e.isSuccess) { - this.toastrService.show(e.message, `Success`, { - status: 'success' - }); - this.electronService.ipcRenderer.send('refresh-timer'); - } else { - this.toastrService.show(e.message, `Warning`, { - status: 'danger' - }); - } - } - public showErrorMessage(msg): void { this.toastrService.show(`${msg}`, `Warning`, { status: 'danger' @@ -2585,4 +2302,29 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { // Return the result of the callback (or void if no callback was provided) return { current, previous, result, params }; } + + public tabs$ = new BehaviorSubject([ + { + title: this._translateService.instant('MENU.TASKS'), + route: ['/', 'time-tracker', 'tasks'], + activeLinkOptions: { exact: false }, + disabled: this._isOffline + }, + { + title: this._translateService.instant('TIMER_TRACKER.MENU.DAILY_RECAP'), + route: ['/', 'time-tracker', 'daily'], + activeLinkOptions: { exact: false }, + disabled: this._isOffline + }, + { + title: this._translateService.instant('TIMER_TRACKER.MENU.WEEKLY_RECAP'), + route: ['/', 'time-tracker', 'weekly'], + disabled: this._isOffline + }, + { + title: this._translateService.instant('TIMER_TRACKER.MENU.MONTHLY_RECAP'), + route: ['/', 'time-tracker', 'monthly'], + disabled: this._isOffline + } + ]); } diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.module.ts b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.module.ts index bf9f77a5282..bcd270d7ae4 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.module.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.module.ts @@ -13,6 +13,7 @@ import { NbIconModule, NbInputModule, NbLayoutModule, + NbRouteTabsetModule, NbSelectModule, NbSidebarModule, NbSidebarService, @@ -31,7 +32,6 @@ import { ImageViewerModule } from '../image-viewer/image-viewer.module'; import { ActivityWatchModule } from '../integrations'; import { LanguageModule } from '../language/language.module'; import { TimeSlotQueueService } from '../offline-sync'; -import { RecapModule } from '../recap/recap.module'; import { ErrorHandlerService, NativeNotificationService, Store, ToastrNotificationService } from '../services'; import { SelectModule } from '../shared/components/ui/select/select.module'; import { TimeTrackerFormModule } from '../shared/features/time-tracker-form/time-tracker-form.module'; @@ -88,10 +88,10 @@ import { TimerTrackerChangeDialogComponent } from './timer-tracker-change-dialog NoDataMessageModule, PipeModule, NbTabsetModule, - RecapModule, TimeTrackerFormModule, SelectModule, - DesktopDirectiveModule + DesktopDirectiveModule, + NbRouteTabsetModule ], providers: [ NbSidebarService, diff --git a/packages/desktop-ui-lib/src/lib/utils/smart-table/cached-server.data-source.ts b/packages/desktop-ui-lib/src/lib/utils/smart-table/cached-server.data-source.ts new file mode 100644 index 00000000000..64895f85f54 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/utils/smart-table/cached-server.data-source.ts @@ -0,0 +1,195 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { isNotEmpty, toParams } from '@gauzy/ui-core/common'; +import { IFilterConfig, LocalDataSource } from 'angular2-smart-table'; +import { firstValueFrom, Observable, shareReplay } from 'rxjs'; +import { catchError, map, tap } from 'rxjs/operators'; +import { AbstractCacheService } from '../../services/abstract-cache.service'; +import { ServerSourceConf } from './server-source.conf'; + +export class CachedServerDataSource extends LocalDataSource { + protected conf: ServerSourceConf; + protected lastRequestCount: number = 0; + protected operatorFunctions: any[] = []; + + constructor( + protected http: HttpClient, + conf: ServerSourceConf | {} = {}, + readonly cacheService?: AbstractCacheService + ) { + super(); + this.conf = new ServerSourceConf(conf); + if (!this.conf.endPoint) { + throw new Error('At least endPoint must be specified as a configuration of the server data source.'); + } + } + + override count(): number { + return this.lastRequestCount; + } + + getData(): any[] { + return this.data; + } + + override getElements(): Promise { + return firstValueFrom( + this.operatorFunctions + .reduce( + (obs, operatorFn) => obs.pipe(operatorFn), + this.requestElements().pipe( + map((res) => { + this.lastRequestCount = this.extractTotalFromResponse(res); + this.data = this.extractDataFromResponse(res); + return this.data; + }) + ) + ) + .pipe( + tap(() => this.conf.finalize?.()), + catchError((error) => { + this.conf.finalize?.(); + throw new Error(error); + }) + ) + ); + } + + /** + * Extracts array of data from server response + * @param res + * @returns {any} + */ + protected extractDataFromResponse(res: any): Array { + const rawData = res.body; + let data = !!this.conf.dataKey ? rawData[this.conf.dataKey] : rawData; + try { + if (data instanceof Array) { + return this.conf.resultMap ? data.map(this.conf.resultMap).filter(Boolean) : data; + } + throw new Error( + `Data must be an array. Please check that data extracted from the server response by the key '${this.conf.dataKey}' exists and is array.` + ); + } catch (error) { + console.log(`Failed to extract data from response: ${error}`); + return data; + } + } + + /** + * Extracts total rows count from the server response + * Looks for the count in the headers first, then in the response body + * @param res + * @returns {any} + */ + protected extractTotalFromResponse(res: any): number { + if (res.headers.has(this.conf.totalKey)) { + return +res.headers.get(this.conf.totalKey); + } else { + const rawData = res.body; + return rawData[this.conf.totalKey] || 0; + } + } + + protected requestElements(): Observable { + const httpParams = this.createRequestParams(); + return this.cacheService + ? this.cachedRequestElements(httpParams) + : this.http.get(this.conf.endPoint, { params: httpParams, observe: 'response' }); + } + + protected cachedRequestElements(params: HttpParams): Observable { + let elements$ = this.cacheService.getValue(params); + + if (!elements$) { + // Fetch elements + elements$ = this.http.get(this.conf.endPoint, { params, observe: 'response' }).pipe( + map((httpResponse) => httpResponse), + shareReplay(1) + ); + // Set elements in the cache + this.cacheService.setValue(elements$, params); + } + // Return elements + return elements$; + } + + protected createRequestParams(): HttpParams { + const requestParams = { + ...(this.conf.where ? { where: this.conf.where } : {}), + ...(this.conf.join ? { join: this.conf.join } : {}), + ...(this.conf.relations ? { relations: this.conf.relations } : {}), + ...(this.conf.withDeleted ? { withDeleted: this.conf.withDeleted } : {}), + ...(isNotEmpty(this.conf.select) ? { select: this.conf.select } : {}), + ...this.addSortRequestParams(), + ...this.addFilterRequestParams(), + ...this.addPagerRequestParams() + }; + return toParams(requestParams); + } + + protected addSortRequestParams() { + if (this.sortConf) { + const orders: any = {}; + this.sortConf.forEach((fieldConf) => { + orders[fieldConf.field] = fieldConf.direction.toUpperCase(); + }); + return { + [this.conf.sortDirKey]: orders + }; + } else { + return {}; + } + } + + /** + * Add additional smart datatables filters to the request parameters. + * + * @returns {Object} The constructed filter object for request parameters. + */ + protected addFilterRequestParams(): any { + // Check if filter configuration is defined + if (!this.filterConf) { + // If not defined, return an empty object + return {}; + } + + // Initialize an object to store filter values + const filters: any = {}; + + // Iterate over each filter configuration + this.filterConf.forEach(({ field, search }: IFilterConfig) => { + // Check if search value is truthy, and add it to filters + if (search) { + filters[field] = search; + } + }); + + // Construct and return the final filter object with the specified key + return { + [this.conf.filterFieldKey]: filters + }; + } + + protected addPagerRequestParams() { + try { + if (this.pagingConf) { + if (typeof this.pagingConf['page'] === 'number' && typeof this.pagingConf['perPage'] === 'number') { + return { + [this.conf.pagerPageKey]: this.pagingConf['page'], + [this.conf.pagerLimitKey]: this.pagingConf['perPage'] + }; + } + return {}; + } else { + return {}; + } + } catch (error) { + console.log('Error while retrieving pagination configuration', error); + return {}; + } + } + + public registerOperatorFunction(operatorFunction: any) { + this.operatorFunctions.push(operatorFunction); + } +} diff --git a/packages/desktop-ui-lib/src/lib/utils/smart-table/index.ts b/packages/desktop-ui-lib/src/lib/utils/smart-table/index.ts new file mode 100644 index 00000000000..41a2fd4359e --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/utils/smart-table/index.ts @@ -0,0 +1,2 @@ +export * from './cached-server.data-source'; +export * from './server-source.conf'; diff --git a/packages/desktop-ui-lib/src/lib/utils/smart-table/server-source.conf.ts b/packages/desktop-ui-lib/src/lib/utils/smart-table/server-source.conf.ts new file mode 100644 index 00000000000..43dc4207691 --- /dev/null +++ b/packages/desktop-ui-lib/src/lib/utils/smart-table/server-source.conf.ts @@ -0,0 +1,63 @@ +import { IOptionsSelect } from '@gauzy/contracts'; + +export class ServerSourceConf { + protected static readonly SORT_FIELD_KEY = 'orderBy'; + protected static readonly SORT_DIR_KEY = 'order'; + protected static readonly PAGER_PAGE_KEY = 'skip'; + protected static readonly PAGER_LIMIT_KEY = 'take'; + protected static readonly FILTER_FIELD_KEY = 'filters'; + protected static readonly TOTAL_KEY = 'total'; + protected static readonly DATA_KEY = 'items'; + + endPoint: string; + + sortFieldKey: string; + sortDirKey: string; + pagerPageKey: string; + pagerLimitKey: string; + filterFieldKey: string; + totalKey: string; + dataKey: string; + where: any; + join: any; + relations: string[]; + resultMap: any; + finalize: any; + withDeleted: boolean; + select: IOptionsSelect; + + constructor({ + resultMap = null, + finalize = null, + endPoint = '', + sortFieldKey = '', + sortDirKey = '', + pagerPageKey = '', + pagerLimitKey = '', + filterFieldKey = '', + totalKey = '', + dataKey = '', + where = '', + join = '', + relations = [], + withDeleted = false, + select = {} + } = {}) { + this.endPoint = endPoint ? endPoint : ''; + + this.sortFieldKey = sortFieldKey ? sortFieldKey : ServerSourceConf.SORT_FIELD_KEY; + this.sortDirKey = sortDirKey ? sortDirKey : ServerSourceConf.SORT_DIR_KEY; + this.pagerPageKey = pagerPageKey ? pagerPageKey : ServerSourceConf.PAGER_PAGE_KEY; + this.pagerLimitKey = pagerLimitKey ? pagerLimitKey : ServerSourceConf.PAGER_LIMIT_KEY; + this.filterFieldKey = filterFieldKey ? filterFieldKey : ServerSourceConf.FILTER_FIELD_KEY; + this.totalKey = totalKey ? totalKey : ServerSourceConf.TOTAL_KEY; + this.dataKey = dataKey ? dataKey : ServerSourceConf.DATA_KEY; + this.where = where; + this.join = join; + this.relations = relations; + this.resultMap = resultMap; + this.finalize = finalize; + this.withDeleted = withDeleted; + this.select = select; + } +}