diff --git a/.scripts/configure.ts b/.scripts/configure.ts index ddc676d7deb..d9df0da12b5 100644 --- a/.scripts/configure.ts +++ b/.scripts/configure.ts @@ -78,6 +78,7 @@ if (!isDocker) { API_BASE_URL: API_BASE_URL, CLIENT_BASE_URL: CLIENT_BASE_URL, + COOKIE_DOMAIN: '${env.COOKIE_DOMAIN}', PLATFORM_WEBSITE_URL: '${env.PLATFORM_WEBSITE_URL}', PLATFORM_WEBSITE_DOWNLOAD_URL: '${env.PLATFORM_WEBSITE_DOWNLOAD_URL}', @@ -221,6 +222,7 @@ if (!isDocker) { API_BASE_URL: API_BASE_URL, CLIENT_BASE_URL: CLIENT_BASE_URL, + COOKIE_DOMAIN: 'DOCKER_COOKIE_DOMAIN', PLATFORM_WEBSITE_URL: 'DOCKER_PLATFORM_WEBSITE_URL', PLATFORM_WEBSITE_DOWNLOAD_URL: 'DOCKER_PLATFORM_WEBSITE_DOWNLOAD_URL', diff --git a/.scripts/env.ts b/.scripts/env.ts index 4303ff0a472..d010c380ec7 100644 --- a/.scripts/env.ts +++ b/.scripts/env.ts @@ -10,6 +10,8 @@ export type Env = Readonly<{ // Set to true if build / runs in Docker IS_DOCKER: boolean; + COOKIE_DOMAIN: string; + // Base URL of Gauzy UI website CLIENT_BASE_URL: string; @@ -140,6 +142,8 @@ export const env: Env = cleanEnv( IS_DOCKER: bool({ default: false }), + COOKIE_DOMAIN: str({ default: '.gauzy.co' }), + CLIENT_BASE_URL: str({ default: 'http://localhost:4200' }), API_BASE_URL: str({ default: 'http://localhost:3000' }), diff --git a/apps/desktop-timer/src/assets/styles/gauzy/theme.gauzy-dark.ts b/apps/desktop-timer/src/assets/styles/gauzy/theme.gauzy-dark.ts index 4ec88895401..9bf0e771154 100644 --- a/apps/desktop-timer/src/assets/styles/gauzy/theme.gauzy-dark.ts +++ b/apps/desktop-timer/src/assets/styles/gauzy/theme.gauzy-dark.ts @@ -45,18 +45,12 @@ const theme = { export const GAUZY_DARK = { name: 'gauzy-dark', - base: 'dark', + base: 'dark', variables: { ...theme, temperature: { - arcFill: [ - theme.primary, - theme.primary, - theme.primary, - theme.primary, - theme.primary - ], + arcFill: Array(5).fill(theme.primary), arcEmpty: theme.bg2, thumbBg: theme.bg2, thumbBorder: theme.primary diff --git a/apps/desktop-timer/src/assets/styles/material/theme.material-light.ts b/apps/desktop-timer/src/assets/styles/material/theme.material-light.ts index 41fd9732d98..bb2e166e089 100644 --- a/apps/desktop-timer/src/assets/styles/material/theme.material-light.ts +++ b/apps/desktop-timer/src/assets/styles/material/theme.material-light.ts @@ -1,4 +1,4 @@ -import { NbJSThemeOptions } from '@nebular/theme'; +import { NbJSThemeOptions, NbJSThemeVariable } from '@nebular/theme'; const palette = { primary: '#6200ee', @@ -47,20 +47,14 @@ export const baseTheme: NbJSThemeOptions = { } }; -const baseThemeVariables = baseTheme.variables; +const baseThemeVariables = baseTheme.variables as NbJSThemeVariable; export const MATERIAL_LIGHT_THEME = { name: 'material-light', base: 'default', variables: { temperature: { - arcFill: [ - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary - ], + arcFill: Array(5).fill(baseThemeVariables.primary), arcEmpty: baseThemeVariables.bg2, thumbBg: baseThemeVariables.bg2, thumbBorder: baseThemeVariables.primary diff --git a/apps/desktop-timer/src/assets/styles/theme.dark.ts b/apps/desktop-timer/src/assets/styles/theme.dark.ts index 983d147d3f4..63e247fa5b4 100644 --- a/apps/desktop-timer/src/assets/styles/theme.dark.ts +++ b/apps/desktop-timer/src/assets/styles/theme.dark.ts @@ -49,13 +49,7 @@ export const DARK_THEME = { ...theme, temperature: { - arcFill: [ - theme.primary, - theme.primary, - theme.primary, - theme.primary, - theme.primary - ], + arcFill: Array(5).fill(theme.primary), arcEmpty: theme.bg2, thumbBg: theme.bg2, thumbBorder: theme.primary diff --git a/apps/desktop-timer/src/preload.ts b/apps/desktop-timer/src/preload.ts index fd1459c8709..d918b204482 100644 --- a/apps/desktop-timer/src/preload.ts +++ b/apps/desktop-timer/src/preload.ts @@ -17,6 +17,10 @@ window.addEventListener('DOMContentLoaded', async () => { titleBar.refreshMenu(); }); + ipcRenderer.on('hide-menu', () => { + titleBar.dispose(); + }) + const overStyle = document.createElement('style'); overStyle.innerHTML = ` .cet-container { diff --git a/apps/desktop/src/assets/styles/gauzy/theme.gauzy-dark.ts b/apps/desktop/src/assets/styles/gauzy/theme.gauzy-dark.ts index 4ec88895401..9bf0e771154 100644 --- a/apps/desktop/src/assets/styles/gauzy/theme.gauzy-dark.ts +++ b/apps/desktop/src/assets/styles/gauzy/theme.gauzy-dark.ts @@ -45,18 +45,12 @@ const theme = { export const GAUZY_DARK = { name: 'gauzy-dark', - base: 'dark', + base: 'dark', variables: { ...theme, temperature: { - arcFill: [ - theme.primary, - theme.primary, - theme.primary, - theme.primary, - theme.primary - ], + arcFill: Array(5).fill(theme.primary), arcEmpty: theme.bg2, thumbBg: theme.bg2, thumbBorder: theme.primary diff --git a/apps/desktop/src/assets/styles/material/theme.material-light.ts b/apps/desktop/src/assets/styles/material/theme.material-light.ts index 41fd9732d98..bb2e166e089 100644 --- a/apps/desktop/src/assets/styles/material/theme.material-light.ts +++ b/apps/desktop/src/assets/styles/material/theme.material-light.ts @@ -1,4 +1,4 @@ -import { NbJSThemeOptions } from '@nebular/theme'; +import { NbJSThemeOptions, NbJSThemeVariable } from '@nebular/theme'; const palette = { primary: '#6200ee', @@ -47,20 +47,14 @@ export const baseTheme: NbJSThemeOptions = { } }; -const baseThemeVariables = baseTheme.variables; +const baseThemeVariables = baseTheme.variables as NbJSThemeVariable; export const MATERIAL_LIGHT_THEME = { name: 'material-light', base: 'default', variables: { temperature: { - arcFill: [ - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary - ], + arcFill: Array(5).fill(baseThemeVariables.primary), arcEmpty: baseThemeVariables.bg2, thumbBg: baseThemeVariables.bg2, thumbBorder: baseThemeVariables.primary diff --git a/apps/desktop/src/assets/styles/theme.dark.ts b/apps/desktop/src/assets/styles/theme.dark.ts index 983d147d3f4..63e247fa5b4 100644 --- a/apps/desktop/src/assets/styles/theme.dark.ts +++ b/apps/desktop/src/assets/styles/theme.dark.ts @@ -49,13 +49,7 @@ export const DARK_THEME = { ...theme, temperature: { - arcFill: [ - theme.primary, - theme.primary, - theme.primary, - theme.primary, - theme.primary - ], + arcFill: Array(5).fill(theme.primary), arcEmpty: theme.bg2, thumbBg: theme.bg2, thumbBorder: theme.primary diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index dbcec63fd7b..96f890c7c53 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -21,6 +21,10 @@ window.addEventListener('DOMContentLoaded', async () => { titleBar.refreshMenu(); }); + ipcRenderer.on('hide-menu', () => { + titleBar.dispose(); + }) + ipcRenderer.on('adjust_view', () => { clearInterval(contentInterval); const headerIcon = '/html/body/div[2]/ga-app/ngx-pages/ngx-one-column-layout/nb-layout/div[1]/div/div/nb-sidebar[1]/div/div/div'; diff --git a/apps/gauzy/package.json b/apps/gauzy/package.json index dea89eb5772..76b6c7f4275 100644 --- a/apps/gauzy/package.json +++ b/apps/gauzy/package.json @@ -88,7 +88,7 @@ "@sentry/types": "^7.101.1", "@sentry/utils": "^7.90.0", "@swimlane/ngx-charts": "^20.1.0", - "angular2-smart-table": "^3.2.0", + "angular2-smart-table": "^3.3.0", "angular2-toaster": "^11.0.1", "bootstrap": "^4.3.1", "brace": "^0.11.1", diff --git a/apps/gauzy/src/app/app.module.guard.ts b/apps/gauzy/src/app/app.module.guard.ts index 915c37794ac..aef68709acf 100644 --- a/apps/gauzy/src/app/app.module.guard.ts +++ b/apps/gauzy/src/app/app.module.guard.ts @@ -1,10 +1,10 @@ import { Injectable } from '@angular/core'; -import { Router, CanActivate, ActivatedRouteSnapshot } from '@angular/router'; +import { Router, ActivatedRouteSnapshot } from '@angular/router'; import { environment } from '@gauzy/ui-config'; import { Store } from '@gauzy/ui-core/core'; @Injectable() -export class AppModuleGuard implements CanActivate { +export class AppModuleGuard { constructor(private readonly router: Router, private readonly store: Store) {} /** diff --git a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-profile.component.html b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-profile.component.html index b6d433df26f..4bb2915e29a 100644 --- a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-profile.component.html +++ b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-profile.component.html @@ -1 +1 @@ - + diff --git a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-profile.component.ts b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-profile.component.ts index 4b364f4759b..bc67e42da33 100644 --- a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-profile.component.ts +++ b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-profile.component.ts @@ -1,20 +1,21 @@ import { Component, OnDestroy, OnInit, Output, EventEmitter } from '@angular/core'; -import { ActivatedRoute, Params } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { firstValueFrom, Subject } from 'rxjs'; -import { debounceTime, filter, tap } from 'rxjs/operators'; -import { NbRouteTab } from '@nebular/theme'; +import { debounceTime, filter, firstValueFrom, Subject, tap } from 'rxjs'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { IEmployee, IEmployeeUpdateInput, IUserUpdateInput, PermissionsEnum } from '@gauzy/contracts'; -import { TranslationBaseComponent } from '@gauzy/ui-core/i18n'; +import { ID, IEmployee, IEmployeeUpdateInput, IUserUpdateInput, PermissionsEnum } from '@gauzy/contracts'; import { EmployeesService, EmployeeStore, ErrorHandlingService, + PageTabRegistryConfig, + PageTabRegistryService, + PageTabsetRegistryId, Store, ToastrService, UsersService } from '@gauzy/ui-core/core'; +import { TranslationBaseComponent } from '@gauzy/ui-core/i18n'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -24,10 +25,11 @@ import { providers: [EmployeeStore] }) export class EditEmployeeProfileComponent extends TranslationBaseComponent implements OnInit, OnDestroy { - routeParams: Params; + public tabsetId: PageTabsetRegistryId = this._route.snapshot.data.tabsetId; // The identifier for the tabset + public employeeId: ID = this._route.snapshot.params.id; + selectedEmployee: IEmployee; employeeName: string; - tabs: NbRouteTab[] = []; subject$: Subject = new Subject(); @Output() updatedImage = new EventEmitter(); @@ -40,7 +42,8 @@ export class EditEmployeeProfileComponent extends TranslationBaseComponent imple private readonly _toastrService: ToastrService, private readonly _employeeStore: EmployeeStore, private readonly _errorHandlingService: ErrorHandlingService, - private readonly _store: Store + private readonly _store: Store, + private readonly _pageTabRegistryService: PageTabRegistryService ) { super(translateService); } @@ -56,8 +59,7 @@ export class EditEmployeeProfileComponent extends TranslationBaseComponent imple this._route.params .pipe( filter((params) => !!params), - tap((params) => (this.routeParams = params)), - tap(() => this.loadTabs()), + tap(() => this._registerPageTabs()), tap(() => this.subject$.next(true)), untilDestroyed(this) ) @@ -84,73 +86,167 @@ export class EditEmployeeProfileComponent extends TranslationBaseComponent imple this._applyTranslationOnTabs(); } + /** + * Constructs a route URL for a specific tab in the 'edit-employee' view. + * + * This method dynamically generates the route URL based on the employee's ID + * and the tab passed as a parameter. It is used to navigate between + * different sections (tabs) of the employee edit page. + * + * @param {string} tab - The name of the tab for which to generate the route. + * @returns {string} - The complete route URL for the specified tab. + */ getRoute(tab: string): string { - return `/pages/employees/edit/${this.routeParams.id}/${tab}`; + return `/pages/employees/edit/${this.employeeId}/${tab}`; + } + + /** + * Registers custom tabs for the 'employee-edit' page. + * This method defines and registers the various tabs, their icons, routes, and titles. + */ + private _registerPageTabs(): void { + const tabs: PageTabRegistryConfig[] = this._createTabsConfig(); + + // Register each tab using the page tab registry service + tabs.forEach((tab: PageTabRegistryConfig) => this._pageTabRegistryService.registerPageTab(tab)); } - loadTabs() { - this.tabs = [ + /** + * Creates the configuration for the tabs used in the 'employee-edit' page. + * @returns An array of PageTabRegistryConfig objects. + */ + private _createTabsConfig(): PageTabRegistryConfig[] { + return [ { - title: this.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.ACCOUNT'), - icon: 'person-outline', + tabsetId: this.tabsetId, + tabId: 'account', + tabIcon: 'person-outline', + tabsetType: 'route', + tabTitle: (_i18n) => _i18n.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.ACCOUNT'), + order: 0, responsive: true, route: this.getRoute('account') }, { - title: this.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.NETWORKS'), - icon: 'at-outline', + tabsetId: this.tabsetId, + tabId: 'networks', + tabIcon: 'at-outline', + tabsetType: 'route', + tabTitle: (_i18n) => _i18n.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.NETWORKS'), + order: 1, responsive: true, route: this.getRoute('networks') }, { - title: this.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.EMPLOYMENT'), - icon: 'browser-outline', + tabsetId: this.tabsetId, + tabId: 'employment', + tabIcon: 'browser-outline', + tabsetType: 'route', + tabTitle: (_i18n) => _i18n.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.EMPLOYMENT'), + order: 2, responsive: true, route: this.getRoute('employment') }, { - title: this.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.HIRING'), - icon: 'map-outline', + tabsetId: this.tabsetId, + tabId: 'hiring', + tabIcon: 'browser-outline', + tabsetType: 'route', + tabTitle: (_i18n) => _i18n.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.HIRING'), + order: 3, responsive: true, route: this.getRoute('hiring') }, { - title: this.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.LOCATION'), - icon: 'pin-outline', + tabsetId: this.tabsetId, + tabId: 'location', + tabIcon: 'pin-outline', + tabsetType: 'route', + tabTitle: (_i18n) => _i18n.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.LOCATION'), + order: 4, responsive: true, route: this.getRoute('location') }, { - title: this.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.RATES'), - icon: 'pricetags-outline', + tabsetId: this.tabsetId, + tabId: 'rates', + tabIcon: 'pricetags-outline', + tabsetType: 'route', + tabTitle: (_i18n) => _i18n.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.RATES'), + order: 5, responsive: true, route: this.getRoute('rates') }, - ...(this._store.hasAnyPermission(PermissionsEnum.ALL_ORG_VIEW, PermissionsEnum.ORG_PROJECT_VIEW) - ? [ - { - title: this.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.PROJECTS'), - icon: 'book-outline', - responsive: true, - route: this.getRoute('projects') - } - ] - : []), { - title: this.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.CONTACTS'), - icon: 'book-open-outline', + tabsetId: this.tabsetId, + tabId: 'projects', + tabIcon: 'book-open-outline', + tabsetType: 'route', + tabTitle: (_i18n) => _i18n.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.PROJECTS'), + order: 6, + responsive: true, + route: this.getRoute('projects'), + permissions: [PermissionsEnum.ALL_ORG_VIEW, PermissionsEnum.ORG_PROJECT_VIEW] + }, + { + tabsetId: this.tabsetId, + tabId: 'contacts', + tabIcon: 'book-outline', + tabsetType: 'route', + tabTitle: (_i18n) => _i18n.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.CONTACTS'), + order: 7, responsive: true, route: this.getRoute('contacts') }, { - title: this.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.SETTINGS'), - icon: 'settings-outline', + tabsetId: this.tabsetId, + tabId: 'settings', + tabIcon: 'settings-outline', + tabsetType: 'route', + tabTitle: (_i18n) => _i18n.getTranslation('EMPLOYEES_PAGE.EDIT_EMPLOYEE.SETTINGS'), + order: 8, responsive: true, route: this.getRoute('settings') } ]; } + /** + * Retrieves and sets the profile of the selected employee + * + * @returns + */ + private async _getEmployeeProfile() { + try { + if (!this.employeeId) { + return; + } + + // Fetch employee data from the service + const employee = await firstValueFrom( + this._employeeService.getEmployeeById(this.employeeId, [ + 'user', + 'organizationDepartments', + 'organizationPosition', + 'organizationEmploymentTypes', + 'tags', + 'skills', + 'contact' + ]) + ); + + // Set the selected employee in the store and component + this._employeeStore.selectedEmployee = this.selectedEmployee = employee; + + // Set the employee name for display + this.employeeName = employee?.user?.name || employee?.user?.username || 'Unknown Employee'; + } catch (error) { + // Handle errors gracefully + console.error('Error fetching employee profile:', error); + this._errorHandlingService.handleError(error); + } + } + /** * Submit the employee form with updated data * @@ -218,44 +314,12 @@ export class EditEmployeeProfileComponent extends TranslationBaseComponent imple } /** - * Retrieves and sets the profile of the selected employee + * Applies translations to the page tabs. */ - private async _getEmployeeProfile() { - try { - const { id } = this.routeParams; - if (!id) { - return; - } - - // Fetch employee data from the service - const employee = await firstValueFrom( - this._employeeService.getEmployeeById(id, [ - 'user', - 'organizationDepartments', - 'organizationPosition', - 'organizationEmploymentTypes', - 'tags', - 'skills', - 'contact' - ]) - ); - - // Set the selected employee in the store and component - this._employeeStore.selectedEmployee = this.selectedEmployee = employee; - - // Set the employee name for display - this.employeeName = employee?.user?.name || employee?.user?.username || 'Unknown Employee'; - } catch (error) { - // Handle errors gracefully - console.error('Error fetching employee profile:', error); - this._errorHandlingService.handleError(error); - } - } - private _applyTranslationOnTabs() { this.translateService.onLangChange .pipe( - tap(() => this.loadTabs()), + tap(() => this._registerPageTabs()), untilDestroyed(this) ) .subscribe(); diff --git a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.html b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.html index e0083798014..3c10456bc9b 100644 --- a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.html +++ b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.html @@ -9,6 +9,11 @@

{{ 'EMPLOYEES_PAGE.EDIT_EMPLOYEE.GENERAL_SETTINGS' | translate }} + +
  • + {{ 'ORGANIZATIONS_PAGE.EDIT.SETTINGS.TIMER_SETTINGS' | translate }} +
  • +
  • {{ 'EMPLOYEES_PAGE.EDIT_EMPLOYEE.INTEGRATIONS' | translate }} @@ -49,6 +54,112 @@

    + + + {{ 'ORGANIZATIONS_PAGE.EDIT.SETTINGS.TIMER_SETTINGS' | translate }} + + +
    +
    +
    +
    + + {{ 'ORGANIZATIONS_PAGE.EDIT.SETTINGS.ALLOW_MANUAL_TIME' | translate }} + + +
    +
    +
    +
    + + {{ 'ORGANIZATIONS_PAGE.EDIT.SETTINGS.ALLOW_MODIFY_TIME' | translate }} + + +
    +
    +
    +
    + + {{ 'ORGANIZATIONS_PAGE.EDIT.SETTINGS.ALLOW_DELETE_TIME' | translate }} + + +
    +
    + +
    + + {{ 'ORGANIZATIONS_PAGE.EDIT.SETTINGS.ALLOW_SCREEN_CAPTURE' | translate }} + + +
    +
    +
    +
    +
    {{ 'EMPLOYEES_PAGE.EDIT_EMPLOYEE.INTEGRATIONS' | translate }} diff --git a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.scss b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.scss index f39bdbd2305..ce5a3031253 100644 --- a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.scss +++ b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.scss @@ -303,3 +303,22 @@ nb-accordion { width: 100%; } } +:host ::ng-deep nb-toggle { + padding: 10px; + border: 1px solid nb-theme(gauzy-border-default-color); + border-radius: nb-theme(border-radius); + & > label { + margin-bottom: 0; + } +} +:host ::ng-deep .toggle { + border: 1px solid #7e7e8f !important; + background-color: #7e7e8f !important; + &.checked { + background-color: nb-theme(text-primary-color) !important; + border: 1px solid nb-theme(text-primary-color) !important; + & + span { + color: nb-theme(text-primary-color); + } + } +} diff --git a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.ts b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.ts index ab62a34c899..fe79b27c4a6 100644 --- a/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.ts +++ b/apps/gauzy/src/app/pages/employees/edit-employee/edit-employee-profile/edit-employee-settings/edit-employee-other-settings.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, OnDestroy, ChangeDetectorRef, ViewChild } from '@angular/core'; -import { UntypedFormBuilder, UntypedFormGroup, NgForm } from '@angular/forms'; -import { filter, tap } from 'rxjs/operators'; +import { FormBuilder, FormGroup, NgForm } from '@angular/forms'; +import { filter, tap } from 'rxjs'; import { NbAccordionComponent, NbAccordionItemComponent } from '@nebular/theme'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import * as moment from 'moment'; @@ -32,23 +32,28 @@ export class EditEmployeeOtherSettingsComponent implements OnInit, OnDestroy { */ @ViewChild('general') general: NbAccordionItemComponent; @ViewChild('integrations') integrations: NbAccordionItemComponent; + @ViewChild('timer') timer: NbAccordionItemComponent; /** * Employee other settings settings */ - public form: UntypedFormGroup = EditEmployeeOtherSettingsComponent.buildForm(this.fb); - static buildForm(fb: UntypedFormBuilder): UntypedFormGroup { + public form: FormGroup = EditEmployeeOtherSettingsComponent.buildForm(this.fb); + static buildForm(fb: FormBuilder): FormGroup { return fb.group({ timeZone: [], timeFormat: [], upworkId: [], - linkedInId: [] + linkedInId: [], + allowManualTime: [false], + allowModifyTime: [false], + allowDeleteTime: [false], + allowScreenshotCapture: [true] }); } constructor( private readonly cdr: ChangeDetectorRef, - private readonly fb: UntypedFormBuilder, + private readonly fb: FormBuilder, private readonly employeeStore: EmployeeStore ) {} @@ -82,7 +87,11 @@ export class EditEmployeeOtherSettingsComponent implements OnInit, OnDestroy { timeZone: user.timeZone || moment.tz.guess(), // set current timezone, if employee don't have any timezone timeFormat: user.timeFormat, upworkId: employee.upworkId, - linkedInId: employee.linkedInId + linkedInId: employee.linkedInId, + allowManualTime: employee.allowManualTime, + allowDeleteTime: employee.allowDeleteTime, + allowModifyTime: employee.allowModifyTime, + allowScreenshotCapture: employee.allowScreenshotCapture }); this.form.updateValueAndValidity(); } @@ -97,7 +106,16 @@ export class EditEmployeeOtherSettingsComponent implements OnInit, OnDestroy { return; } const { organizationId, tenantId } = this.selectedEmployee; - const { timeZone, timeFormat, upworkId, linkedInId } = this.form.value; + const { + timeZone, + timeFormat, + upworkId, + linkedInId, + allowScreenshotCapture, + allowManualTime, + allowModifyTime, + allowDeleteTime + } = this.form.value; /** Update user fields */ this.employeeStore.userForm = { @@ -110,7 +128,11 @@ export class EditEmployeeOtherSettingsComponent implements OnInit, OnDestroy { upworkId, linkedInId, organizationId, - tenantId + tenantId, + allowManualTime, + allowModifyTime, + allowDeleteTime, + allowScreenshotCapture }; } diff --git a/apps/gauzy/src/app/pages/employees/employees-routing.module.ts b/apps/gauzy/src/app/pages/employees/employees-routing.module.ts index 1814434c172..346b3b8f8eb 100644 --- a/apps/gauzy/src/app/pages/employees/employees-routing.module.ts +++ b/apps/gauzy/src/app/pages/employees/employees-routing.module.ts @@ -18,9 +18,13 @@ import { } from './edit-employee/edit-employee-profile'; import { EmployeeResolver } from './employee.resolver'; -export function redirectTo() { - return '/pages/dashboard'; -} +const selectors = { + team: false, + project: false, + employee: false, + date: false, + organization: false +}; const routes: Routes = [ { @@ -28,15 +32,19 @@ const routes: Routes = [ component: EmployeesComponent, canActivate: [PermissionsGuard], data: { + // The data table identifier for the route + dataTableId: 'employee-manage', + // The permission required to access the route permissions: { only: [PermissionsEnum.ORG_EMPLOYEES_VIEW], - redirectTo + redirectTo: '/pages/dashboard' }, + // The selectors for the route selectors: { + team: false, project: false, employee: false, - date: false, - team: false + date: false } } }, @@ -45,14 +53,17 @@ const routes: Routes = [ component: EditEmployeeComponent, canActivate: [PermissionsGuard], data: { + // The tabset identifier for the route + tabsetId: 'employee-edit', + // The permission required to access the route permissions: { only: [PermissionsEnum.ORG_EMPLOYEES_EDIT, PermissionsEnum.PROFILE_EDIT], - redirectTo - } - }, - resolve: { - employee: EmployeeResolver + redirectTo: '/pages/dashboard' + }, + // The selectors for the route + selectors }, + resolve: { employee: EmployeeResolver }, children: [ { path: '', @@ -62,106 +73,56 @@ const routes: Routes = [ { path: 'account', component: EditEmployeeMainComponent, - data: { - selectors: { - project: false, - organization: false, - date: false - } - } + data: { selectors } }, { path: 'networks', component: EditEmployeeNetworksComponent, - data: { - selectors: { - project: false, - organization: false, - date: false - } - } + data: { selectors } }, { path: 'rates', component: EditEmployeeRatesComponent, - data: { - selectors: { - project: false, - organization: false, - date: false - } - } + data: { selectors } }, { path: 'projects', component: EditEmployeeProjectsComponent, canActivate: [PermissionsGuard], data: { - selectors: { - project: false, - organization: false, - date: false - }, + // The selectors for the route + selectors, + // The permission required to access the route permissions: { only: [PermissionsEnum.ALL_ORG_VIEW, PermissionsEnum.ORG_PROJECT_VIEW], - redirectTo + redirectTo: '/pages/dashboard' } } }, { path: 'contacts', component: EditEmployeeContactComponent, - data: { - selectors: { - project: false, - organization: false, - date: false - } - } + data: { selectors } }, { path: 'location', component: EditEmployeeLocationComponent, - data: { - selectors: { - project: false, - organization: false, - date: false - } - } + data: { selectors } }, { path: 'hiring', component: EditEmployeeHiringComponent, - data: { - selectors: { - project: false, - organization: false, - date: false - } - } + data: { selectors } }, { path: 'employment', component: EditEmployeeEmploymentComponent, - data: { - selectors: { - project: false, - organization: false, - date: false - } - } + data: { selectors } }, { path: 'settings', component: EditEmployeeOtherSettingsComponent, - data: { - selectors: { - project: false, - organization: false, - date: false - } - } + data: { selectors } } ] }, diff --git a/apps/gauzy/src/app/pages/employees/employees.component.html b/apps/gauzy/src/app/pages/employees/employees.component.html index 6e2530a5098..5adff6fe806 100644 --- a/apps/gauzy/src/app/pages/employees/employees.component.html +++ b/apps/gauzy/src/app/pages/employees/employees.component.html @@ -12,7 +12,7 @@

    - + @@ -44,10 +44,44 @@

    - - + + + + +
    + +
    +
    + + + +
    +
    + + + +
    + +
    +
    + + + +

    {{ 'SETTINGS_MENU.NO_LAYOUT' | translate }}

    +
    +
    @@ -57,6 +91,33 @@

    + + + + + + + + + + + + + + + + + +
    @@ -146,57 +207,3 @@

    - - - - - - - - - - - - - - -
    - -
    -
    - - - -
    -
    - - -
    - -
    -
    - - - - - diff --git a/apps/gauzy/src/app/pages/employees/employees.component.ts b/apps/gauzy/src/app/pages/employees/employees.component.ts index 20dd4e757f4..41be82c458e 100644 --- a/apps/gauzy/src/app/pages/employees/employees.component.ts +++ b/apps/gauzy/src/app/pages/employees/employees.component.ts @@ -3,14 +3,16 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { NbDialogService } from '@nebular/theme'; import { TranslateService } from '@ngx-translate/core'; -import { Cell } from 'angular2-smart-table'; -import { debounceTime, filter, tap } from 'rxjs/operators'; -import { Subject, firstValueFrom } from 'rxjs'; +import { Cell, IColumns, Settings } from 'angular2-smart-table'; +import { Subject, debounceTime, filter, firstValueFrom, tap } from 'rxjs'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { EmployeeStore, EmployeesService, ErrorHandlingService, + PageDataTableRegistryConfig, + PageDataTableRegistryId, + PageDataTableRegistryService, ServerDataSource, Store, ToastrService @@ -36,12 +38,12 @@ import { EmployeeStartWorkComponent, InputFilterComponent, InviteMutationComponent, + PaginationFilterBaseComponent, PictureNameTagsComponent, TagsColorFilterComponent, TagsOnlyComponent, ToggleFilterComponent } from '@gauzy/ui-core/shared'; -import { PaginationFilterBaseComponent, IPaginationBase } from '@gauzy/ui-core/shared'; import { EmployeeAverageBonusComponent, EmployeeAverageExpensesComponent, @@ -52,11 +54,13 @@ import { @UntilDestroy({ checkProperties: true }) @Component({ + selector: 'ga-employees-list', templateUrl: './employees.component.html', styleUrls: ['./employees.component.scss'] }) export class EmployeesComponent extends PaginationFilterBaseComponent implements OnInit, OnDestroy { - public settingsSmartTable: object; + public dataTableId: PageDataTableRegistryId = this._route.snapshot.data.dataTableId; // The identifier for the data table + public settingsSmartTable: Settings; public smartTableSource: ServerDataSource; public selectedEmployee: EmployeeViewModel; public employees: EmployeeViewModel[] = []; @@ -68,7 +72,6 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements public disableButton: boolean = true; public includeDeleted: boolean = false; public loading: boolean = false; - public organizationInvitesAllowed: boolean = false; public organization: IOrganization; public refresh$: Subject = new Subject(); public employees$: Subject = this.subject$; @@ -96,16 +99,17 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements private readonly _errorHandlingService: ErrorHandlingService, private readonly _employeeStore: EmployeeStore, private readonly _httpClient: HttpClient, - private readonly _dateFormatPipe: DateFormatPipe + private readonly _dateFormatPipe: DateFormatPipe, + private readonly _pageDataTableRegistryService: PageDataTableRegistryService ) { super(translateService); this.setView(); } ngOnInit() { + this._registerDataTableColumns(); this._loadSmartTableSettings(); - this._applyTranslationOnSmartTable(); - + this._subscribeToQueryParams(); this.employees$ .pipe( debounceTime(300), @@ -128,22 +132,12 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements distinctUntilChange(), filter((organization: IOrganization) => !!organization), tap((organization: IOrganization) => (this.organization = organization)), - tap(({ invitesAllowed }) => (this.organizationInvitesAllowed = invitesAllowed)), tap(() => this._additionalColumns()), tap(() => this.refresh$.next(true)), tap(() => this.employees$.next(true)), untilDestroyed(this) ) .subscribe(); - this._route.queryParamMap - .pipe( - filter((params: ParamMap) => !!params), - filter((params: ParamMap) => params.get('openAddDialog') === 'true'), - debounceTime(1000), - tap(() => this.add()), - untilDestroyed(this) - ) - .subscribe(); this.refresh$ .pipe( filter(() => this.dataLayoutStyle === ComponentLayoutStyleEnum.CARDS_GRID), @@ -154,6 +148,26 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements .subscribe(); } + ngAfterViewInit(): void { + this._applyTranslationOnSmartTable(); + } + + /** + * Subscribes to the query parameters and performs actions based on the 'openAddDialog' parameter. + */ + private _subscribeToQueryParams(): void { + this._route.queryParamMap + .pipe( + // Check if 'openAddDialog' is set to 'true' and filter out falsy values + filter((params: ParamMap) => params?.get('openAddDialog') === 'true'), + // Trigger the add method + tap(() => this.add()), + // Automatically unsubscribe when component is destroyed + untilDestroyed(this) + ) + .subscribe(); + } + /** * Checks if the current user has the necessary permissions to perform button actions. * @returns A boolean indicating whether the user has the required permissions. @@ -163,7 +177,10 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements } /** - * + * @description + * This method sets the view layout for the component based on the current layout configuration. + * It listens for layout changes from the store and updates the `dataLayoutStyle`. + * Depending on the layout (e.g., if it's `CARDS_GRID`), it clears the employee list and triggers a refresh. */ setView() { this._store @@ -597,46 +614,48 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements } /** + * Maps an IEmployee object to a formatted employee object. * - * @param employee - * @returns + * @param employee The IEmployee object to map. + * @returns The formatted employee object. */ private employeeMapper(employee: IEmployee) { const { id, - user, + user = {}, isActive, endWork, tags, - averageIncome, - averageExpenses, - averageBonus, + averageIncome = 0, + averageExpenses = 0, + averageBonus = 0, startedWorkOn, isTrackingEnabled, isDeleted } = employee; + const { name = '', email = '', imageUrl = '' } = user; + + // Format start and end work dates, and create the work status range + const start = startedWorkOn ? this._dateFormatPipe.transform(startedWorkOn, null, 'LL') : ''; + const end = endWork ? this._dateFormatPipe.transform(endWork, null, 'LL') : ''; - /** - * "Range" when was hired and when exit - */ - const start = this._dateFormatPipe.transform(startedWorkOn, null, 'LL'); - const end = this._dateFormatPipe.transform(endWork, null, 'LL'); const workStatus = [start, end].filter(Boolean).join(' - '); + + // Return the mapped object return { - fullName: `${user.name}`, - email: user.email, + fullName: name || '', // Ensure default values for safety + email: email || '', id, isActive, endWork: endWork ? new Date(endWork) : '', workStatus: endWork ? workStatus : '', - imageUrl: user.imageUrl, - tags, - // TODO: load real bonus and bonusDate - bonus: this.bonusForSelectedMonth, + imageUrl: imageUrl || '', + tags: tags || [], + bonus: this.bonusForSelectedMonth, // TODO: load real bonus and bonusDate averageIncome: Math.floor(averageIncome), averageExpenses: Math.floor(averageExpenses), averageBonus: Math.floor(averageBonus), - bonusDate: Date.now(), + bonusDate: Date.now(), // Placeholder for actual bonus date employeeId: id, employee, startedWorkOn, @@ -646,143 +665,208 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements } /** - * Load Smart Table settings + * Registers custom columns for the 'employee-manage' data table. + * This method defines and registers the columns with various properties, + * including a custom filter function and a rendering component. */ - private _loadSmartTableSettings() { - const pagination: IPaginationBase = this.getPagination(); - this.settingsSmartTable = { - actions: false, - selectedRowIndex: -1, - pager: { - display: false, - perPage: pagination ? pagination.itemsPerPage : this.minItemPerPage - }, - noDataMessage: this.getTranslation('SM_TABLE.NO_DATA.EMPLOYEE'), - columns: { - fullName: { - title: this.getTranslation('SM_TABLE.FULL_NAME'), - type: 'custom', - class: 'align-row', - width: '20%', - renderComponent: PictureNameTagsComponent, - componentInitFunction: (instance: PictureNameTagsComponent, cell: Cell) => { - instance.rowData = cell.getRow().getData(); - instance.value = cell.getRawValue(); - }, - filter: { - type: 'custom', - component: InputFilterComponent - }, - filterFunction: (value: string) => { - this.setFilter({ field: 'user.name', search: value }); - } - }, - email: { - title: this.getTranslation('SM_TABLE.EMAIL'), - type: 'email', - class: 'email-column', - width: '20%', - filter: { - type: 'custom', - component: InputFilterComponent - }, - filterFunction: (value: string) => { - this.setFilter({ field: 'user.email', search: value }); - } + private _registerDataTableColumns(): void { + const columns: PageDataTableRegistryConfig[] = [ + { + dataTableId: this.dataTableId, + columnId: 'fullName', + order: 0, + title: () => this.getTranslation('SM_TABLE.FULL_NAME'), + type: 'custom', + class: 'align-row', + width: '20%', + isFilterable: true, + renderComponent: PictureNameTagsComponent, + componentInitFunction: (instance: PictureNameTagsComponent, cell: Cell) => { + instance.rowData = cell.getRow().getData(); + instance.value = cell.getRawValue(); }, - averageIncome: { - title: this.getTranslation('SM_TABLE.INCOME'), + filter: { type: 'custom', - isFilterable: false, - class: 'text-center', - width: '5%', - renderComponent: EmployeeAverageIncomeComponent, - componentInitFunction: (instance: EmployeeAverageIncomeComponent, cell: Cell) => { - instance.rowData = cell.getRow().getData(); - } + component: InputFilterComponent }, - averageExpenses: { - title: this.getTranslation('SM_TABLE.EXPENSES'), + filterFunction: this._getFilterFunction('user.name') + }, + { + dataTableId: this.dataTableId, + columnId: 'email', + order: 1, + title: () => this.getTranslation('SM_TABLE.EMAIL'), + type: 'text', + class: 'align-row', + width: '20%', + isFilterable: true, + filter: { type: 'custom', - isFilterable: false, - class: 'text-center', - width: '5%', - renderComponent: EmployeeAverageExpensesComponent, - componentInitFunction: (instance: EmployeeAverageExpensesComponent, cell: Cell) => { - instance.rowData = cell.getRow().getData(); - } + component: InputFilterComponent }, - averageBonus: { - title: this.getTranslation('SM_TABLE.BONUS_AVG'), + filterFunction: this._getFilterFunction('user.email') + }, + { + dataTableId: this.dataTableId, + columnId: 'averageIncome', + order: 2, + title: () => this.getTranslation('SM_TABLE.INCOME'), + type: 'custom', + isFilterable: false, + isSortable: true, + class: 'text-center', + width: '5%', + renderComponent: EmployeeAverageIncomeComponent, + componentInitFunction: (instance: EmployeeAverageIncomeComponent, cell: Cell) => { + instance.rowData = cell.getRow().getData(); + } + }, + { + dataTableId: this.dataTableId, + columnId: 'averageExpenses', + order: 3, + title: () => this.getTranslation('SM_TABLE.EXPENSES'), + type: 'custom', + isFilterable: false, + isSortable: true, + class: 'text-center', + width: '5%', + renderComponent: EmployeeAverageExpensesComponent, + componentInitFunction: (instance: EmployeeAverageExpensesComponent, cell: Cell) => { + instance.rowData = cell.getRow().getData(); + } + }, + { + dataTableId: this.dataTableId, + columnId: 'averageBonus', + order: 4, + title: () => this.getTranslation('SM_TABLE.BONUS_AVG'), + type: 'custom', + isFilterable: false, + isSortable: true, + class: 'text-center', + width: '5%', + renderComponent: EmployeeAverageBonusComponent, + componentInitFunction: (instance: EmployeeAverageBonusComponent, cell: Cell) => { + instance.rowData = cell.getRow().getData(); + } + }, + { + dataTableId: this.dataTableId, + columnId: 'isTrackingEnabled', + order: 5, + title: () => this.getTranslation('SM_TABLE.TIME_TRACKING'), + type: 'custom', + isFilterable: true, + isSortable: true, + class: 'text-center', + width: '5%', + filter: { type: 'custom', - isFilterable: false, - class: 'text-center', - width: '5%', - renderComponent: EmployeeAverageBonusComponent, - componentInitFunction: (instance: EmployeeAverageBonusComponent, cell: Cell) => { - instance.rowData = cell.getRow().getData(); - } + component: ToggleFilterComponent }, - isTrackingEnabled: { - title: this.getTranslation('SM_TABLE.TIME_TRACKING'), + filterFunction: this._getFilterFunction('isTrackingEnabled'), + renderComponent: EmployeeTimeTrackingStatusComponent, + componentInitFunction: (instance: EmployeeTimeTrackingStatusComponent, cell: Cell) => { + instance.rowData = cell.getRow().getData(); + } + }, + { + dataTableId: this.dataTableId, + columnId: 'tags', + order: 6, + title: () => this.getTranslation('SM_TABLE.TAGS'), + type: 'custom', + width: '20%', + isFilterable: true, + isSortable: false, + filter: { type: 'custom', - class: 'text-center', - width: '5%', - renderComponent: EmployeeTimeTrackingStatusComponent, - componentInitFunction: (instance: EmployeeTimeTrackingStatusComponent, cell: Cell) => { - instance.rowData = cell.getRow().getData(); - }, - filter: { - type: 'custom', - component: ToggleFilterComponent - }, - filterFunction: (checked: boolean) => { - this.setFilter({ - field: 'isTrackingEnabled', - search: checked - }); - } + component: TagsColorFilterComponent }, - tags: { - title: this.getTranslation('SM_TABLE.TAGS'), - type: 'custom', - width: '20%', - renderComponent: TagsOnlyComponent, - componentInitFunction: (instance: TagsOnlyComponent, cell: Cell) => { - instance.rowData = cell.getRow().getData(); - instance.value = cell.getValue(); - }, - filter: { - type: 'custom', - component: TagsColorFilterComponent - }, - filterFunction: (tags: ITag[]) => { - const tagIds = []; - for (const tag of tags) { - tagIds.push(tag.id); - } - this.setFilter({ field: 'tags', search: tagIds }); - }, - isSortable: false + filterFunction: (tags: ITag[]) => { + const tagIds = tags.map((tag) => tag.id); + this.setFilter({ field: 'tags', search: tagIds }); + return tags.length > 0; }, - workStatus: { - title: this.getTranslation('SM_TABLE.STATUS'), + renderComponent: TagsOnlyComponent, + componentInitFunction: (instance: TagsOnlyComponent, cell: Cell) => { + instance.rowData = cell.getRow().getData(); + instance.value = cell.getValue(); + } + }, + { + dataTableId: this.dataTableId, + columnId: 'workStatus', + order: 7, + title: () => this.getTranslation('SM_TABLE.STATUS'), + type: 'custom', + class: 'text-center', + width: '5%', + isFilterable: true, + isSortable: false, + filter: { type: 'custom', - class: 'text-center', - width: '5%', - renderComponent: EmployeeWorkStatusComponent, - componentInitFunction: (instance: EmployeeWorkStatusComponent, cell: Cell) => { - instance.rowData = cell.getRow().getData(); - }, - filter: { - type: 'custom', - component: ToggleFilterComponent - }, - filterFunction: (isActive: boolean) => { - this.setFilter({ field: 'isActive', search: isActive }); - } + component: ToggleFilterComponent + }, + filterFunction: (isActive: boolean) => { + this.setFilter({ field: 'isActive', search: isActive }); + return isActive; + }, + renderComponent: EmployeeWorkStatusComponent, + componentInitFunction: (instance: EmployeeWorkStatusComponent, cell: Cell) => { + instance.rowData = cell.getRow().getData(); } } + ]; + + columns.forEach((column: PageDataTableRegistryConfig) => { + this._pageDataTableRegistryService.registerPageDataTableColumn(column); + }); + } + + /** + * Helper function to create a reusable filter function for columns. + * @param field - The field to filter by. + */ + private _getFilterFunction(field: string) { + return (value: string) => { + this.setFilter({ field, search: value }); + return value.length > 0; // Return `true` if the value is non-empty + }; + } + + /** + * Retrieves the registered columns for the 'employee-manage' data table. + * + * This method fetches all the column configurations registered under the + * 'employee-manage' data table from the PageDataTableRegistryService. + * It returns the columns in the format of `IColumns`, which can be used for rendering or + * further manipulation in the smart table. + * + * @returns {IColumns} The column configurations for the 'employee-manage' table. + */ + getColumns(): IColumns { + // Fetch and return the columns for 'employee-manage' data table + return this._pageDataTableRegistryService.getPageDataTableColumns(this.dataTableId); + } + + /** + * Load Smart Table settings + */ + private _loadSmartTableSettings() { + // Get pagination settings + const { itemsPerPage } = this.getPagination() || { itemsPerPage: this.minItemPerPage }; + + // Configure Smart Table settings + this.settingsSmartTable = { + actions: false, + noDataMessage: this.getTranslation('SM_TABLE.NO_DATA.EMPLOYEE'), + pager: { + display: false, + perPage: itemsPerPage + }, + columns: { ...this.getColumns() } }; } @@ -799,52 +883,50 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements // Destructure properties for clarity const { allowScreenshotCapture } = this.organization; - // Check if screenshot capture is allowed - if (allowScreenshotCapture) { - // Configure the additional column for screenshot capture - this.settingsSmartTable['columns']['allowScreenshotCapture'] = { - title: this.getTranslation('SM_TABLE.SCREEN_CAPTURE'), + // Check if screenshot capture is allowed and hide the column if not + this._pageDataTableRegistryService.registerPageDataTableColumn({ + dataTableId: this.dataTableId, // The identifier for the data table location + columnId: 'allowScreenshotCapture', // The identifier for the column + order: 8, // The order of the column in the table + title: () => this.getTranslation('SM_TABLE.SCREEN_CAPTURE'), // The title of the column + type: 'custom', // The type of the column + class: 'text-center', // The class of the column + width: '5%', // The width of the column + isFilterable: true, // Indicates whether the column is filterable + isSortable: false, + hide: allowScreenshotCapture === false, + filter: { type: 'custom', - class: 'text-center', - editable: false, - addable: false, - notShownField: true, - // Configure custom filter for the column - filter: { - type: 'custom', - component: ToggleFilterComponent - }, - // Define filter function to update the filter settings - filterFunction: (isEnable: boolean) => { - this.setFilter({ - field: 'allowScreenshotCapture', - search: isEnable - }); - }, - // Configure custom component for rendering the column - renderComponent: AllowScreenshotCaptureComponent, - // Initialize component function to set initial values - componentInitFunction: (instance: AllowScreenshotCaptureComponent, cell: Cell) => { - instance.rowData = cell.getRow().getData(); - instance.value = cell.getValue(); - - // Subscribe to the allowScreenshotCaptureChange event - instance.allowScreenshotCaptureChange.subscribe({ - next: (isAllow: boolean) => { - // Clear selected items and update allowScreenshotCapture - this.clearItem(); - this._updateAllowScreenshotCapture(instance.rowData, isAllow); - }, - error: (err: any) => { - console.warn(err); - } - }); - } - }; - } + component: ToggleFilterComponent + }, + filterFunction: (isEnable: boolean) => { + this.setFilter({ field: 'allowScreenshotCapture', search: isEnable }); + return isEnable; + }, + renderComponent: AllowScreenshotCaptureComponent, // The component to render the column + componentInitFunction: (instance: AllowScreenshotCaptureComponent, cell: Cell) => { + instance.rowData = cell.getRow().getData(); + instance.value = cell.getValue(); + + // Subscribe to the allowScreenshotCaptureChange event + instance.allowScreenshotCaptureChange.subscribe({ + next: (isAllow: boolean) => { + // Clear selected items and update allowScreenshotCapture + this.clearItem(); + this._updateAllowScreenshotCapture(instance.rowData, isAllow); + }, + error: (err: any) => { + console.warn(err); + } + }); + } + }); - // Copy the settingsSmartTable to trigger change detection - this.settingsSmartTable = { ...this.settingsSmartTable }; + // Update the settingsSmartTable with the new columns + this.settingsSmartTable = { + ...this.settingsSmartTable, + columns: this.getColumns() + }; } /** @@ -901,10 +983,7 @@ export class EmployeesComponent extends PaginationFilterBaseComponent implements * Clear selected item */ clearItem() { - this.selectEmployee({ - isSelected: false, - data: null - }); + this.selectEmployee({ isSelected: false, data: null }); } /** diff --git a/apps/gauzy/src/app/pages/employees/employees.module.ts b/apps/gauzy/src/app/pages/employees/employees.module.ts index 031c70f6f97..30077b3843b 100644 --- a/apps/gauzy/src/app/pages/employees/employees.module.ts +++ b/apps/gauzy/src/app/pages/employees/employees.module.ts @@ -48,7 +48,8 @@ import { SkillsInputModule, TableComponentsModule, TagsColorInputModule, - TimeZoneSelectorModule + TimeZoneSelectorModule, + DynamicTabsModule } from '@gauzy/ui-core/shared'; import { EditEmployeeContactComponent, @@ -139,7 +140,8 @@ const COMPONENTS = [ LanguageSelectorModule, SmartDataViewLayoutModule, CardGridModule, - TimeZoneSelectorModule + TimeZoneSelectorModule, + DynamicTabsModule ], declarations: [...COMPONENTS], providers: [OrganizationsService, InviteGuard, CandidatesService, OrganizationEmploymentTypesService, SkillsService] diff --git a/apps/gauzy/src/app/pages/integrations/components/integration-list/list.component.html b/apps/gauzy/src/app/pages/integrations/components/integration-list/list.component.html index 5ac9a3c99c1..e4c92f0528b 100644 --- a/apps/gauzy/src/app/pages/integrations/components/integration-list/list.component.html +++ b/apps/gauzy/src/app/pages/integrations/components/integration-list/list.component.html @@ -62,11 +62,11 @@

    {{ integration?.lastSyncedAt || integration?.updatedAt | dateTimeFormat }}

    - + (onSwitched)="updateIntegrationTenant(integration, $event)" + >
    { - return product?.name - ? `${this.translatableService.getTranslatedProperty(product, 'name')}` - : ''; + const translatedName = this.translatableService.getTranslatedProperty(product, 'name'); + return translatedName || ''; } }; break; diff --git a/apps/gauzy/src/app/pages/pages.component.ts b/apps/gauzy/src/app/pages/pages.component.ts index cc12f8a93ca..b8c2facdba0 100644 --- a/apps/gauzy/src/app/pages/pages.component.ts +++ b/apps/gauzy/src/app/pages/pages.component.ts @@ -2,12 +2,10 @@ import { AfterViewInit, Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Data, Router } from '@angular/router'; import { NbMenuItem } from '@nebular/theme'; import { TranslateService } from '@ngx-translate/core'; -import { merge, pairwise } from 'rxjs'; -import { filter, map, take, tap } from 'rxjs/operators'; +import { filter, map, merge, pairwise, take, tap } from 'rxjs'; import { NgxPermissionsService } from 'ngx-permissions'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { chain } from 'underscore'; -import { TranslationBaseComponent } from '@gauzy/ui-core/i18n'; +import { FeatureEnum, IOrganization, IRolePermission, IUser, IntegrationEnum, PermissionsEnum } from '@gauzy/contracts'; import { AuthStrategy, IJobMatchingEntity, @@ -19,8 +17,8 @@ import { Store, UsersService } from '@gauzy/ui-core/core'; -import { FeatureEnum, IOrganization, IRolePermission, IUser, IntegrationEnum, PermissionsEnum } from '@gauzy/contracts'; import { distinctUntilChange, isNotEmpty } from '@gauzy/ui-core/common'; +import { TranslationBaseComponent } from '@gauzy/ui-core/i18n'; import { ReportService } from './reports/all-report/report.service'; @UntilDestroy({ checkProperties: true }) @@ -80,15 +78,9 @@ export class PagesComponent extends TranslationBaseComponent implements AfterVie filter((organization: IOrganization) => !!organization), distinctUntilChange(), pairwise(), // Pair each emitted value with the previous one - tap(([organization]: [IOrganization, IOrganization]) => { - const { id: organizationId, tenantId } = organization; - + tap(([previousOrganization]: [IOrganization, IOrganization]) => { // Remove the specified menu items for previous selected organization - this._navMenuBuilderService.removeNavMenuItems( - // Define the base item IDs - this.getReportMenuBaseItemIds().map((itemId) => `${itemId}-${organizationId}-${tenantId}`), - 'reports' - ); + this.removeOrganizationReportsMenuItems(previousOrganization); }), untilDestroyed(this) ) @@ -118,25 +110,21 @@ export class PagesComponent extends TranslationBaseComponent implements AfterVie .subscribe(); this.reportService.menuItems$.pipe(distinctUntilChange(), untilDestroyed(this)).subscribe((menuItems) => { - if (menuItems) { - this.reportMenuItems = chain(menuItems) - .values() - .map((item) => { - return { - id: item.slug + `-${this.organization?.id}`, - title: item.name, - link: `/pages/reports/${item.slug}`, - icon: item.iconClass, - data: { - translationKey: `${item.name}` - } - }; - }) - .value(); - } else { - this.reportMenuItems = []; - } - this.addOrganizationReportsMenuItems(); + // Convert the menuItems object to an array + const reportItems = menuItems ? Object.values(menuItems) : []; + + this.reportMenuItems = reportItems.map((item) => ({ + id: item.slug, + title: item.name, + link: `/pages/reports/${item.slug}`, + icon: item.iconClass, + data: { + translationKey: item.name + } + })); + + // Add the report menu items to the navigation menu + this.addOrRemoveOrganizationReportsMenuItems(); }); } @@ -176,49 +164,62 @@ export class PagesComponent extends TranslationBaseComponent implements AfterVie } /** - * Adds report menu items to the organization's navigation menu. + * Removes the report menu items associated with the current organization. + * + * This function checks if the organization is defined. If not, it logs a warning and exits early. + * If the organization is defined, it constructs item IDs based on the organization and tenant ID + * and removes these items from the navigation menu. + * + * @returns {void} This function does not return a value. */ - private addOrganizationReportsMenuItems() { - if (!this.organization) { - // Handle the case where this.organization is not defined - console.warn('Organization not defined. Unable to add/remove menu items.'); + private removeOrganizationReportsMenuItems(organization: IOrganization): void { + // Return early if the organization is not defined, logging a warning + if (!organization) { + console.warn(`Organization not defined. Unable to remove menu items.`); return; } - const { id: organizationId, tenantId } = this.organization; - // Remove the specified menu items for current selected organization - // Note: We need to remove old menus before constructing new menus for the organization. - this._navMenuBuilderService.removeNavMenuItems( - // Define the base item IDs - this.getReportMenuBaseItemIds().map((itemId) => `${itemId}-${organizationId}-${tenantId}`), - 'reports' + // Destructure organization properties + const { id: organizationId, tenantId } = organization; + + // Generate the item IDs to remove and call the service method + const itemIdsToRemove = this.getReportMenuBaseItemIds().map( + (itemId) => `${itemId}-${organizationId}-${tenantId}` ); - // Validate if reportMenuItems is an array and has elements - if (!Array.isArray(this.reportMenuItems) || this.reportMenuItems.length === 0) { + this._navMenuBuilderService.removeNavMenuItems(itemIdsToRemove, 'reports'); + } + + /** + * Adds report menu items to the organization's navigation menu. + */ + private addOrRemoveOrganizationReportsMenuItems() { + if (!this.organization) { + console.warn('Organization not defined. Unable to add/remove menu items.'); return; } + const { id: organizationId, tenantId } = this.organization; + + // Remove old menu items before constructing new ones for the organization + this.removeOrganizationReportsMenuItems(this.organization); + // Iterate over each report and add it to the navigation menu - try { - this.reportMenuItems.forEach((report: NavMenuSectionItem) => { - // Validate the structure of each report item - if (report && report.id && report.title) { - this._navMenuBuilderService.addNavMenuItem( - { - id: report.id, // Unique identifier for the menu item - title: report.title, // The title of the menu item - icon: report.icon, // The icon class for the menu item, using FontAwesome in this case - link: report.link, // The link where the menu item directs - data: report.data - }, - 'reports' - ); // The id of the section where this item should be added - } - }); - } catch (error) { - console.error('Error adding report menu items', error); - } + this.reportMenuItems.forEach((report: NavMenuSectionItem) => { + // Validate the structure of each report item + if (report?.id && report?.title) { + this._navMenuBuilderService.addNavMenuItem( + { + id: `${report.id}-${organizationId}-${tenantId}`, // Unique identifier for the menu item + title: report.title, // The title of the menu item + icon: report.icon, // The icon class for the menu item + link: report.link, // The link where the menu item directs + data: report.data // The data associated with the menu item + }, + 'reports' // The id of the section where this item should be added + ); + } + }); } /** @@ -402,5 +403,8 @@ export class PagesComponent extends TranslationBaseComponent implements AfterVie this.store.featureTenant = tenant.featureOrganizations.filter((item) => !item.organizationId); } - ngOnDestroy() {} + ngOnDestroy() { + // Remove the report menu items associated with the current organization before destroying the component + this.removeOrganizationReportsMenuItems(this.organization); + } } diff --git a/apps/server-api/src/assets/styles/gauzy/theme.gauzy-dark.ts b/apps/server-api/src/assets/styles/gauzy/theme.gauzy-dark.ts index 4ec88895401..9bf0e771154 100644 --- a/apps/server-api/src/assets/styles/gauzy/theme.gauzy-dark.ts +++ b/apps/server-api/src/assets/styles/gauzy/theme.gauzy-dark.ts @@ -45,18 +45,12 @@ const theme = { export const GAUZY_DARK = { name: 'gauzy-dark', - base: 'dark', + base: 'dark', variables: { ...theme, temperature: { - arcFill: [ - theme.primary, - theme.primary, - theme.primary, - theme.primary, - theme.primary - ], + arcFill: Array(5).fill(theme.primary), arcEmpty: theme.bg2, thumbBg: theme.bg2, thumbBorder: theme.primary diff --git a/apps/server-api/src/assets/styles/material/theme.material-dark.ts b/apps/server-api/src/assets/styles/material/theme.material-dark.ts index fa30be941ca..9eb1709ff77 100644 --- a/apps/server-api/src/assets/styles/material/theme.material-dark.ts +++ b/apps/server-api/src/assets/styles/material/theme.material-dark.ts @@ -54,13 +54,7 @@ export const MATERIAL_DARK_THEME = { base: 'default', variables: { temperature: { - arcFill: [ - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary - ], + arcFill: Array(5).fill(baseThemeVariables.primary), arcEmpty: baseThemeVariables.bg2, thumbBg: baseThemeVariables.bg2, thumbBorder: baseThemeVariables.primary diff --git a/apps/server-api/src/assets/styles/material/theme.material-light.ts b/apps/server-api/src/assets/styles/material/theme.material-light.ts index 53cec035f34..bb2e166e089 100644 --- a/apps/server-api/src/assets/styles/material/theme.material-light.ts +++ b/apps/server-api/src/assets/styles/material/theme.material-light.ts @@ -54,13 +54,7 @@ export const MATERIAL_LIGHT_THEME = { base: 'default', variables: { temperature: { - arcFill: [ - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary - ], + arcFill: Array(5).fill(baseThemeVariables.primary), arcEmpty: baseThemeVariables.bg2, thumbBg: baseThemeVariables.bg2, thumbBorder: baseThemeVariables.primary diff --git a/apps/server-api/src/assets/styles/theme.dark.ts b/apps/server-api/src/assets/styles/theme.dark.ts index 983d147d3f4..63e247fa5b4 100644 --- a/apps/server-api/src/assets/styles/theme.dark.ts +++ b/apps/server-api/src/assets/styles/theme.dark.ts @@ -49,13 +49,7 @@ export const DARK_THEME = { ...theme, temperature: { - arcFill: [ - theme.primary, - theme.primary, - theme.primary, - theme.primary, - theme.primary - ], + arcFill: Array(5).fill(theme.primary), arcEmpty: theme.bg2, thumbBg: theme.bg2, thumbBorder: theme.primary diff --git a/apps/server-api/src/preload/preload.ts b/apps/server-api/src/preload/preload.ts index c9b66089034..b5ba9a2fc97 100644 --- a/apps/server-api/src/preload/preload.ts +++ b/apps/server-api/src/preload/preload.ts @@ -17,6 +17,10 @@ window.addEventListener('DOMContentLoaded', async () => { titleBar.refreshMenu(); }); + ipcRenderer.on('hide-menu', () => { + titleBar.dispose(); + }) + const overStyle = document.createElement('style'); overStyle.innerHTML = ` .cet-menubar-menu-container { diff --git a/apps/server/src/assets/styles/gauzy/_gauzy-dialogs.scss b/apps/server/src/assets/styles/gauzy/_gauzy-dialogs.scss index b5c00201c50..b3b2a2c797b 100644 --- a/apps/server/src/assets/styles/gauzy/_gauzy-dialogs.scss +++ b/apps/server/src/assets/styles/gauzy/_gauzy-dialogs.scss @@ -113,4 +113,4 @@ $shadow: 0 0 0 nb-theme(button-outline-width) rgba($color: $green, } @include dialog(var(--gauzy-card-1), var(--gauzy-sidebar-background-4)); -} \ No newline at end of file +} diff --git a/apps/server/src/assets/styles/gauzy/_gauzy-table.scss b/apps/server/src/assets/styles/gauzy/_gauzy-table.scss index ad4334cda60..a66381765ad 100644 --- a/apps/server/src/assets/styles/gauzy/_gauzy-table.scss +++ b/apps/server/src/assets/styles/gauzy/_gauzy-table.scss @@ -110,4 +110,4 @@ button { border-radius: nb-theme(border-radius); box-shadow: var(--gauzy-shadow) inset; } -} \ No newline at end of file +} diff --git a/apps/server/src/assets/styles/gauzy/index.ts b/apps/server/src/assets/styles/gauzy/index.ts index 178f1f36dbc..368e51a156c 100644 --- a/apps/server/src/assets/styles/gauzy/index.ts +++ b/apps/server/src/assets/styles/gauzy/index.ts @@ -1,2 +1,2 @@ export * from './theme.gauzy-dark'; -export * from './theme.gauzy-light'; \ No newline at end of file +export * from './theme.gauzy-light'; diff --git a/apps/server/src/assets/styles/gauzy/theme.gauzy-dark.ts b/apps/server/src/assets/styles/gauzy/theme.gauzy-dark.ts index 4ec88895401..9bf0e771154 100644 --- a/apps/server/src/assets/styles/gauzy/theme.gauzy-dark.ts +++ b/apps/server/src/assets/styles/gauzy/theme.gauzy-dark.ts @@ -45,18 +45,12 @@ const theme = { export const GAUZY_DARK = { name: 'gauzy-dark', - base: 'dark', + base: 'dark', variables: { ...theme, temperature: { - arcFill: [ - theme.primary, - theme.primary, - theme.primary, - theme.primary, - theme.primary - ], + arcFill: Array(5).fill(theme.primary), arcEmpty: theme.bg2, thumbBg: theme.bg2, thumbBorder: theme.primary diff --git a/apps/server/src/assets/styles/material/theme.material-dark.ts b/apps/server/src/assets/styles/material/theme.material-dark.ts index fa30be941ca..9eb1709ff77 100644 --- a/apps/server/src/assets/styles/material/theme.material-dark.ts +++ b/apps/server/src/assets/styles/material/theme.material-dark.ts @@ -54,13 +54,7 @@ export const MATERIAL_DARK_THEME = { base: 'default', variables: { temperature: { - arcFill: [ - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary - ], + arcFill: Array(5).fill(baseThemeVariables.primary), arcEmpty: baseThemeVariables.bg2, thumbBg: baseThemeVariables.bg2, thumbBorder: baseThemeVariables.primary diff --git a/apps/server/src/assets/styles/material/theme.material-light.ts b/apps/server/src/assets/styles/material/theme.material-light.ts index 53cec035f34..bb2e166e089 100644 --- a/apps/server/src/assets/styles/material/theme.material-light.ts +++ b/apps/server/src/assets/styles/material/theme.material-light.ts @@ -54,13 +54,7 @@ export const MATERIAL_LIGHT_THEME = { base: 'default', variables: { temperature: { - arcFill: [ - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary - ], + arcFill: Array(5).fill(baseThemeVariables.primary), arcEmpty: baseThemeVariables.bg2, thumbBg: baseThemeVariables.bg2, thumbBorder: baseThemeVariables.primary diff --git a/apps/server/src/assets/styles/theme.dark.ts b/apps/server/src/assets/styles/theme.dark.ts index 983d147d3f4..63e247fa5b4 100644 --- a/apps/server/src/assets/styles/theme.dark.ts +++ b/apps/server/src/assets/styles/theme.dark.ts @@ -49,13 +49,7 @@ export const DARK_THEME = { ...theme, temperature: { - arcFill: [ - theme.primary, - theme.primary, - theme.primary, - theme.primary, - theme.primary - ], + arcFill: Array(5).fill(theme.primary), arcEmpty: theme.bg2, thumbBg: theme.bg2, thumbBorder: theme.primary diff --git a/apps/server/src/preload/preload.ts b/apps/server/src/preload/preload.ts index c9b66089034..b5ba9a2fc97 100644 --- a/apps/server/src/preload/preload.ts +++ b/apps/server/src/preload/preload.ts @@ -17,6 +17,10 @@ window.addEventListener('DOMContentLoaded', async () => { titleBar.refreshMenu(); }); + ipcRenderer.on('hide-menu', () => { + titleBar.dispose(); + }) + const overStyle = document.createElement('style'); overStyle.innerHTML = ` .cet-menubar-menu-container { diff --git a/package.json b/package.json index 523c69d0c94..7a708dcde77 100644 --- a/package.json +++ b/package.json @@ -137,8 +137,8 @@ "build:package:config:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/config build:prod", "build:package:plugin": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugin build", "build:package:plugin:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugin build", - "build:package:plugins:pre": "yarn run build:package:ui-config && yarn run build:package:ui-core && yarn run build:package:ui-auth && yarn run build:package:plugin:onboarding-ui && yarn run build:package:plugin:legal-ui && yarn run build:package:plugin:job-search-ui && yarn run build:package:plugin:job-matching-ui && yarn run build:package:plugin:job-employee-ui && yarn run build:package:plugin:job-proposal-ui && yarn run build:package:plugin:public-layout-ui && yarn run build:package:plugin:maintenance-ui && yarn run build:package:plugin:integration-ai-ui && yarn run build:package:plugin:integration-hubstaff-ui", - "build:package:plugins:pre:prod": "yarn run build:package:ui-config:prod && yarn run build:package:ui-core:prod && yarn run build:package:ui-auth && yarn run build:package:plugin:onboarding-ui:prod && yarn run build:package:plugin:legal-ui:prod && yarn run build:package:plugin:job-search-ui:prod && yarn run build:package:plugin:job-matching-ui:prod && yarn run build:package:plugin:job-employee-ui:prod && yarn run build:package:plugin:job-proposal-ui:prod && yarn run build:package:plugin:public-layout-ui:prod && yarn run build:package:plugin:maintenance-ui:prod && yarn run build:package:plugin:integration-ai-ui:prod && yarn run build:package:plugin:integration-hubstaff-ui:prod", + "build:package:plugins:pre": "yarn run build:package:ui-config && yarn run build:package:ui-core && yarn run build:package:ui-auth && yarn run build:package:plugin:onboarding-ui && yarn run build:package:plugin:legal-ui && yarn run build:package:plugin:job-search-ui && yarn run build:package:plugin:job-matching-ui && yarn run build:package:plugin:job-employee-ui && yarn run build:package:plugin:job-proposal-ui && yarn run build:package:plugin:public-layout-ui && yarn run build:package:plugin:maintenance-ui && yarn run build:integration-ui-plugins", + "build:package:plugins:pre:prod": "yarn run build:package:ui-config:prod && yarn run build:package:ui-core:prod && yarn run build:package:ui-auth:prod && yarn run build:package:plugin:onboarding-ui:prod && yarn run build:package:plugin:legal-ui:prod && yarn run build:package:plugin:job-search-ui:prod && yarn run build:package:plugin:job-matching-ui:prod && yarn run build:package:plugin:job-employee-ui:prod && yarn run build:package:plugin:job-proposal-ui:prod && yarn run build:package:plugin:public-layout-ui:prod && yarn run build:package:plugin:maintenance-ui:prod && yarn run build:integration-ui-plugins:prod", "build:package:plugins:post": "yarn run build:package:plugin:integration-jira && yarn run build:package:plugin:integration-ai && yarn run build:package:plugin:sentry && yarn run build:package:plugin:jitsu-analytic && yarn run build:package:plugin:product-reviews && yarn run build:package:plugin:job-search && yarn run build:package:plugin:job-proposal && yarn run build:package:plugin:integration-github && yarn run build:package:plugin:knowledge-base && yarn run build:package:plugin:changelog && yarn run build:package:plugin:integration-hubstaff && yarn run build:package:plugin:integration-upwork", "build:package:plugins:post:prod": "yarn run build:package:plugin:integration-jira:prod && yarn run build:package:plugin:integration-ai:prod && yarn run build:package:plugin:sentry:prod && yarn run build:package:plugin:jitsu-analytic:prod && yarn run build:package:plugin:product-reviews:prod && yarn run build:package:plugin:job-search:prod && yarn run build:package:plugin:job-proposal:prod && yarn run build:package:plugin:integration-github:prod && yarn run build:package:plugin:knowledge-base:prod && yarn run build:package:plugin:changelog:prod && yarn run build:package:plugin:integration-hubstaff:prod && yarn run build:package:plugin:integration-upwork:prod", "build:package:plugin:integration-ai": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-ai build", @@ -160,13 +160,15 @@ "build:package:plugin:job-search": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/job-search build", "build:package:plugin:job-search:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/job-search build:prod", "build:package:plugin:integration-ai-ui": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-ai-ui lib:build", - "build:package:plugin:integration-ai-ui:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-ai-ui lib:build:prod", "build:package:plugin:integration-hubstaff-ui": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-hubstaff-ui lib:build", - "build:package:plugin:integration-hubstaff-ui:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-hubstaff-ui lib:build:prod", "build:package:plugin:integration-github-ui": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-github-ui lib:build", - "build:package:plugin:integration-github-ui:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-github-ui lib:build:prod", "build:package:plugin:integration-upwork-ui": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-upwork-ui lib:build", + "build:package:plugin:integration-ai-ui:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-ai-ui lib:build:prod", + "build:package:plugin:integration-hubstaff-ui:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-hubstaff-ui lib:build:prod", + "build:package:plugin:integration-github-ui:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-github-ui lib:build:prod", "build:package:plugin:integration-upwork-ui:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/integration-upwork-ui lib:build:prod", + "build:integration-ui-plugins": "yarn run build:package:plugin:integration-ai-ui && yarn run build:package:plugin:integration-hubstaff-ui && yarn run build:package:plugin:integration-upwork-ui && yarn run build:package:plugin:integration-github-ui", + "build:integration-ui-plugins:prod": "yarn run build:package:plugin:integration-ai-ui:prod && yarn run build:package:plugin:integration-hubstaff-ui:prod && yarn run build:package:plugin:integration-upwork-ui:prod && yarn run build:package:plugin:integration-github-ui:prod", "build:package:plugin:job-employee-ui": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/job-employee-ui lib:build", "build:package:plugin:job-employee-ui:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/job-employee-ui lib:build:prod", "build:package:plugin:job-matching-ui": "cross-env NODE_ENV=development NODE_OPTIONS=--max-old-space-size=12288 yarn --cwd ./packages/plugins/job-matching-ui lib:build", @@ -337,7 +339,7 @@ "@nebular/security": "^12.0.0", "@nebular/theme": "^12.0.0", "@ng-select/ng-select": "^11.2.0", - "angular2-smart-table": "^3.2.0", + "angular2-smart-table": "^3.3.0", "autoprefixer": "10.4.14", "bcrypt": "^5.1.1", "camelcase": "^6.3.0", diff --git a/packages/contracts/src/employee.model.ts b/packages/contracts/src/employee.model.ts index bf4bab5e5fe..3bbd3957f24 100644 --- a/packages/contracts/src/employee.model.ts +++ b/packages/contracts/src/employee.model.ts @@ -113,6 +113,10 @@ export interface IEmployee extends IBasePerTenantAndOrganizationEntityModel, ITa isTrackingEnabled: boolean; isDeleted?: boolean; allowScreenshotCapture?: boolean; + allowManualTime?: boolean; + allowModifyTime?: boolean; + allowDeleteTime?: boolean; + /** Upwork ID For Gauzy AI*/ upworkId?: string; /** LinkedIn ID For Gauzy AI*/ @@ -176,6 +180,9 @@ export interface IEmployeeUpdateInput extends IBasePerTenantAndOrganizationEntit upworkUrl?: string; profile_link?: string; allowScreenshotCapture?: boolean; + allowManualTime?: boolean; + allowModifyTime?: boolean; + allowDeleteTime?: boolean; /** Upwork ID For Gauzy AI*/ upworkId?: string; /** LinkedIn ID For Gauzy AI*/ diff --git a/packages/contracts/src/organization-projects.model.ts b/packages/contracts/src/organization-projects.model.ts index f3453439d3d..9b1c7892285 100644 --- a/packages/contracts/src/organization-projects.model.ts +++ b/packages/contracts/src/organization-projects.model.ts @@ -83,6 +83,7 @@ export interface IOrganizationProjectsFindInput billable?: boolean; billingFlat?: boolean; organizationTeamId?: ID; + members?: Partial; } export interface IOrganizationProjectCreateInput extends IOrganizationProjectBase, IMemberEntityBased {} @@ -94,6 +95,8 @@ export interface IOrganizationProjectStoreState { action: CrudActionEnum; } +export interface IOrganizationProjectEmployeeFindInput extends Partial {} + export interface IOrganizationProjectEmployee extends IBasePerTenantAndOrganizationEntityModel, IEmployeeEntityInput, diff --git a/packages/core/src/database/migration-executor.ts b/packages/core/src/database/migration-executor.ts index 6b1b78a22d0..36707a3674e 100644 --- a/packages/core/src/database/migration-executor.ts +++ b/packages/core/src/database/migration-executor.ts @@ -265,6 +265,7 @@ function queryParams(parameters: any[] | undefined): string { */ function getTemplate(connection: DataSource, name: string, timestamp: number, upSqls: string[], downSqls: string[]): string { return ` +import { Logger } from '@nestjs/common'; import { MigrationInterface, QueryRunner } from "typeorm"; import { yellow } from "chalk"; import { DatabaseTypeEnum } from "@gauzy/config"; @@ -279,7 +280,7 @@ export class ${camelCase(name, true)}${timestamp} implements MigrationInterface * @param queryRunner */ public async up(queryRunner: QueryRunner): Promise { - console.log(yellow(this.name + ' start running!')); + Logger.debug(yellow(this.name + ' start running!'), 'Migration'); switch (queryRunner.connection.options.type) { case DatabaseTypeEnum.sqlite: diff --git a/packages/core/src/database/migrations/1729861943822-AlterConstraintsForResourceLinkTable.ts b/packages/core/src/database/migrations/1729861943822-AlterConstraintsForResourceLinkTable.ts new file mode 100644 index 00000000000..7a9533ab4d5 --- /dev/null +++ b/packages/core/src/database/migrations/1729861943822-AlterConstraintsForResourceLinkTable.ts @@ -0,0 +1,276 @@ +import { Logger } from '@nestjs/common'; +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { yellow } from 'chalk'; +import { DatabaseTypeEnum } from '@gauzy/config'; + +export class AlterConstraintsForResourceLinkTable1729861943822 implements MigrationInterface { + name = 'AlterConstraintsForResourceLinkTable1729861943822'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + Logger.debug(yellow(this.name + ' start running!'), 'Migration'); + + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlUpQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlDownQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "resource_link" DROP CONSTRAINT "FK_2ef674d18792e8864fd8d484eac"`); + await queryRunner.query(`ALTER TABLE "resource_link" DROP CONSTRAINT "FK_95603855ae10050123e48a66881"`); + await queryRunner.query(`ALTER TABLE "resource_link" DROP CONSTRAINT "FK_f9438f82f6e93bd6a87b8216af9"`); + await queryRunner.query(`DROP INDEX "public"."IDX_841b729b80bc03ea38d16b8508"`); + await queryRunner.query(`DROP INDEX "public"."IDX_4c25c2c9d7ebbd0c07edd824ff"`); + await queryRunner.query(`DROP INDEX "public"."IDX_f9438f82f6e93bd6a87b8216af"`); + await queryRunner.query(`DROP INDEX "public"."IDX_95603855ae10050123e48a6688"`); + await queryRunner.query(`DROP INDEX "public"."IDX_44100d3eaf418ee67fa7a756f1"`); + await queryRunner.query(`DROP INDEX "public"."IDX_b73c278619bd8fb7f30f93182c"`); + await queryRunner.query(`DROP INDEX "public"."IDX_2ef674d18792e8864fd8d484ea"`); + await queryRunner.query(`CREATE INDEX "IDX_e891dad6f91b8eb04a47f42a06" ON "resource_link" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_2efdd5f6dc5d0c483edbc932ff" ON "resource_link" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_64d90b997156b7de382fd8a88f" ON "resource_link" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_b3caaf70dcd98d572c0fe09c59" ON "resource_link" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_ada8b0cf4463e653a756fc6db2" ON "resource_link" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_61dc38c01dfd2fe25cd934a0d1" ON "resource_link" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_df91a85b49f78544da67aa9d9a" ON "resource_link" ("creatorId") `); + await queryRunner.query( + `ALTER TABLE "resource_link" ADD CONSTRAINT "FK_64d90b997156b7de382fd8a88f2" FOREIGN KEY ("tenantId") REFERENCES "tenant"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "resource_link" ADD CONSTRAINT "FK_b3caaf70dcd98d572c0fe09c59f" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE "resource_link" ADD CONSTRAINT "FK_df91a85b49f78544da67aa9d9ad" FOREIGN KEY ("creatorId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "resource_link" DROP CONSTRAINT "FK_df91a85b49f78544da67aa9d9ad"`); + await queryRunner.query(`ALTER TABLE "resource_link" DROP CONSTRAINT "FK_b3caaf70dcd98d572c0fe09c59f"`); + await queryRunner.query(`ALTER TABLE "resource_link" DROP CONSTRAINT "FK_64d90b997156b7de382fd8a88f2"`); + await queryRunner.query(`DROP INDEX "public"."IDX_df91a85b49f78544da67aa9d9a"`); + await queryRunner.query(`DROP INDEX "public"."IDX_61dc38c01dfd2fe25cd934a0d1"`); + await queryRunner.query(`DROP INDEX "public"."IDX_ada8b0cf4463e653a756fc6db2"`); + await queryRunner.query(`DROP INDEX "public"."IDX_b3caaf70dcd98d572c0fe09c59"`); + await queryRunner.query(`DROP INDEX "public"."IDX_64d90b997156b7de382fd8a88f"`); + await queryRunner.query(`DROP INDEX "public"."IDX_2efdd5f6dc5d0c483edbc932ff"`); + await queryRunner.query(`DROP INDEX "public"."IDX_e891dad6f91b8eb04a47f42a06"`); + await queryRunner.query(`CREATE INDEX "IDX_2ef674d18792e8864fd8d484ea" ON "resource_link" ("creatorId") `); + await queryRunner.query(`CREATE INDEX "IDX_b73c278619bd8fb7f30f93182c" ON "resource_link" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_44100d3eaf418ee67fa7a756f1" ON "resource_link" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_95603855ae10050123e48a6688" ON "resource_link" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_f9438f82f6e93bd6a87b8216af" ON "resource_link" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_4c25c2c9d7ebbd0c07edd824ff" ON "resource_link" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_841b729b80bc03ea38d16b8508" ON "resource_link" ("isActive") `); + await queryRunner.query( + `ALTER TABLE "resource_link" ADD CONSTRAINT "FK_f9438f82f6e93bd6a87b8216af9" FOREIGN KEY ("tenantId") REFERENCES "tenant"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE "resource_link" ADD CONSTRAINT "FK_95603855ae10050123e48a66881" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE "resource_link" ADD CONSTRAINT "FK_2ef674d18792e8864fd8d484eac" FOREIGN KEY ("creatorId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_2ef674d18792e8864fd8d484ea"`); + await queryRunner.query(`DROP INDEX "IDX_b73c278619bd8fb7f30f93182c"`); + await queryRunner.query(`DROP INDEX "IDX_44100d3eaf418ee67fa7a756f1"`); + await queryRunner.query(`DROP INDEX "IDX_95603855ae10050123e48a6688"`); + await queryRunner.query(`DROP INDEX "IDX_f9438f82f6e93bd6a87b8216af"`); + await queryRunner.query(`DROP INDEX "IDX_4c25c2c9d7ebbd0c07edd824ff"`); + await queryRunner.query(`DROP INDEX "IDX_841b729b80bc03ea38d16b8508"`); + await queryRunner.query( + `CREATE TABLE "temporary_resource_link" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entity" varchar NOT NULL, "entityId" varchar NOT NULL, "title" varchar NOT NULL, "url" text NOT NULL, "metaData" text, "creatorId" varchar)` + ); + await queryRunner.query( + `INSERT INTO "temporary_resource_link"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "title", "url", "metaData", "creatorId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "title", "url", "metaData", "creatorId" FROM "resource_link"` + ); + await queryRunner.query(`DROP TABLE "resource_link"`); + await queryRunner.query(`ALTER TABLE "temporary_resource_link" RENAME TO "resource_link"`); + await queryRunner.query( + `CREATE TABLE "temporary_resource_link" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entity" varchar NOT NULL, "entityId" varchar NOT NULL, "title" varchar NOT NULL, "url" text NOT NULL, "metaData" text, "creatorId" varchar, CONSTRAINT "FK_64d90b997156b7de382fd8a88f2" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_b3caaf70dcd98d572c0fe09c59f" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_df91a85b49f78544da67aa9d9ad" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_resource_link"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "title", "url", "metaData", "creatorId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "title", "url", "metaData", "creatorId" FROM "resource_link"` + ); + await queryRunner.query(`DROP TABLE "resource_link"`); + await queryRunner.query(`ALTER TABLE "temporary_resource_link" RENAME TO "resource_link"`); + await queryRunner.query(`CREATE INDEX "IDX_e891dad6f91b8eb04a47f42a06" ON "resource_link" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_2efdd5f6dc5d0c483edbc932ff" ON "resource_link" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_64d90b997156b7de382fd8a88f" ON "resource_link" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_b3caaf70dcd98d572c0fe09c59" ON "resource_link" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_ada8b0cf4463e653a756fc6db2" ON "resource_link" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_61dc38c01dfd2fe25cd934a0d1" ON "resource_link" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_df91a85b49f78544da67aa9d9a" ON "resource_link" ("creatorId") `); + } + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_df91a85b49f78544da67aa9d9a"`); + await queryRunner.query(`DROP INDEX "IDX_61dc38c01dfd2fe25cd934a0d1"`); + await queryRunner.query(`DROP INDEX "IDX_ada8b0cf4463e653a756fc6db2"`); + await queryRunner.query(`DROP INDEX "IDX_b3caaf70dcd98d572c0fe09c59"`); + await queryRunner.query(`DROP INDEX "IDX_64d90b997156b7de382fd8a88f"`); + await queryRunner.query(`DROP INDEX "IDX_2efdd5f6dc5d0c483edbc932ff"`); + await queryRunner.query(`DROP INDEX "IDX_e891dad6f91b8eb04a47f42a06"`); + await queryRunner.query(`ALTER TABLE "resource_link" RENAME TO "temporary_resource_link"`); + await queryRunner.query( + `CREATE TABLE "resource_link" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entity" varchar NOT NULL, "entityId" varchar NOT NULL, "title" varchar NOT NULL, "url" text NOT NULL, "metaData" text, "creatorId" varchar)` + ); + await queryRunner.query( + `INSERT INTO "resource_link"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "title", "url", "metaData", "creatorId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "title", "url", "metaData", "creatorId" FROM "temporary_resource_link"` + ); + await queryRunner.query(`DROP TABLE "temporary_resource_link"`); + await queryRunner.query(`DROP INDEX "IDX_e891dad6f91b8eb04a47f42a06"`); + await queryRunner.query(`DROP INDEX "IDX_2ef674d18792e8864fd8d484ea"`); + await queryRunner.query(`ALTER TABLE "resource_link" RENAME TO "temporary_resource_link"`); + await queryRunner.query( + `CREATE TABLE "resource_link" ("deletedAt" datetime, "id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "isActive" boolean DEFAULT (1), "isArchived" boolean DEFAULT (0), "archivedAt" datetime, "tenantId" varchar, "organizationId" varchar, "entity" varchar NOT NULL, "entityId" varchar NOT NULL, "title" varchar NOT NULL, "url" text NOT NULL, "metaData" text, "creatorId" varchar, CONSTRAINT "FK_2ef674d18792e8864fd8d484eac" FOREIGN KEY ("creatorId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_95603855ae10050123e48a66881" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_f9438f82f6e93bd6a87b8216af9" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "resource_link"("deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "title", "url", "metaData", "creatorId") SELECT "deletedAt", "id", "createdAt", "updatedAt", "isActive", "isArchived", "archivedAt", "tenantId", "organizationId", "entity", "entityId", "title", "url", "metaData", "creatorId" FROM "temporary_resource_link"` + ); + await queryRunner.query(`DROP TABLE "temporary_resource_link"`); + await queryRunner.query(`CREATE INDEX "IDX_841b729b80bc03ea38d16b8508" ON "resource_link" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_4c25c2c9d7ebbd0c07edd824ff" ON "resource_link" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_f9438f82f6e93bd6a87b8216af" ON "resource_link" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_95603855ae10050123e48a6688" ON "resource_link" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_44100d3eaf418ee67fa7a756f1" ON "resource_link" ("entity") `); + await queryRunner.query(`CREATE INDEX "IDX_b73c278619bd8fb7f30f93182c" ON "resource_link" ("entityId") `); + await queryRunner.query(`CREATE INDEX "IDX_2ef674d18792e8864fd8d484ea" ON "resource_link" ("creatorId") `); + } + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`resource_link\` DROP FOREIGN KEY \`FK_2ef674d18792e8864fd8d484eac\``); + await queryRunner.query(`ALTER TABLE \`resource_link\` DROP FOREIGN KEY \`FK_95603855ae10050123e48a66881\``); + await queryRunner.query(`ALTER TABLE \`resource_link\` DROP FOREIGN KEY \`FK_f9438f82f6e93bd6a87b8216af9\``); + await queryRunner.query(`DROP INDEX \`IDX_4c25c2c9d7ebbd0c07edd824ff\` ON \`resource_link\``); + await queryRunner.query(`DROP INDEX \`IDX_f9438f82f6e93bd6a87b8216af\` ON \`resource_link\``); + await queryRunner.query(`DROP INDEX \`IDX_44100d3eaf418ee67fa7a756f1\` ON \`resource_link\``); + await queryRunner.query(`DROP INDEX \`IDX_2ef674d18792e8864fd8d484ea\` ON \`resource_link\``); + await queryRunner.query(`DROP INDEX \`IDX_841b729b80bc03ea38d16b8508\` ON \`resource_link\``); + await queryRunner.query(`DROP INDEX \`IDX_b73c278619bd8fb7f30f93182c\` ON \`resource_link\``); + await queryRunner.query(`DROP INDEX \`IDX_95603855ae10050123e48a6688\` ON \`resource_link\``); + await queryRunner.query(`CREATE INDEX \`IDX_e891dad6f91b8eb04a47f42a06\` ON \`resource_link\` (\`isActive\`)`); + await queryRunner.query( + `CREATE INDEX \`IDX_2efdd5f6dc5d0c483edbc932ff\` ON \`resource_link\` (\`isArchived\`)` + ); + await queryRunner.query(`CREATE INDEX \`IDX_64d90b997156b7de382fd8a88f\` ON \`resource_link\` (\`tenantId\`)`); + await queryRunner.query( + `CREATE INDEX \`IDX_b3caaf70dcd98d572c0fe09c59\` ON \`resource_link\` (\`organizationId\`)` + ); + await queryRunner.query(`CREATE INDEX \`IDX_ada8b0cf4463e653a756fc6db2\` ON \`resource_link\` (\`entity\`)`); + await queryRunner.query(`CREATE INDEX \`IDX_61dc38c01dfd2fe25cd934a0d1\` ON \`resource_link\` (\`entityId\`)`); + await queryRunner.query(`CREATE INDEX \`IDX_df91a85b49f78544da67aa9d9a\` ON \`resource_link\` (\`creatorId\`)`); + await queryRunner.query( + `ALTER TABLE \`resource_link\` ADD CONSTRAINT \`FK_64d90b997156b7de382fd8a88f2\` FOREIGN KEY (\`tenantId\`) REFERENCES \`tenant\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`resource_link\` ADD CONSTRAINT \`FK_b3caaf70dcd98d572c0fe09c59f\` FOREIGN KEY (\`organizationId\`) REFERENCES \`organization\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE \`resource_link\` ADD CONSTRAINT \`FK_df91a85b49f78544da67aa9d9ad\` FOREIGN KEY (\`creatorId\`) REFERENCES \`user\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`resource_link\` DROP FOREIGN KEY \`FK_df91a85b49f78544da67aa9d9ad\``); + await queryRunner.query(`ALTER TABLE \`resource_link\` DROP FOREIGN KEY \`FK_b3caaf70dcd98d572c0fe09c59f\``); + await queryRunner.query(`ALTER TABLE \`resource_link\` DROP FOREIGN KEY \`FK_64d90b997156b7de382fd8a88f2\``); + await queryRunner.query(`DROP INDEX \`IDX_df91a85b49f78544da67aa9d9a\` ON \`resource_link\``); + await queryRunner.query(`DROP INDEX \`IDX_61dc38c01dfd2fe25cd934a0d1\` ON \`resource_link\``); + await queryRunner.query(`DROP INDEX \`IDX_ada8b0cf4463e653a756fc6db2\` ON \`resource_link\``); + await queryRunner.query(`DROP INDEX \`IDX_b3caaf70dcd98d572c0fe09c59\` ON \`resource_link\``); + await queryRunner.query(`DROP INDEX \`IDX_64d90b997156b7de382fd8a88f\` ON \`resource_link\``); + await queryRunner.query(`DROP INDEX \`IDX_2efdd5f6dc5d0c483edbc932ff\` ON \`resource_link\``); + await queryRunner.query(`DROP INDEX \`IDX_e891dad6f91b8eb04a47f42a06\` ON \`resource_link\``); + await queryRunner.query( + `CREATE INDEX \`IDX_95603855ae10050123e48a6688\` ON \`resource_link\` (\`organizationId\`)` + ); + await queryRunner.query(`CREATE INDEX \`IDX_b73c278619bd8fb7f30f93182c\` ON \`resource_link\` (\`entityId\`)`); + await queryRunner.query(`CREATE INDEX \`IDX_841b729b80bc03ea38d16b8508\` ON \`resource_link\` (\`isActive\`)`); + await queryRunner.query(`CREATE INDEX \`IDX_2ef674d18792e8864fd8d484ea\` ON \`resource_link\` (\`creatorId\`)`); + await queryRunner.query(`CREATE INDEX \`IDX_44100d3eaf418ee67fa7a756f1\` ON \`resource_link\` (\`entity\`)`); + await queryRunner.query(`CREATE INDEX \`IDX_f9438f82f6e93bd6a87b8216af\` ON \`resource_link\` (\`tenantId\`)`); + await queryRunner.query( + `CREATE INDEX \`IDX_4c25c2c9d7ebbd0c07edd824ff\` ON \`resource_link\` (\`isArchived\`)` + ); + await queryRunner.query( + `ALTER TABLE \`resource_link\` ADD CONSTRAINT \`FK_f9438f82f6e93bd6a87b8216af9\` FOREIGN KEY (\`tenantId\`) REFERENCES \`tenant\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + await queryRunner.query( + `ALTER TABLE \`resource_link\` ADD CONSTRAINT \`FK_95603855ae10050123e48a66881\` FOREIGN KEY (\`organizationId\`) REFERENCES \`organization\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE` + ); + await queryRunner.query( + `ALTER TABLE \`resource_link\` ADD CONSTRAINT \`FK_2ef674d18792e8864fd8d484eac\` FOREIGN KEY (\`creatorId\`) REFERENCES \`user\`(\`id\`) ON DELETE CASCADE ON UPDATE NO ACTION` + ); + } +} diff --git a/packages/core/src/database/migrations/1729867054227-AddTimeTrackingPermissionsColumnsToEmployeeTable.ts b/packages/core/src/database/migrations/1729867054227-AddTimeTrackingPermissionsColumnsToEmployeeTable.ts new file mode 100644 index 00000000000..d24fe4f5013 --- /dev/null +++ b/packages/core/src/database/migrations/1729867054227-AddTimeTrackingPermissionsColumnsToEmployeeTable.ts @@ -0,0 +1,166 @@ +import { Logger } from '@nestjs/common'; +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { yellow } from 'chalk'; +import { DatabaseTypeEnum } from '@gauzy/config'; + +export class AddTimeTrackingPermissionsColumnsToEmployeeTable1729867054227 implements MigrationInterface { + name = 'AddTimeTrackingPermissionsColumnsToEmployeeTable1729867054227'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + Logger.debug(yellow(this.name + ' start running!'), 'Migration'); + + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresUpQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlUpQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise { + switch (queryRunner.connection.options.type) { + case DatabaseTypeEnum.sqlite: + case DatabaseTypeEnum.betterSqlite3: + await this.sqliteDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.postgres: + await this.postgresDownQueryRunner(queryRunner); + break; + case DatabaseTypeEnum.mysql: + await this.mysqlDownQueryRunner(queryRunner); + break; + default: + throw Error(`Unsupported database: ${queryRunner.connection.options.type}`); + } + } + + /** + * PostgresDB Up Migration + * + * @param queryRunner + */ + public async postgresUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "employee" ADD "allowManualTime" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "employee" ADD "allowModifyTime" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "employee" ADD "allowDeleteTime" boolean NOT NULL DEFAULT false`); + } + + /** + * PostgresDB Down Migration + * + * @param queryRunner + */ + public async postgresDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "employee" DROP COLUMN "allowDeleteTime"`); + await queryRunner.query(`ALTER TABLE "employee" DROP COLUMN "allowModifyTime"`); + await queryRunner.query(`ALTER TABLE "employee" DROP COLUMN "allowManualTime"`); + } + + /** + * SqliteDB and BetterSQlite3DB Up Migration + * + * @param queryRunner + */ + public async sqliteUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_5e719204dcafa8d6b2ecdeda13"`); + await queryRunner.query(`DROP INDEX "IDX_1c0c1370ecd98040259625e17e"`); + await queryRunner.query(`DROP INDEX "IDX_f4b0d329c4a3cf79ffe9d56504"`); + await queryRunner.query(`DROP INDEX "IDX_96dfbcaa2990df01fe5bb39ccc"`); + await queryRunner.query(`DROP INDEX "IDX_c6a48286f3aa8ae903bee0d1e7"`); + await queryRunner.query(`DROP INDEX "IDX_4b3303a6b7eb92d237a4379734"`); + await queryRunner.query(`DROP INDEX "IDX_510cb87f5da169e57e694d1a5c"`); + await queryRunner.query(`DROP INDEX "IDX_175b7be641928a31521224daa8"`); + await queryRunner.query( + `CREATE TABLE "temporary_employee" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "valueDate" datetime, "isActive" boolean DEFAULT (1), "short_description" varchar(200), "description" varchar, "startedWorkOn" datetime, "endWork" datetime, "payPeriod" varchar, "billRateValue" integer, "billRateCurrency" varchar, "reWeeklyLimit" integer, "offerDate" datetime, "acceptDate" datetime, "rejectDate" datetime, "employeeLevel" varchar(500), "anonymousBonus" boolean, "averageIncome" numeric, "averageBonus" numeric, "totalWorkHours" numeric DEFAULT (0), "averageExpenses" numeric, "show_anonymous_bonus" boolean, "show_average_bonus" boolean, "show_average_expenses" boolean, "show_average_income" boolean, "show_billrate" boolean, "show_payperiod" boolean, "show_start_work_on" boolean, "isJobSearchActive" boolean, "linkedInUrl" varchar, "facebookUrl" varchar, "instagramUrl" varchar, "twitterUrl" varchar, "githubUrl" varchar, "gitlabUrl" varchar, "upworkUrl" varchar, "stackoverflowUrl" varchar, "isVerified" boolean, "isVetted" boolean, "totalJobs" numeric, "jobSuccess" numeric, "profile_link" varchar, "userId" varchar NOT NULL, "contactId" varchar, "organizationPositionId" varchar, "isTrackingEnabled" boolean DEFAULT (0), "deletedAt" datetime, "allowScreenshotCapture" boolean NOT NULL DEFAULT (1), "upworkId" varchar, "linkedInId" varchar, "isOnline" boolean DEFAULT (0), "isTrackingTime" boolean DEFAULT (0), "minimumBillingRate" integer, "isAway" boolean DEFAULT (0), "isArchived" boolean DEFAULT (0), "fix_relational_custom_fields" boolean, "archivedAt" datetime, "allowManualTime" boolean NOT NULL DEFAULT (0), "allowModifyTime" boolean NOT NULL DEFAULT (0), "allowDeleteTime" boolean NOT NULL DEFAULT (0), CONSTRAINT "REL_1c0c1370ecd98040259625e17e" UNIQUE ("contactId"), CONSTRAINT "REL_f4b0d329c4a3cf79ffe9d56504" UNIQUE ("userId"), CONSTRAINT "FK_5e719204dcafa8d6b2ecdeda130" FOREIGN KEY ("organizationPositionId") REFERENCES "organization_position" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_1c0c1370ecd98040259625e17e2" FOREIGN KEY ("contactId") REFERENCES "contact" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_f4b0d329c4a3cf79ffe9d565047" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_c6a48286f3aa8ae903bee0d1e72" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_4b3303a6b7eb92d237a4379734e" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_employee"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "valueDate", "isActive", "short_description", "description", "startedWorkOn", "endWork", "payPeriod", "billRateValue", "billRateCurrency", "reWeeklyLimit", "offerDate", "acceptDate", "rejectDate", "employeeLevel", "anonymousBonus", "averageIncome", "averageBonus", "totalWorkHours", "averageExpenses", "show_anonymous_bonus", "show_average_bonus", "show_average_expenses", "show_average_income", "show_billrate", "show_payperiod", "show_start_work_on", "isJobSearchActive", "linkedInUrl", "facebookUrl", "instagramUrl", "twitterUrl", "githubUrl", "gitlabUrl", "upworkUrl", "stackoverflowUrl", "isVerified", "isVetted", "totalJobs", "jobSuccess", "profile_link", "userId", "contactId", "organizationPositionId", "isTrackingEnabled", "deletedAt", "allowScreenshotCapture", "upworkId", "linkedInId", "isOnline", "isTrackingTime", "minimumBillingRate", "isAway", "isArchived", "fix_relational_custom_fields", "archivedAt") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "valueDate", "isActive", "short_description", "description", "startedWorkOn", "endWork", "payPeriod", "billRateValue", "billRateCurrency", "reWeeklyLimit", "offerDate", "acceptDate", "rejectDate", "employeeLevel", "anonymousBonus", "averageIncome", "averageBonus", "totalWorkHours", "averageExpenses", "show_anonymous_bonus", "show_average_bonus", "show_average_expenses", "show_average_income", "show_billrate", "show_payperiod", "show_start_work_on", "isJobSearchActive", "linkedInUrl", "facebookUrl", "instagramUrl", "twitterUrl", "githubUrl", "gitlabUrl", "upworkUrl", "stackoverflowUrl", "isVerified", "isVetted", "totalJobs", "jobSuccess", "profile_link", "userId", "contactId", "organizationPositionId", "isTrackingEnabled", "deletedAt", "allowScreenshotCapture", "upworkId", "linkedInId", "isOnline", "isTrackingTime", "minimumBillingRate", "isAway", "isArchived", "fix_relational_custom_fields", "archivedAt" FROM "employee"` + ); + await queryRunner.query(`DROP TABLE "employee"`); + await queryRunner.query(`ALTER TABLE "temporary_employee" RENAME TO "employee"`); + await queryRunner.query( + `CREATE INDEX "IDX_5e719204dcafa8d6b2ecdeda13" ON "employee" ("organizationPositionId") ` + ); + await queryRunner.query(`CREATE INDEX "IDX_1c0c1370ecd98040259625e17e" ON "employee" ("contactId") `); + await queryRunner.query(`CREATE INDEX "IDX_f4b0d329c4a3cf79ffe9d56504" ON "employee" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_96dfbcaa2990df01fe5bb39ccc" ON "employee" ("profile_link") `); + await queryRunner.query(`CREATE INDEX "IDX_c6a48286f3aa8ae903bee0d1e7" ON "employee" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_4b3303a6b7eb92d237a4379734" ON "employee" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_510cb87f5da169e57e694d1a5c" ON "employee" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_175b7be641928a31521224daa8" ON "employee" ("isArchived") `); + } + + /** + * SqliteDB and BetterSQlite3DB Down Migration + * + * @param queryRunner + */ + public async sqliteDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_175b7be641928a31521224daa8"`); + await queryRunner.query(`DROP INDEX "IDX_510cb87f5da169e57e694d1a5c"`); + await queryRunner.query(`DROP INDEX "IDX_4b3303a6b7eb92d237a4379734"`); + await queryRunner.query(`DROP INDEX "IDX_c6a48286f3aa8ae903bee0d1e7"`); + await queryRunner.query(`DROP INDEX "IDX_96dfbcaa2990df01fe5bb39ccc"`); + await queryRunner.query(`DROP INDEX "IDX_f4b0d329c4a3cf79ffe9d56504"`); + await queryRunner.query(`DROP INDEX "IDX_1c0c1370ecd98040259625e17e"`); + await queryRunner.query(`DROP INDEX "IDX_5e719204dcafa8d6b2ecdeda13"`); + await queryRunner.query(`ALTER TABLE "employee" RENAME TO "temporary_employee"`); + await queryRunner.query( + `CREATE TABLE "employee" ("id" varchar PRIMARY KEY NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "tenantId" varchar, "organizationId" varchar, "valueDate" datetime, "isActive" boolean DEFAULT (1), "short_description" varchar(200), "description" varchar, "startedWorkOn" datetime, "endWork" datetime, "payPeriod" varchar, "billRateValue" integer, "billRateCurrency" varchar, "reWeeklyLimit" integer, "offerDate" datetime, "acceptDate" datetime, "rejectDate" datetime, "employeeLevel" varchar(500), "anonymousBonus" boolean, "averageIncome" numeric, "averageBonus" numeric, "totalWorkHours" numeric DEFAULT (0), "averageExpenses" numeric, "show_anonymous_bonus" boolean, "show_average_bonus" boolean, "show_average_expenses" boolean, "show_average_income" boolean, "show_billrate" boolean, "show_payperiod" boolean, "show_start_work_on" boolean, "isJobSearchActive" boolean, "linkedInUrl" varchar, "facebookUrl" varchar, "instagramUrl" varchar, "twitterUrl" varchar, "githubUrl" varchar, "gitlabUrl" varchar, "upworkUrl" varchar, "stackoverflowUrl" varchar, "isVerified" boolean, "isVetted" boolean, "totalJobs" numeric, "jobSuccess" numeric, "profile_link" varchar, "userId" varchar NOT NULL, "contactId" varchar, "organizationPositionId" varchar, "isTrackingEnabled" boolean DEFAULT (0), "deletedAt" datetime, "allowScreenshotCapture" boolean NOT NULL DEFAULT (1), "upworkId" varchar, "linkedInId" varchar, "isOnline" boolean DEFAULT (0), "isTrackingTime" boolean DEFAULT (0), "minimumBillingRate" integer, "isAway" boolean DEFAULT (0), "isArchived" boolean DEFAULT (0), "fix_relational_custom_fields" boolean, "archivedAt" datetime, CONSTRAINT "REL_1c0c1370ecd98040259625e17e" UNIQUE ("contactId"), CONSTRAINT "REL_f4b0d329c4a3cf79ffe9d56504" UNIQUE ("userId"), CONSTRAINT "FK_5e719204dcafa8d6b2ecdeda130" FOREIGN KEY ("organizationPositionId") REFERENCES "organization_position" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_1c0c1370ecd98040259625e17e2" FOREIGN KEY ("contactId") REFERENCES "contact" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_f4b0d329c4a3cf79ffe9d565047" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_c6a48286f3aa8ae903bee0d1e72" FOREIGN KEY ("organizationId") REFERENCES "organization" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_4b3303a6b7eb92d237a4379734e" FOREIGN KEY ("tenantId") REFERENCES "tenant" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "employee"("id", "createdAt", "updatedAt", "tenantId", "organizationId", "valueDate", "isActive", "short_description", "description", "startedWorkOn", "endWork", "payPeriod", "billRateValue", "billRateCurrency", "reWeeklyLimit", "offerDate", "acceptDate", "rejectDate", "employeeLevel", "anonymousBonus", "averageIncome", "averageBonus", "totalWorkHours", "averageExpenses", "show_anonymous_bonus", "show_average_bonus", "show_average_expenses", "show_average_income", "show_billrate", "show_payperiod", "show_start_work_on", "isJobSearchActive", "linkedInUrl", "facebookUrl", "instagramUrl", "twitterUrl", "githubUrl", "gitlabUrl", "upworkUrl", "stackoverflowUrl", "isVerified", "isVetted", "totalJobs", "jobSuccess", "profile_link", "userId", "contactId", "organizationPositionId", "isTrackingEnabled", "deletedAt", "allowScreenshotCapture", "upworkId", "linkedInId", "isOnline", "isTrackingTime", "minimumBillingRate", "isAway", "isArchived", "fix_relational_custom_fields", "archivedAt") SELECT "id", "createdAt", "updatedAt", "tenantId", "organizationId", "valueDate", "isActive", "short_description", "description", "startedWorkOn", "endWork", "payPeriod", "billRateValue", "billRateCurrency", "reWeeklyLimit", "offerDate", "acceptDate", "rejectDate", "employeeLevel", "anonymousBonus", "averageIncome", "averageBonus", "totalWorkHours", "averageExpenses", "show_anonymous_bonus", "show_average_bonus", "show_average_expenses", "show_average_income", "show_billrate", "show_payperiod", "show_start_work_on", "isJobSearchActive", "linkedInUrl", "facebookUrl", "instagramUrl", "twitterUrl", "githubUrl", "gitlabUrl", "upworkUrl", "stackoverflowUrl", "isVerified", "isVetted", "totalJobs", "jobSuccess", "profile_link", "userId", "contactId", "organizationPositionId", "isTrackingEnabled", "deletedAt", "allowScreenshotCapture", "upworkId", "linkedInId", "isOnline", "isTrackingTime", "minimumBillingRate", "isAway", "isArchived", "fix_relational_custom_fields", "archivedAt" FROM "temporary_employee"` + ); + await queryRunner.query(`DROP TABLE "temporary_employee"`); + await queryRunner.query(`CREATE INDEX "IDX_175b7be641928a31521224daa8" ON "employee" ("isArchived") `); + await queryRunner.query(`CREATE INDEX "IDX_510cb87f5da169e57e694d1a5c" ON "employee" ("isActive") `); + await queryRunner.query(`CREATE INDEX "IDX_4b3303a6b7eb92d237a4379734" ON "employee" ("tenantId") `); + await queryRunner.query(`CREATE INDEX "IDX_c6a48286f3aa8ae903bee0d1e7" ON "employee" ("organizationId") `); + await queryRunner.query(`CREATE INDEX "IDX_96dfbcaa2990df01fe5bb39ccc" ON "employee" ("profile_link") `); + await queryRunner.query(`CREATE INDEX "IDX_f4b0d329c4a3cf79ffe9d56504" ON "employee" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_1c0c1370ecd98040259625e17e" ON "employee" ("contactId") `); + await queryRunner.query( + `CREATE INDEX "IDX_5e719204dcafa8d6b2ecdeda13" ON "employee" ("organizationPositionId") ` + ); + } + + /** + * MySQL Up Migration + * + * @param queryRunner + */ + public async mysqlUpQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`employee\` ADD \`allowManualTime\` tinyint NOT NULL DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`employee\` ADD \`allowModifyTime\` tinyint NOT NULL DEFAULT 0`); + await queryRunner.query(`ALTER TABLE \`employee\` ADD \`allowDeleteTime\` tinyint NOT NULL DEFAULT 0`); + } + + /** + * MySQL Down Migration + * + * @param queryRunner + */ + public async mysqlDownQueryRunner(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`employee\` DROP COLUMN \`allowDeleteTime\``); + await queryRunner.query(`ALTER TABLE \`employee\` DROP COLUMN \`allowModifyTime\``); + await queryRunner.query(`ALTER TABLE \`employee\` DROP COLUMN \`allowManualTime\``); + } +} diff --git a/packages/core/src/employee/commands/handlers/employee.update.handler.ts b/packages/core/src/employee/commands/handlers/employee.update.handler.ts index 95cfa25641c..e43379d0624 100644 --- a/packages/core/src/employee/commands/handlers/employee.update.handler.ts +++ b/packages/core/src/employee/commands/handlers/employee.update.handler.ts @@ -7,10 +7,7 @@ import { RequestContext } from './../../../core/context'; @CommandHandler(EmployeeUpdateCommand) export class EmployeeUpdateHandler implements ICommandHandler { - - constructor( - private readonly _employeeService: EmployeeService, - ) { } + constructor(private readonly _employeeService: EmployeeService) {} public async execute(command: EmployeeUpdateCommand): Promise { const { id, input } = command; diff --git a/packages/core/src/employee/dto/update-employee.dto.ts b/packages/core/src/employee/dto/update-employee.dto.ts index 5c9c2e3567a..8313cff2363 100644 --- a/packages/core/src/employee/dto/update-employee.dto.ts +++ b/packages/core/src/employee/dto/update-employee.dto.ts @@ -31,7 +31,10 @@ export class UpdateEmployeeDTO 'isTrackingEnabled', 'isTrackingTime', 'isJobSearchActive', - 'allowScreenshotCapture' + 'allowScreenshotCapture', + 'allowManualTime', + 'allowModifyTime', + 'allowDeleteTime' ] as const) ) implements IEmployeeUpdateInput {} diff --git a/packages/core/src/employee/employee.entity.ts b/packages/core/src/employee/employee.entity.ts index a95c7117774..043079d7729 100644 --- a/packages/core/src/employee/employee.entity.ts +++ b/packages/core/src/employee/employee.entity.ts @@ -376,6 +376,36 @@ export class Employee extends TenantOrganizationBaseEntity implements IEmployee, @MultiORMColumn({ default: true }) allowScreenshotCapture?: boolean; + /** + * Indicates whether manual time entry is allowed for time tracking + * for a specific employee. + */ + @ApiPropertyOptional({ type: () => Boolean }) + @IsOptional() + @IsBoolean() + @MultiORMColumn({ default: false }) + allowManualTime?: boolean; + + /** + * Indicates whether modification of time entries is allowed for time tracking + * for a specific employee. + */ + @ApiPropertyOptional({ type: () => Boolean }) + @IsOptional() + @IsBoolean() + @MultiORMColumn({ default: false }) + allowModifyTime?: boolean; + + /** + * Indicates whether deletion of time entries is allowed for time tracking + * for a specific employee. + */ + @ApiPropertyOptional({ type: () => Boolean }) + @IsOptional() + @IsBoolean() + @MultiORMColumn({ default: false }) + allowDeleteTime?: boolean; + /** Upwork ID */ @ApiPropertyOptional({ type: () => String }) @IsOptional() diff --git a/packages/core/src/employee/employee.service.ts b/packages/core/src/employee/employee.service.ts index 5fc7b8a1d3e..c716fa91f87 100644 --- a/packages/core/src/employee/employee.service.ts +++ b/packages/core/src/employee/employee.service.ts @@ -449,6 +449,9 @@ export class EmployeeService extends TenantAwareCrudService { isTrackingEnabled: true, deletedAt: true, allowScreenshotCapture: true, + allowManualTime: true, + allowModifyTime: true, + allowDeleteTime: true, isActive: true, isArchived: true, isAway: true, diff --git a/packages/core/src/organization-sprint/organization-sprint.entity.ts b/packages/core/src/organization-sprint/organization-sprint.entity.ts index 1637d1b668d..df195e7720e 100644 --- a/packages/core/src/organization-sprint/organization-sprint.entity.ts +++ b/packages/core/src/organization-sprint/organization-sprint.entity.ts @@ -1,6 +1,7 @@ import { JoinColumn } from 'typeorm'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsDate, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; +import { Type } from 'class-transformer'; import { isMySQL, isPostgres } from '@gauzy/config'; import { ID, @@ -56,12 +57,14 @@ export class OrganizationSprint extends TenantOrganizationBaseEntity implements @ApiPropertyOptional({ type: () => Date }) @IsDate() @IsOptional() + @Type(() => Date) @MultiORMColumn({ nullable: true }) startDate?: Date; @ApiPropertyOptional({ type: () => Date }) @IsDate() @IsOptional() + @Type(() => Date) @MultiORMColumn({ nullable: true }) endDate?: Date; diff --git a/packages/core/src/resource-link/resource-link.entity.ts b/packages/core/src/resource-link/resource-link.entity.ts index 0a4e79f5928..4409e0aefcb 100644 --- a/packages/core/src/resource-link/resource-link.entity.ts +++ b/packages/core/src/resource-link/resource-link.entity.ts @@ -12,7 +12,7 @@ import { MikroOrmResourceLinkRepository } from './repository/mikro-orm-resource- export class ResourceLink extends TenantOrganizationBaseEntity implements IResourceLink { [EntityRepositoryType]?: MikroOrmResourceLinkRepository; - @ApiProperty({ type: () => String, enum: BaseEntityEnum }) + @ApiProperty({ enum: BaseEntityEnum }) @IsNotEmpty() @IsEnum(BaseEntityEnum) @ColumnIndex() @@ -38,14 +38,9 @@ export class ResourceLink extends TenantOrganizationBaseEntity implements IResou @MultiORMColumn({ type: 'text' }) url: string; - @ApiPropertyOptional({ - type: () => (isSqlite() || isBetterSqlite3() ? String : Object) - }) + @ApiPropertyOptional({ type: () => (isSqlite() || isBetterSqlite3() ? String : Object) }) @IsOptional() - @MultiORMColumn({ - nullable: true, - type: isSqlite() || isBetterSqlite3() ? 'text' : 'json' - }) + @MultiORMColumn({ nullable: true, type: isSqlite() || isBetterSqlite3() ? 'text' : 'json' }) metaData?: string | IURLMetaData; /* @@ -53,12 +48,9 @@ export class ResourceLink extends TenantOrganizationBaseEntity implements IResou | @ManyToOne |-------------------------------------------------------------------------- */ - /** - * User comment author + * User Author of the Resource Link */ - @ApiPropertyOptional({ type: () => Object }) - @IsOptional() @MultiORMManyToOne(() => User, { /** Indicates if relation column value can be nullable or not. */ nullable: true, @@ -69,11 +61,8 @@ export class ResourceLink extends TenantOrganizationBaseEntity implements IResou @JoinColumn() creator?: IUser; - @ApiPropertyOptional({ type: () => String }) - @IsOptional() - @IsUUID() @RelationId((it: ResourceLink) => it.creator) @ColumnIndex() @MultiORMColumn({ nullable: true, relationId: true }) - creatorId?: string; + creatorId?: ID; } diff --git a/packages/desktop-libs/src/lib/desktop-menu.ts b/packages/desktop-libs/src/lib/desktop-menu.ts index 68ccf3658b3..a439701eed0 100644 --- a/packages/desktop-libs/src/lib/desktop-menu.ts +++ b/packages/desktop-libs/src/lib/desktop-menu.ts @@ -23,7 +23,7 @@ export class AppMenu { label: TranslateService.instant('MENU.ABOUT'), enabled: true, async click() { - const window: BrowserWindow = await createAboutWindow(windowPath.timeTrackerUi); + const window: BrowserWindow = await createAboutWindow(windowPath.timeTrackerUi, windowPath.preloadPath); window.show(); } }, diff --git a/packages/desktop-ui-lib/package.json b/packages/desktop-ui-lib/package.json index 783121698e7..958f25e547d 100644 --- a/packages/desktop-ui-lib/package.json +++ b/packages/desktop-ui-lib/package.json @@ -53,7 +53,7 @@ "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", "@swimlane/ngx-charts": "20.5.0", - "angular2-smart-table": "^3.2.0", + "angular2-smart-table": "^3.3.0", "ckeditor4-angular": "4.0.1", "electron-log": "^4.4.8", "electron-store": "^8.1.0", diff --git a/packages/desktop-ui-lib/src/lib/dialogs/about/about.component.html b/packages/desktop-ui-lib/src/lib/dialogs/about/about.component.html index 6314cb813f9..9f932d38438 100644 --- a/packages/desktop-ui-lib/src/lib/dialogs/about/about.component.html +++ b/packages/desktop-ui-lib/src/lib/dialogs/about/about.component.html @@ -19,11 +19,13 @@ Copyright © 2020-{{'FOOTER.PRESENT' | translate}} {{ application?.companyName}} -
    +
    {{'FOOTER.RIGHTS_RESERVED' | translate}} -
    - {{'FOOTER.TERMS_OF_SERVICE' | translate}} | - {{'FOOTER.PRIVACY_POLICY' | translate}} +
    + {{'FOOTER.TERMS_OF_SERVICE' | + translate}} | + {{'FOOTER.PRIVACY_POLICY' | + translate}}
    diff --git a/packages/desktop-ui-lib/src/lib/dialogs/about/about.component.scss b/packages/desktop-ui-lib/src/lib/dialogs/about/about.component.scss index 020389953df..cc210301002 100644 --- a/packages/desktop-ui-lib/src/lib/dialogs/about/about.component.scss +++ b/packages/desktop-ui-lib/src/lib/dialogs/about/about.component.scss @@ -46,4 +46,4 @@ div.logo { ::ng-deep nb-layout .layout .layout-container .content nb-layout-footer nav { padding: 0px; - } \ No newline at end of file +} diff --git a/packages/desktop-ui-lib/src/lib/theme/styles/theme.gauzy-dark.ts b/packages/desktop-ui-lib/src/lib/theme/styles/theme.gauzy-dark.ts index 4ec88895401..9bf0e771154 100644 --- a/packages/desktop-ui-lib/src/lib/theme/styles/theme.gauzy-dark.ts +++ b/packages/desktop-ui-lib/src/lib/theme/styles/theme.gauzy-dark.ts @@ -45,18 +45,12 @@ const theme = { export const GAUZY_DARK = { name: 'gauzy-dark', - base: 'dark', + base: 'dark', variables: { ...theme, temperature: { - arcFill: [ - theme.primary, - theme.primary, - theme.primary, - theme.primary, - theme.primary - ], + arcFill: Array(5).fill(theme.primary), arcEmpty: theme.bg2, thumbBg: theme.bg2, thumbBorder: theme.primary diff --git a/packages/desktop-window/src/lib/desktop-window-about.ts b/packages/desktop-window/src/lib/desktop-window-about.ts index 65b61879bcb..6f4a61a9631 100644 --- a/packages/desktop-window/src/lib/desktop-window-about.ts +++ b/packages/desktop-window/src/lib/desktop-window-about.ts @@ -1,7 +1,6 @@ import * as remoteMain from '@electron/remote/main'; import { BrowserWindow, Menu } from 'electron'; import * as url from 'url'; -import { attachTitlebarToWindow } from 'custom-electron-titlebar/main'; import log from 'electron-log'; import { WindowManager } from './concretes/window.manager'; @@ -16,6 +15,13 @@ export async function createAboutWindow(filePath, preloadPath?) { const mainWindowSettings: Electron.BrowserWindowConstructorOptions = windowSetting(preloadPath); const manager = WindowManager.getInstance(); + const allwindows = BrowserWindow.getAllWindows(); + const aboutWindows = allwindows.find((win) => win.getTitle() === 'About'); + if (aboutWindows) { + aboutWindows.show(); + return aboutWindows; + } + const window = new BrowserWindow(mainWindowSettings); remoteMain.enable(window.webContents); const launchPath = url.format({ @@ -42,7 +48,7 @@ export async function createAboutWindow(filePath, preloadPath?) { manager.register(RegisteredWindow.ABOUT, window); if (preloadPath) { - attachTitlebarToWindow(window); + window.webContents.send('hide-menu'); } return window; } @@ -69,11 +75,6 @@ const windowSetting = (preloadPath) => { }; if (preloadPath) { mainWindowSettings.webPreferences.preload = preloadPath; - mainWindowSettings.titleBarOverlay = true; - mainWindowSettings.titleBarStyle = 'hidden'; - if (process.platform === 'linux') { - mainWindowSettings.frame = false; - } } return mainWindowSettings; }; diff --git a/packages/desktop-window/src/lib/desktop-window-timer.ts b/packages/desktop-window/src/lib/desktop-window-timer.ts index 35eb87ad7b7..53baaf5c2b5 100644 --- a/packages/desktop-window/src/lib/desktop-window-timer.ts +++ b/packages/desktop-window/src/lib/desktop-window-timer.ts @@ -12,6 +12,13 @@ Object.assign(console, log.functions); const Store = require('electron-store'); const store = new Store(); +function getScreenSize() { + const sizes = screen.getPrimaryDisplay().workAreaSize; + const width = sizes.height < 768 ? 310 : 360; + const height = sizes.height < 768 ? sizes.height - 20 : 768; + return { width, height } +} + export async function createTimeTrackerWindow(timeTrackerWindow, filePath, preloadPath?) { const mainWindowSettings: Electron.BrowserWindowConstructorOptions = windowSetting(preloadPath); const manager = WindowManager.getInstance(); @@ -29,6 +36,11 @@ export async function createTimeTrackerWindow(timeTrackerWindow, filePath, prelo timeTrackerWindow.hide(); await timeTrackerWindow.loadURL(launchPath); + if (preloadPath) { + attachTitlebarToWindow(timeTrackerWindow); + } + const { width, height } = getScreenSize(); + timeTrackerWindow.setMinimumSize(width, height); timeTrackerWindow.setMenu(null); timeTrackerWindow.on('close', (event) => { event.preventDefault(); @@ -36,17 +48,14 @@ export async function createTimeTrackerWindow(timeTrackerWindow, filePath, prelo }); manager.register(RegisteredWindow.TIMER, timeTrackerWindow); - if (preloadPath) { - attachTitlebarToWindow(timeTrackerWindow); - } + return timeTrackerWindow; } const windowSetting = (preloadPath?) => { const sizes = screen.getPrimaryDisplay().workAreaSize; - const height = sizes.height < 768 ? sizes.height - 20 : 768; + const { width, height } = getScreenSize(); const zoomF = sizes.height < 768 ? 0.8 : 1.0; - const width = sizes.height < 768 ? 310 : 360; const filesPath = store.get('filePath'); console.log('file path == ', filesPath); const mainWindowSettings: Electron.BrowserWindowConstructorOptions = { diff --git a/packages/plugins/integration-github-ui/package.json b/packages/plugins/integration-github-ui/package.json index abd3c3c0ece..3b0ee5d10be 100644 --- a/packages/plugins/integration-github-ui/package.json +++ b/packages/plugins/integration-github-ui/package.json @@ -44,7 +44,7 @@ "@nebular/theme": "^12.0.0", "@ngneat/until-destroy": "^9.2.0", "@ngx-translate/core": "^15.0.0", - "angular2-smart-table": "^3.2.0", + "angular2-smart-table": "^3.3.0", "ngx-permissions": "^13.0.1", "rxjs": "^7.4.0", "tslib": "^2.6.2" diff --git a/packages/plugins/integration-github-ui/src/lib/components/view/view.component.ts b/packages/plugins/integration-github-ui/src/lib/components/view/view.component.ts index f00ab6373b0..8c7856a5ff4 100644 --- a/packages/plugins/integration-github-ui/src/lib/components/view/view.component.ts +++ b/packages/plugins/integration-github-ui/src/lib/components/view/view.component.ts @@ -35,7 +35,7 @@ import { ProjectComponent, GithubRepositoryComponent, GithubIssueTitleDescriptionComponent, - ToggleSwitchComponent, + ToggleSwitcherComponent, ResyncButtonComponent, PaginationFilterBaseComponent, IPaginationBase, @@ -402,17 +402,17 @@ export class GithubViewComponent extends PaginationFilterBaseComponent implement title: this.getTranslation('SM_TABLE.ENABLED_DISABLED_SYNC'), type: 'custom', isFilterable: false, - renderComponent: ToggleSwitchComponent, - componentInitFunction: (instance: ToggleSwitchComponent, cell: Cell) => { + renderComponent: ToggleSwitcherComponent, + componentInitFunction: (instance: ToggleSwitcherComponent, cell: Cell) => { // Get the data of the entire row const rowData = cell.getRow().getData(); - // Set properties on the ToggleSwitchComponent instance + // Set properties on the ToggleSwitcherComponent instance instance.rowData = rowData; instance.value = rowData?.customFields?.repository?.hasSyncEnabled || false; - // Subscribe to the 'switched' event of the ToggleSwitchComponent - instance.switched.subscribe({ + // Subscribe to the 'switched' event of the ToggleSwitcherComponent + instance.onSwitched.subscribe({ // When the switch state changes, execute the following callback next: (hasSyncEnabled: boolean) => { // Call the 'updateGithubRepository' method with the row data and the new switch state diff --git a/packages/plugins/integration-hubstaff-ui/package.json b/packages/plugins/integration-hubstaff-ui/package.json index 867c79437d1..b1e41fbca29 100644 --- a/packages/plugins/integration-hubstaff-ui/package.json +++ b/packages/plugins/integration-hubstaff-ui/package.json @@ -47,7 +47,7 @@ "@ng-select/ng-select": "^11.2.0", "@ngneat/until-destroy": "^9.2.0", "@ngx-translate/core": "^15.0.0", - "angular2-smart-table": "^3.2.0", + "angular2-smart-table": "^3.3.0", "ngx-permissions": "^13.0.1", "rxjs": "^7.4.0", "tslib": "^2.6.2" diff --git a/packages/plugins/integration-upwork-ui/package.json b/packages/plugins/integration-upwork-ui/package.json index f0e38980ce0..31404f2ceb7 100644 --- a/packages/plugins/integration-upwork-ui/package.json +++ b/packages/plugins/integration-upwork-ui/package.json @@ -44,7 +44,7 @@ "@nebular/theme": "^12.0.0", "@ngneat/until-destroy": "^9.2.0", "@ngx-translate/core": "^15.0.0", - "angular2-smart-table": "^3.2.0", + "angular2-smart-table": "^3.3.0", "ngx-permissions": "^13.0.1", "rxjs": "^7.4.0", "tslib": "^2.6.2" diff --git a/packages/plugins/job-employee-ui/package.json b/packages/plugins/job-employee-ui/package.json index 7a5cd6c74c2..b0a0ae8d4fd 100644 --- a/packages/plugins/job-employee-ui/package.json +++ b/packages/plugins/job-employee-ui/package.json @@ -44,7 +44,7 @@ "@ngneat/until-destroy": "^9.2.0", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", - "angular2-smart-table": "^3.2.0", + "angular2-smart-table": "^3.3.0", "ngx-permissions": "^13.0.1", "rxjs": "^7.4.0", "tslib": "^2.6.2" diff --git a/packages/plugins/job-employee-ui/src/lib/components/job-employee/job-employee.component.html b/packages/plugins/job-employee-ui/src/lib/components/job-employee/job-employee.component.html index 5578d94fe8c..1a25595f902 100644 --- a/packages/plugins/job-employee-ui/src/lib/components/job-employee/job-employee.component.html +++ b/packages/plugins/job-employee-ui/src/lib/components/job-employee/job-employee.component.html @@ -32,6 +32,7 @@

    [settings]="settingsSmartTable" [source]="smartTableSource" (editConfirm)="onEditConfirm($event)" + (editCancel)="onEditCancel($event)" (userRowSelect)="onSelectEmployee($event)" > diff --git a/packages/plugins/job-employee-ui/src/lib/components/job-employee/job-employee.component.ts b/packages/plugins/job-employee-ui/src/lib/components/job-employee/job-employee.component.ts index f18f17bc82f..2dba511bcb6 100644 --- a/packages/plugins/job-employee-ui/src/lib/components/job-employee/job-employee.component.ts +++ b/packages/plugins/job-employee-ui/src/lib/components/job-employee/job-employee.component.ts @@ -10,17 +10,17 @@ import { TranslateService } from '@ngx-translate/core'; import { Cell } from 'angular2-smart-table'; import { NgxPermissionsService } from 'ngx-permissions'; import { ID, IEmployee, IOrganization, LanguagesEnum, PermissionsEnum } from '@gauzy/contracts'; -import { API_PREFIX, distinctUntilChange } from '@gauzy/ui-core/common'; +import { API_PREFIX, distinctUntilChange, isNotNullOrUndefined } from '@gauzy/ui-core/common'; import { PageDataTableRegistryService, EmployeesService, - JobService, ServerDataSource, Store, ToastrService, PageTabsetRegistryId, PageTabRegistryService, - PageDataTableRegistryId + PageDataTableRegistryId, + JobSearchStoreService } from '@gauzy/ui-core/core'; import { I18nService } from '@gauzy/ui-core/i18n'; import { @@ -29,7 +29,9 @@ import { NumberEditorComponent, EmployeeLinkEditorComponent, PaginationFilterBaseComponent, - SmartTableToggleComponent + NonEditableNumberEditorComponent, + JobSearchAvailabilityEditorComponent, + ToggleSwitcherComponent } from '@gauzy/ui-core/shared'; /** @@ -74,7 +76,7 @@ export class JobEmployeeComponent extends PaginationFilterBaseComponent implemen private readonly _ngxPermissionsService: NgxPermissionsService, private readonly _store: Store, private readonly _employeesService: EmployeesService, - private readonly _jobService: JobService, + private readonly _jobSearchStoreService: JobSearchStoreService, private readonly _toastrService: ToastrService, private readonly _currencyPipe: CurrencyPipe, private readonly _i18nService: I18nService, @@ -206,7 +208,7 @@ export class JobEmployeeComponent extends PaginationFilterBaseComponent implemen registerDataTableColumns(_pageDataTableRegistryService: PageDataTableRegistryService): void { // Register the data table column _pageDataTableRegistryService.registerPageDataTableColumn({ - dataTableId: 'job-employee', // The identifier for the data table location + dataTableId: this.dataTableId, // The identifier for the data table location columnId: 'name', // The identifier for the column order: 0, // The order of the column in the table title: () => this.getTranslation('JOB_EMPLOYEE.EMPLOYEE'), // The title of the column @@ -230,7 +232,7 @@ export class JobEmployeeComponent extends PaginationFilterBaseComponent implemen // Register the data table column _pageDataTableRegistryService.registerPageDataTableColumn({ - dataTableId: 'job-employee', // The identifier for the data table location + dataTableId: this.dataTableId, // The identifier for the data table location columnId: 'availableJobs', // The identifier for the column order: 1, // The order of the column in the table title: () => this.getTranslation('JOB_EMPLOYEE.AVAILABLE_JOBS'), // The title of the column @@ -238,12 +240,16 @@ export class JobEmployeeComponent extends PaginationFilterBaseComponent implemen width: '10%', // The width of the column isSortable: false, // Indicates whether the column is sortable isEditable: false, // Indicates whether the column is editable - valuePrepareFunction: (rawValue: any) => rawValue || 0 + valuePrepareFunction: (rawValue: any) => (isNotNullOrUndefined(rawValue) ? rawValue : 0), + editor: { + type: 'custom', + component: NonEditableNumberEditorComponent + } }); // Register the data table column _pageDataTableRegistryService.registerPageDataTableColumn({ - dataTableId: 'job-employee', // The identifier for the data table location + dataTableId: this.dataTableId, // The identifier for the data table location columnId: 'appliedJobs', // The identifier for the column order: 2, // The order of the column in the table title: () => this.getTranslation('JOB_EMPLOYEE.APPLIED_JOBS'), // The title of the column @@ -251,12 +257,16 @@ export class JobEmployeeComponent extends PaginationFilterBaseComponent implemen width: '10%', // The width of the column isSortable: false, // Indicates whether the column is sortable isEditable: false, // Indicates whether the column is editable - valuePrepareFunction: (rawValue: any) => rawValue || 0 + valuePrepareFunction: (rawValue: any) => (isNotNullOrUndefined(rawValue) ? rawValue : 0), + editor: { + type: 'custom', + component: NonEditableNumberEditorComponent + } }); // Register the data table column _pageDataTableRegistryService.registerPageDataTableColumn({ - dataTableId: 'job-employee', // The identifier for the data table location + dataTableId: this.dataTableId, // The identifier for the data table location columnId: 'billRateValue', // The identifier for the column order: 3, // The order of the column in the table title: () => this.getTranslation('JOB_EMPLOYEE.BILLING_RATE'), // The title of the column @@ -278,7 +288,7 @@ export class JobEmployeeComponent extends PaginationFilterBaseComponent implemen // Register the data table column _pageDataTableRegistryService.registerPageDataTableColumn({ - dataTableId: 'job-employee', // The identifier for the data table location + dataTableId: this.dataTableId, // The identifier for the data table location columnId: 'minimumBillingRate', // The identifier for the column order: 4, // The order of the column in the table title: () => this.getTranslation('JOB_EMPLOYEE.MINIMUM_BILLING_RATE'), // The title of the column @@ -298,26 +308,33 @@ export class JobEmployeeComponent extends PaginationFilterBaseComponent implemen // Register the data table column _pageDataTableRegistryService.registerPageDataTableColumn({ - dataTableId: 'job-employee', // The identifier for the data table location + dataTableId: this.dataTableId, // The identifier for the data table location columnId: 'isJobSearchActive', // The identifier for the column order: 5, // The order of the column in the table title: () => this.getTranslation('JOB_EMPLOYEE.JOB_SEARCH_STATUS'), // The title of the column type: 'custom', // The type of the column width: '20%', // The width of the column isSortable: false, // Indicates whether the column is sortable - isEditable: false, // Indicates whether the column is editable - renderComponent: SmartTableToggleComponent, - componentInitFunction: (instance: SmartTableToggleComponent, cell: Cell) => { + isEditable: true, // Indicates whether the column is editable + renderComponent: ToggleSwitcherComponent, + componentInitFunction: (instance: ToggleSwitcherComponent, cell: Cell) => { // Get the employee data from the cell const employee: IEmployee = cell.getRow().getData(); + // Set the label property to false to hide the label + instance.label = false; // Set the initial value of the toggle instance.value = employee.isJobSearchActive; // Subscribe to the toggleChange event - instance.toggleChange.pipe(untilDestroyed(this)).subscribe((toggle: boolean) => { - this.updateJobSearchAvailability(employee, toggle); + instance.onSwitched.subscribe((toggle: boolean) => { + // Call the JobSearchStoreService to update the job search availability + this._jobSearchStoreService.updateJobSearchAvailability(this.organization, employee, toggle); }); + }, + editor: { + type: 'custom', + component: JobSearchAvailabilityEditorComponent } }); } @@ -526,39 +543,18 @@ export class JobEmployeeComponent extends PaginationFilterBaseComponent implemen } /** - * Updates the job search availability status of an employee within the organization. - * @param employee - The employee object to update. - * @param isJobSearchActive - A boolean flag indicating whether the job search is active. - * @returns {Promise} - A Promise resolving to void. + * Handles the cancellation of an edit operation in the smart table. + * Refreshes the data table to reflect any changes made. + * + * @param event - The event object containing details about the canceled edit. + * */ - async updateJobSearchAvailability(employee: IEmployee, isJobSearchActive: boolean): Promise { - try { - // Ensure the organization context is available before proceeding. - if (!this.organization) { - return; - } - - // Destructure organization properties for clarity. - const { id: organizationId, tenantId } = this.organization; - - // Update the job search status using the employeesService. - await this._jobService.updateJobSearchStatus(employee.id, { - isJobSearchActive, - organizationId, - tenantId - }); + onEditCancel(event: any): void { + // Optionally, you can log the event for debugging purposes + console.log('Edit canceled for row:', event); - // Display a success toastr notification based on the job search status. - const toastrMessageKey = isJobSearchActive - ? 'TOASTR.MESSAGE.EMPLOYEE_JOB_STATUS_ACTIVE' - : 'TOASTR.MESSAGE.EMPLOYEE_JOB_STATUS_INACTIVE'; - - const fullName = employee.fullName.trim() || 'Unknown Employee'; - this._toastrService.success(toastrMessageKey, { name: fullName }); - } catch (error) { - // Display an error toastr notification in case of any exceptions. - this._toastrService.danger(error); - } + // Refresh the data table to revert any changes made during the edit + this.smartTableSource.refresh(); } /** diff --git a/packages/plugins/job-proposal-ui/package.json b/packages/plugins/job-proposal-ui/package.json index 4df54349620..c4b3a6973ae 100644 --- a/packages/plugins/job-proposal-ui/package.json +++ b/packages/plugins/job-proposal-ui/package.json @@ -45,7 +45,7 @@ "@ng-select/ng-select": "^11.2.0", "@ngneat/until-destroy": "^9.2.0", "@ngx-translate/core": "^15.0.0", - "angular2-smart-table": "^3.2.0", + "angular2-smart-table": "^3.3.0", "ckeditor4-angular": "4.0.1", "moment": "^2.30.1", "ngx-infinite-scroll": "^16.0.0", diff --git a/packages/plugins/job-search-ui/package.json b/packages/plugins/job-search-ui/package.json index d69f015cf3f..147fe8dfda2 100644 --- a/packages/plugins/job-search-ui/package.json +++ b/packages/plugins/job-search-ui/package.json @@ -45,7 +45,7 @@ "@ngneat/until-destroy": "^9.2.0", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", - "angular2-smart-table": "^3.2.0", + "angular2-smart-table": "^3.3.0", "ckeditor4-angular": "4.0.1", "ng2-file-upload": "^5.0.0", "ngx-moment": "^6.0.2", diff --git a/packages/ui-config/src/lib/environments/model.ts b/packages/ui-config/src/lib/environments/model.ts index bece11decb0..41960b5ebd2 100644 --- a/packages/ui-config/src/lib/environments/model.ts +++ b/packages/ui-config/src/lib/environments/model.ts @@ -3,6 +3,7 @@ export interface Environment { API_BASE_URL: string; CLIENT_BASE_URL: string; + COOKIE_DOMAIN?: string; PLATFORM_WEBSITE_URL?: string; PLATFORM_WEBSITE_DOWNLOAD_URL?: string; diff --git a/packages/ui-core/common/src/lib/constants/index.ts b/packages/ui-core/common/src/lib/constants/index.ts index fff2f7aa02a..98f36d229c5 100644 --- a/packages/ui-core/common/src/lib/constants/index.ts +++ b/packages/ui-core/common/src/lib/constants/index.ts @@ -1,3 +1,4 @@ export * from './app.constants'; export * from './layout.constants'; +export * from './route.constant'; export * from './timesheet.constants'; diff --git a/packages/ui-core/common/src/lib/constants/route.constant.ts b/packages/ui-core/common/src/lib/constants/route.constant.ts new file mode 100644 index 00000000000..6bd97cfca14 --- /dev/null +++ b/packages/ui-core/common/src/lib/constants/route.constant.ts @@ -0,0 +1,4 @@ +// In a constants file or configuration service +export const ROUTES = { + DASHBOARD: '/pages/dashboard' +} as const; diff --git a/packages/ui-core/core/src/index.ts b/packages/ui-core/core/src/index.ts index b6be0396738..38d20e147f1 100644 --- a/packages/ui-core/core/src/index.ts +++ b/packages/ui-core/core/src/index.ts @@ -5,6 +5,7 @@ export * from './lib/auth'; export * from './lib/common/component-registry.types'; export * from './lib/components'; export * from './lib/core.module'; +export * from './lib/extension'; export * from './lib/guards'; export * from './lib/interceptors'; export * from './lib/module-import-guard'; diff --git a/packages/ui-core/core/src/lib/auth/auth.guard.ts b/packages/ui-core/core/src/lib/auth/auth.guard.ts index 071cd7f8562..14802e45f08 100644 --- a/packages/ui-core/core/src/lib/auth/auth.guard.ts +++ b/packages/ui-core/core/src/lib/auth/auth.guard.ts @@ -1,16 +1,17 @@ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; import { firstValueFrom } from 'rxjs'; import { AuthService, AuthStrategy, ElectronService, Store } from '../services'; +import { getCookie } from './cookie-helper'; @Injectable() -export class AuthGuard implements CanActivate { +export class AuthGuard { constructor( - private readonly router: Router, - private readonly authService: AuthService, - private readonly authStrategy: AuthStrategy, - private readonly store: Store, - private readonly electronService: ElectronService + private readonly _router: Router, + private readonly _authService: AuthService, + private readonly _authStrategy: AuthStrategy, + private readonly _store: Store, + private readonly _electronService: ElectronService ) {} /** @@ -21,20 +22,30 @@ export class AuthGuard implements CanActivate { * @return {Promise} A promise that resolves to true if the user is authenticated, false otherwise. */ async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { - const token = route.queryParamMap.get('token'); - const userId = route.queryParamMap.get('userId'); + const token = route.queryParamMap.get('token') || getCookie('token'); + const userId = route.queryParamMap.get('userId') || getCookie('userId'); + const refreshToken = route.queryParamMap.get('refresh_token') || getCookie('refresh_token'); + // If token and userId exist, store them if (token && userId) { - this.store.token = token; - this.store.userId = userId; + this._store.token = token; + this._store.userId = userId; + this._store.refresh_token = refreshToken; } - if (await this.authService.isAuthenticated()) { - // Logged in, so allow navigation - return true; + // Validate the token before proceeding + if (token && !this.validateToken(token)) { + console.error('Invalid token, redirecting to login page...'); + await this.handleLogout(state.url); // Handle invalid token + return false; // Prevent navigation } - // Not logged in, handle the logout process + // Check if the user is authenticated + if (await this._authService.isAuthenticated()) { + return true; // Allow navigation + } + + // Not authenticated, handle logout await this.handleLogout(state.url); return false; } @@ -45,15 +56,25 @@ export class AuthGuard implements CanActivate { * @param {string} returnUrl - The URL to return to after logging in. */ private async handleLogout(returnUrl: string): Promise { - if (this.electronService.isElectron) { + if (this._electronService.isElectron) { try { - this.electronService.ipcRenderer.send('logout'); + this._electronService.ipcRenderer.send('logout'); } catch (error) { console.error('Error sending logout message to Electron:', error); } } - await firstValueFrom(this.authStrategy.logout()); - await this.router.navigate(['/auth/login'], { queryParams: { returnUrl } }); + await firstValueFrom(this._authStrategy.logout()); + await this._router.navigate(['/auth/login'], { queryParams: { returnUrl } }); + } + + /** + * Validates the format of a JWT token. + * + * @param {string} token - The JWT token to validate. + * @returns {boolean} - Returns true if the token is valid, otherwise false. + */ + private validateToken(token: string): boolean { + return typeof token === 'string' && token.trim().length > 0 && token.split('.').length === 3; } } diff --git a/packages/ui-core/core/src/lib/auth/auth.module.ts b/packages/ui-core/core/src/lib/auth/auth.module.ts index be9ed870a50..d80b7c158f3 100644 --- a/packages/ui-core/core/src/lib/auth/auth.module.ts +++ b/packages/ui-core/core/src/lib/auth/auth.module.ts @@ -6,6 +6,9 @@ import { AuthGuard } from './auth.guard'; import { NoAuthGuard } from './no-auth.guard'; import { AuthService, AuthStrategy, ElectronService, Store } from '../services'; +/** + * Social links for auth + */ const socialLinks = [ { url: environment.GOOGLE_AUTH_LINK, diff --git a/packages/ui-core/core/src/lib/auth/cookie-helper.ts b/packages/ui-core/core/src/lib/auth/cookie-helper.ts new file mode 100644 index 00000000000..be0a3cc28d9 --- /dev/null +++ b/packages/ui-core/core/src/lib/auth/cookie-helper.ts @@ -0,0 +1,174 @@ +import { environment } from '@gauzy/ui-config'; + +/** + * Retrieves the value of a cookie by its name for the current domain and its subdomains. + * + * @param {string} name - The name of the cookie to retrieve. + * @return {string | null} - The value of the cookie if found, or null if not found. + */ +export function getCookie(name: string): string | null { + if (!name || typeof name !== 'string') { + return null; + } + + // Sanitize the cookie name + const sanitizedName = encodeURIComponent(name); + const value = `; ${document.cookie}`; // Get all cookies as a string and add a leading semicolon + const parts = value.split(`; ${sanitizedName}=`); // Split the string by the desired cookie name + + // If the cookie is found, split to isolate its value and return it + if (parts.length === 2) { + const cookie = parts.pop()?.split(';').shift() || null; // Get the cookie value + + // Validate if the cookie is set for the current domain or its subdomains + if (isCookieForValidDomain(cookie)) { + return decodeURIComponent(cookie); // Return the cookie value if it's for a valid domain + } + } + + // Return null if the cookie is not found + return null; +} + +/** + * Checks if the cookie is set for the current domain, its subdomains, or localhost. + * + * @param {string} cookie - The value of the cookie to check. + * @return {boolean} - True if the cookie is considered valid, otherwise false. + */ +function isCookieForValidDomain(cookie: string | null): boolean { + // Check if the cookie is not null + if (cookie === null) { + return false; // Not valid if cookie does not exist + } + + // Get the current hostname + const hostname = window.location.hostname; // e.g., "demo.gauzy.co" or "app.gauzy.co" + + // Define allowed domains for each environment + const DOMAIN_CONFIG = { + production: ['gauzy.co', 'app.gauzy.co'], + demo: ['demo.gauzy.co'], + staging: ['staging.gauzy.co'], + development: ['localhost', '127.0.0.1'] + } as const; + + // Check for development environments + if (DOMAIN_CONFIG.development.includes(hostname as 'localhost' | '127.0.0.1')) { + return true; // Allow cookies for localhost and 127.0.0.1 + } + + // Get environment-specific domains + const validDomains = [...DOMAIN_CONFIG.production, ...DOMAIN_CONFIG.demo, ...DOMAIN_CONFIG.staging]; + + // More robust domain validation + return validDomains.some((domain: string) => { + // Convert hostname and domain to lowercase for case-insensitive comparison + const normalizedHostname = hostname.toLowerCase(); + const normalizedDomain = domain.toLowerCase(); + + // Check for exact match + if (normalizedHostname === normalizedDomain) { + return true; + } + + // Check if the hostname ends with the domain and ensure proper boundaries + if (normalizedHostname.endsWith(`.${normalizedDomain}`)) { + // Ensure there are no additional dots to prevent attacks + const subdomain = normalizedHostname.slice(0, -normalizedDomain.length - 1); + return !subdomain.includes('.'); + } + + // Prevent direct domain spoofing by checking if it matches the exact domain + if (normalizedHostname === `www.${normalizedDomain}`) { + return true; + } + + return false; // Invalid if none of the checks pass + }); +} + +/** + * Prepares cookie options with default values and overrides. + * + * @param {Object} [options={}] - Additional options to customize the cookie settings. + * @param {string} [options.path='/'] - The path where the cookie is accessible. + * @param {string} [options.SameSite='Lax'] - SameSite attribute to control cookie sharing across sites. + * @param {boolean} [options.Secure] - If true, the cookie will only be sent over HTTPS. + * @param {string} [options.domain] - The domain for which the cookie is valid. + * @returns {Object} The final cookie options object with defaults applied. + */ +function prepareCookieOptions(options: { [key: string]: any } = {}): { [key: string]: any } { + // Prepare cookie options with defaults + const cookieOptions = { + path: '/', // Default path for all cookies + SameSite: 'Lax', // Prevent CSRF attacks + Secure: window.location.protocol === 'https:', // Send only over HTTPS + ...options // Spread existing options + }; + + // Cache hostname lookup to avoid repeated access to window.location.hostname + const getCurrentHostname = (() => { + let hostname: string; + return () => (hostname ??= window.location.hostname); + })(); + + // Get current host name + const hostname = getCurrentHostname(); + if (hostname === 'localhost' || hostname === '127.0.0.1') { + cookieOptions['domain'] = undefined; // Don't set the domain for localhost + } else { + cookieOptions['domain'] = cookieOptions['domain'] || environment.COOKIE_DOMAIN; // Default domain for production + } + + return cookieOptions; // Return the final cookie options +} + +/** + * Sets a cookie with the specified name, value, and options. + * + * @param {string} name - The name of the cookie. + * @param {string} value - The value of the cookie. + * @param {Object} options - Additional options for the cookie. + */ +export function setCookie(name: string, value: string, options: { [key: string]: any } = {}) { + if (!name || typeof value === 'undefined') { + return; // Ensure valid inputs + } + + // Prepare cookie options with defaults + const cookieOptions = prepareCookieOptions(options); + + // Build the cookie string + const cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; `; + Object.entries(cookieOptions) + .map(([key, val]) => `${key}=${val}`) + .join('; '); + + // Set the cookie + document.cookie = cookieString; +} + +/** + * Deletes a cookie by setting its expiration date to a time in the past. + * + * @param {string} name - The name of the cookie to delete. + * @param {Object} options - Additional options for the cookie. + */ +export function deleteCookie(name: string, options: { [key: string]: any } = {}) { + if (!name) { + return; // Invalid name, exit function + } + + // Prepare cookie options with defaults + const cookieOptions = prepareCookieOptions(options); + + // Build the cookie string for deletion + const cookieString = `${encodeURIComponent(name)}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; `; + Object.entries(cookieOptions) + .map(([key, val]) => `${key}=${val}`) + .join('; '); + + // Set the cookie to delete it + document.cookie = cookieString; +} diff --git a/packages/ui-core/core/src/lib/auth/no-auth.guard.ts b/packages/ui-core/core/src/lib/auth/no-auth.guard.ts index e95f4a41b77..33f079f28aa 100644 --- a/packages/ui-core/core/src/lib/auth/no-auth.guard.ts +++ b/packages/ui-core/core/src/lib/auth/no-auth.guard.ts @@ -1,15 +1,17 @@ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; +import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; +import { ROUTES } from '@gauzy/ui-core/common'; import { AuthService, Store } from '../services'; + /** * Use for routes which only need to be displayed if user is NOT logged in */ @Injectable() -export class NoAuthGuard implements CanActivate { +export class NoAuthGuard { constructor( - private readonly router: Router, - private readonly authService: AuthService, - private readonly store: Store + private readonly _router: Router, + private readonly _authService: AuthService, + private readonly _store: Store ) {} /** @@ -20,18 +22,18 @@ export class NoAuthGuard implements CanActivate { * @return {Promise} A promise that resolves to true if the user is authenticated, false otherwise. */ async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { - if (!this.store.token) { + if (!this._store.token) { // not logged in so return true return true; } - if (!(await this.authService.isAuthenticated())) { + if (!(await this._authService.isAuthenticated())) { // not logged in so return true return true; } // logged in so redirect to dashboard - this.router.navigate(['/pages/dashboard']); + this._router.navigate([ROUTES.DASHBOARD]); return false; } diff --git a/packages/ui-core/core/src/lib/common/component-registry.types.ts b/packages/ui-core/core/src/lib/common/component-registry.types.ts index b68d6a2e4ab..60bb2472db7 100644 --- a/packages/ui-core/core/src/lib/common/component-registry.types.ts +++ b/packages/ui-core/core/src/lib/common/component-registry.types.ts @@ -57,10 +57,12 @@ export type PageRouteRegistryId = * based on the context and requirements of the application. * * Possible values: + * - 'dashboard': A dashboard tab. * - 'timesheet': A timesheet tab. * - 'time-activity': A time and activity tab. + * - 'employee-edit': An employee edit tab. */ -export type PageTabsetRegistryId = 'dashboard' | 'timesheet' | 'time-activity'; +export type PageTabsetRegistryId = 'dashboard' | 'timesheet' | 'time-activity' | 'employee-edit'; /** * @description @@ -72,6 +74,7 @@ export type PageTabsetRegistryId = 'dashboard' | 'timesheet' | 'time-activity'; * on the context and requirements of the application. * * Possible values: - * - 'job-employee': A sub-page under the jobs section. + * - 'employee-manage': Employee management page + * - 'job-employee': Sub-page under the jobs section (Job employee management page) */ -export type PageDataTableRegistryId = 'job-employee'; +export type PageDataTableRegistryId = 'employee-manage' | 'job-employee'; diff --git a/packages/ui-core/core/src/lib/components/base-nav-menu/base-nav-menu.component.ts b/packages/ui-core/core/src/lib/components/base-nav-menu/base-nav-menu.component.ts index 2c34b3ad464..00ccaa192c4 100644 --- a/packages/ui-core/core/src/lib/components/base-nav-menu/base-nav-menu.component.ts +++ b/packages/ui-core/core/src/lib/components/base-nav-menu/base-nav-menu.component.ts @@ -46,6 +46,18 @@ export class BaseNavMenuComponent extends TranslationBaseComponent implements On */ private defineBaseNavMenus() { this._navMenuBuilderService.defineNavMenuSections([ + ...this._getMainMenu(), + ...this._getAccordionMenu(), + ...this._getSettingsMenu() + ]); + } + + /** + * Retrieves the main navigation menu configuration. + * @returns An array of NavMenuSectionItem objects representing the main menu. + */ + private _getMainMenu(): NavMenuSectionItem[] { + return [ { id: 'dashboards', title: 'Dashboards', @@ -877,7 +889,214 @@ export class BaseNavMenuComponent extends TranslationBaseComponent implements On } ] } - ]); + ]; + } + + /** + * Retrieves the accordion menu configuration based on user permissions. + * Each menu item includes an ID, title, icon, link, and additional data such as translation keys, + * permission keys, and feature keys. + * + * @returns An array of NavMenuSectionItem objects representing the accordion menu. + */ + private _getAccordionMenu(): NavMenuSectionItem[] { + return [ + { + id: 'invite-people', + title: 'Invite people', + icon: 'fas fa-user-plus', + link: '/pages/employees/invites', + menuCategory: 'accordion', + data: { + translationKey: 'MENU.INVITE_PEOPLE', + permissionKeys: [PermissionsEnum.ALL_ORG_VIEW, PermissionsEnum.ORG_INVITE_VIEW], + featureKey: FeatureEnum.FEATURE_MANAGE_INVITE + } + }, + { + id: 'users', + title: 'Users', + icon: 'fas fa-users', + link: '/pages/users', + menuCategory: 'accordion', + data: { + translationKey: 'MENU.USERS', + permissionKeys: [PermissionsEnum.ALL_ORG_VIEW, PermissionsEnum.ORG_USERS_VIEW], + featureKey: FeatureEnum.FEATURE_USER + } + }, + { + id: 'import-export', + title: 'Import/Export', + icon: 'fas fa-exchange-alt', + link: '/pages/settings/import-export', + menuCategory: 'accordion', + data: { + translationKey: 'MENU.IMPORT_EXPORT.IMPORT_EXPORT', + permissionKeys: [ + PermissionsEnum.ALL_ORG_VIEW, + PermissionsEnum.IMPORT_ADD, + PermissionsEnum.EXPORT_ADD + ], + featureKey: FeatureEnum.FEATURE_IMPORT_EXPORT + } + }, + { + id: 'organizations', + title: 'Organizations', + icon: 'fas fa-globe', + link: '/pages/organizations', + menuCategory: 'accordion', + data: { + translationKey: 'MENU.ORGANIZATIONS', + permissionKeys: [PermissionsEnum.ALL_ORG_VIEW, PermissionsEnum.ORG_EXPENSES_EDIT], + featureKey: FeatureEnum.FEATURE_ORGANIZATIONS + } + }, + { + id: 'integrations', + title: 'Integrations', + icon: 'fas fa-swatchbook', + link: '/pages/integrations', + menuCategory: 'accordion', + pathMatch: 'prefix', + data: { + translationKey: 'MENU.INTEGRATIONS', + permissionKeys: [PermissionsEnum.INTEGRATION_ADD, PermissionsEnum.INTEGRATION_EDIT], + featureKey: FeatureEnum.FEATURE_APP_INTEGRATION + } + } + ]; + } + + /** + * Retrieves the settings menu configuration. + * + * @returns An array containing a single NavMenuSectionItem for settings. + */ + private _getSettingsMenu(): NavMenuSectionItem[] { + return [ + { + id: 'settings', + title: 'Settings', + icon: 'fas fa-cog', + menuCategory: 'settings', + data: { + translationKey: 'MENU.SETTINGS' + }, + items: [ + { + id: 'settings-general', + title: 'General', + icon: 'fas fa-pen', + link: '/pages/settings/general', + data: { + translationKey: 'MENU.GENERAL', + featureKey: FeatureEnum.FEATURE_SETTING + } + }, + { + id: 'settings-features', + title: 'Features', + icon: 'fas fa-swatchbook', + link: '/pages/settings/features', + data: { + translationKey: 'MENU.FEATURES', + permissionKeys: [PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ALL_ORG_VIEW] + } + }, + { + id: 'settings-email-history', + title: 'Email History', + icon: 'fas fa-envelope-open', + link: '/pages/settings/email-history', + data: { + translationKey: 'MENU.EMAIL_HISTORY', + permissionKeys: [PermissionsEnum.VIEW_ALL_EMAILS], + featureKey: FeatureEnum.FEATURE_EMAIL_HISTORY + } + }, + { + id: 'settings-email-templates', + title: 'Email Templates', + icon: 'fas fa-envelope', + link: '/pages/settings/email-templates', + data: { + translationKey: 'MENU.EMAIL_TEMPLATES', + permissionKeys: [PermissionsEnum.VIEW_ALL_EMAIL_TEMPLATES], + featureKey: FeatureEnum.FEATURE_EMAIL_TEMPLATE + } + }, + { + id: 'settings-accounting-templates', + title: 'Accounting Templates', + icon: 'fas fa-address-card', + link: '/pages/settings/accounting-templates', + data: { + translationKey: 'MENU.ACCOUNTING_TEMPLATES', + permissionKeys: [PermissionsEnum.VIEW_ALL_ACCOUNTING_TEMPLATES] + } + }, + { + id: 'settings-file-storage', + title: 'File storage', + icon: 'fas fa-database', + link: '/pages/settings/file-storage', + data: { + translationKey: 'MENU.FILE_STORAGE', + permissionKeys: [PermissionsEnum.FILE_STORAGE_VIEW], + featureKey: FeatureEnum.FEATURE_FILE_STORAGE + } + }, + { + id: 'settings-sms-gateways', + title: 'SMS Gateways', + icon: 'fas fa-at', + link: '/pages/settings/sms-gateway', + data: { + translationKey: 'MENU.SMS_GATEWAYS', + permissionKeys: [PermissionsEnum.SMS_GATEWAY_VIEW], + featureKey: FeatureEnum.FEATURE_SMS_GATEWAY + } + }, + { + id: 'settings-custom-smtp', + title: 'Custom SMTP', + icon: 'fas fa-at', + link: '/pages/settings/custom-smtp', + data: { + translationKey: 'MENU.CUSTOM_SMTP', + permissionKeys: [PermissionsEnum.CUSTOM_SMTP_VIEW], + featureKey: FeatureEnum.FEATURE_SMTP + } + }, + { + id: 'settings-roles-permissions', + title: 'Roles & Permissions', + link: '/pages/settings/roles-permissions', + icon: 'fas fa-award', + data: { + translationKey: 'MENU.ROLES', + permissionKeys: [PermissionsEnum.CHANGE_ROLES_PERMISSIONS], + featureKey: FeatureEnum.FEATURE_ROLES_PERMISSION + } + }, + { + id: 'settings-danger-zone', + title: 'Danger Zone', + link: '/pages/settings/danger-zone', + icon: 'fas fa-radiation-alt', + data: { + translationKey: 'MENU.DANGER_ZONE', + permissionKeys: [ + PermissionsEnum.ACCESS_DELETE_ACCOUNT, + PermissionsEnum.ACCESS_DELETE_ALL_DATA + ] + } + } + ] + } + ]; } /** diff --git a/packages/ui-core/core/src/lib/components/common-nav.module.ts b/packages/ui-core/core/src/lib/components/common-nav.module.ts index f4f7956118b..f49ff65e6c2 100644 --- a/packages/ui-core/core/src/lib/components/common-nav.module.ts +++ b/packages/ui-core/core/src/lib/components/common-nav.module.ts @@ -4,18 +4,14 @@ import { NgxPermissionsModule } from 'ngx-permissions'; import { NbAccordionModule, NbButtonModule, NbTooltipModule } from '@nebular/theme'; import { BaseNavMenuComponent } from './base-nav-menu/base-nav-menu.component'; import { MainNavMenuComponent } from './main-nav-menu/main-nav-menu.component'; +import { SettingsNavMenuComponent } from './settings-nav-menu/settings-nav-menu.component'; import { MenuItemComponent, SidebarMenuComponent, ChildrenMenuItemComponent, TooltipDirective } from './sidebar-menu'; +const COMPONENTS = [BaseNavMenuComponent, MainNavMenuComponent, SidebarMenuComponent, SettingsNavMenuComponent]; + @NgModule({ - declarations: [ - BaseNavMenuComponent, - MainNavMenuComponent, - SidebarMenuComponent, - MenuItemComponent, - ChildrenMenuItemComponent, - TooltipDirective - ], imports: [CommonModule, NbAccordionModule, NbTooltipModule, NbButtonModule, NgxPermissionsModule.forChild()], - exports: [BaseNavMenuComponent, MainNavMenuComponent, SidebarMenuComponent] + declarations: [...COMPONENTS, MenuItemComponent, ChildrenMenuItemComponent, TooltipDirective], + exports: [...COMPONENTS] }) export class CommonNavModule {} diff --git a/packages/ui-core/core/src/lib/components/index.ts b/packages/ui-core/core/src/lib/components/index.ts index 8fa6fb1554e..18e1f111551 100644 --- a/packages/ui-core/core/src/lib/components/index.ts +++ b/packages/ui-core/core/src/lib/components/index.ts @@ -2,3 +2,4 @@ export * from './common-nav.module'; export * from './base-nav-menu/base-nav-menu.component'; export * from './main-nav-menu/main-nav-menu.component'; export * from './sidebar-menu/sidebar-menu.component'; +export * from './settings-nav-menu/settings-nav-menu.component'; diff --git a/packages/ui-core/core/src/lib/components/main-nav-menu/main-nav-menu.component.ts b/packages/ui-core/core/src/lib/components/main-nav-menu/main-nav-menu.component.ts index b9f2641b18e..5b40fb6358b 100644 --- a/packages/ui-core/core/src/lib/components/main-nav-menu/main-nav-menu.component.ts +++ b/packages/ui-core/core/src/lib/components/main-nav-menu/main-nav-menu.component.ts @@ -1,15 +1,20 @@ -import { Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { NavMenuSectionItem } from '../../services/nav-builder/nav-builder-types'; +import { Component, Input, OnInit } from '@angular/core'; +import { Observable, catchError, map } from 'rxjs'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { BaseNavMenuComponent } from '../base-nav-menu/base-nav-menu.component'; +import { NavMenuCategory, NavMenuSectionItem } from '../../services/nav-builder/nav-builder-types'; +@UntilDestroy({ checkProperties: true }) @Component({ selector: 'ga-main-nav-menu', templateUrl: './main-nav-menu.component.html', styleUrls: ['./main-nav-menu.component.scss'] }) export class MainNavMenuComponent extends BaseNavMenuComponent implements OnInit { + // Define the input property menuCategory of type NavMenuCategory | undefined + @Input() menuCategory: NavMenuCategory | undefined; + + // Define the observable property mainMenuConfig$ of type Observable public mainMenuConfig$: Observable; override ngOnInit(): void { @@ -17,7 +22,28 @@ export class MainNavMenuComponent extends BaseNavMenuComponent implements OnInit // Subscribe to the menuConfig$ observable provided by _navMenuBuilderService this.mainMenuConfig$ = this._navMenuBuilderService.menuConfig$.pipe( - map((sections: NavMenuSectionItem[]) => this.mapMenuSections(sections)) + map((sections: NavMenuSectionItem[]) => this.filterSectionsByCategory(sections)), + catchError((error) => { + console.error('Error while retrieving main menu sections:', error); + return []; + }), + untilDestroyed(this) + ); + } + + /** + * Filters the provided menu sections based on the specified menu category. + * + * @param sections - An array of navigation menu section items to filter. + * @returns An array of navigation menu section items that match the specified menu category. + */ + private filterSectionsByCategory(sections: NavMenuSectionItem[]): NavMenuSectionItem[] { + return this.mapMenuSections(sections ?? []).filter((section) => + this.menuCategory ? section?.menuCategory === this.menuCategory : !section?.menuCategory ); } + + override ngOnDestroy(): void { + super.ngOnDestroy(); + } } diff --git a/packages/ui-core/core/src/lib/components/settings-nav-menu/settings-nav-menu.component.html b/packages/ui-core/core/src/lib/components/settings-nav-menu/settings-nav-menu.component.html new file mode 100644 index 00000000000..f9bc7901699 --- /dev/null +++ b/packages/ui-core/core/src/lib/components/settings-nav-menu/settings-nav-menu.component.html @@ -0,0 +1 @@ + diff --git a/packages/ui-core/shared/src/lib/smart-data-layout/smart-table-toggle/smart-table-toggle.component.scss b/packages/ui-core/core/src/lib/components/settings-nav-menu/settings-nav-menu.component.scss similarity index 100% rename from packages/ui-core/shared/src/lib/smart-data-layout/smart-table-toggle/smart-table-toggle.component.scss rename to packages/ui-core/core/src/lib/components/settings-nav-menu/settings-nav-menu.component.scss diff --git a/packages/ui-core/core/src/lib/components/settings-nav-menu/settings-nav-menu.component.ts b/packages/ui-core/core/src/lib/components/settings-nav-menu/settings-nav-menu.component.ts new file mode 100644 index 00000000000..d8158ae8f9a --- /dev/null +++ b/packages/ui-core/core/src/lib/components/settings-nav-menu/settings-nav-menu.component.ts @@ -0,0 +1,31 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable, map } from 'rxjs'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { BaseNavMenuComponent } from '../base-nav-menu/base-nav-menu.component'; +import { NavMenuSectionItem } from '../../services/nav-builder/nav-builder-types'; + +@UntilDestroy({ checkProperties: true }) +@Component({ + selector: 'ga-settings-nav-menu', + templateUrl: './settings-nav-menu.component.html', + styleUrls: ['./settings-nav-menu.component.scss'] +}) +export class SettingsNavMenuComponent extends BaseNavMenuComponent implements OnInit { + public settingsMenuConfig$: Observable; + + override ngOnInit(): void { + super.ngOnInit(); // Call the parent class's ngOnInit function + + // Subscribe to the menuConfig$ observable provided by _navMenuBuilderService + this.settingsMenuConfig$ = this._navMenuBuilderService.menuConfig$.pipe( + map((sections: NavMenuSectionItem[]) => + this.mapMenuSections(sections ?? []).filter((section) => section.menuCategory === 'settings') + ), + untilDestroyed(this) + ); + } + + override ngOnDestroy(): void { + super.ngOnDestroy(); + } +} diff --git a/packages/ui-core/core/src/lib/components/sidebar-menu/menu-items/concrete/menu-item/menu-item.component.html b/packages/ui-core/core/src/lib/components/sidebar-menu/menu-items/concrete/menu-item/menu-item.component.html index df94aca76cc..9674e3e1880 100644 --- a/packages/ui-core/core/src/lib/components/sidebar-menu/menu-items/concrete/menu-item/menu-item.component.html +++ b/packages/ui-core/core/src/lib/components/sidebar-menu/menu-items/concrete/menu-item/menu-item.component.html @@ -24,15 +24,18 @@ [ngClass]="onCollapse ? 'collapsed' : ''" (click)="redirectTo()" > - {{ item?.title }} + + + + {{ item?.title }} + +
    { diff --git a/packages/ui-core/core/src/lib/services/job/index.ts b/packages/ui-core/core/src/lib/services/job/index.ts index d4f512d7ea0..a6379958983 100644 --- a/packages/ui-core/core/src/lib/services/job/index.ts +++ b/packages/ui-core/core/src/lib/services/job/index.ts @@ -1,4 +1,5 @@ export * from './job-preset.service'; export * from './job-search-category.service'; export * from './job-search-occupation.service'; -export * from './job.service'; \ No newline at end of file +export * from './job.service'; +export * from './job-search.store.service'; diff --git a/packages/ui-core/core/src/lib/services/job/job-preset.service.ts b/packages/ui-core/core/src/lib/services/job/job-preset.service.ts index 7e97f2372ed..3f9485a8e15 100644 --- a/packages/ui-core/core/src/lib/services/job/job-preset.service.ts +++ b/packages/ui-core/core/src/lib/services/job/job-preset.service.ts @@ -2,8 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { firstValueFrom } from 'rxjs'; import { ID, IEmployeePresetInput, IGetJobPresetInput, IJobPreset, IMatchingCriterions } from '@gauzy/contracts'; -import { toParams } from '@gauzy/ui-core/common'; -import { API_PREFIX } from '@gauzy/ui-core/common'; +import { API_PREFIX, toParams } from '@gauzy/ui-core/common'; @Injectable({ providedIn: 'root' diff --git a/packages/ui-core/core/src/lib/services/job/job-search.store.service.ts b/packages/ui-core/core/src/lib/services/job/job-search.store.service.ts new file mode 100644 index 00000000000..e2381a07a65 --- /dev/null +++ b/packages/ui-core/core/src/lib/services/job/job-search.store.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@angular/core'; +import { IEmployee, IOrganization } from '@gauzy/contracts'; +import { JobService } from './job.service'; +import { ToastrService } from '../notification/toastr.service'; + +@Injectable({ + providedIn: 'root' +}) +export class JobSearchStoreService { + constructor(private readonly _jobService: JobService, private readonly _toastrService: ToastrService) {} + + /** + * Updates the job search availability status of an employee within the organization. + * + * @param organization - The current organization context. + * @param employee - The employee object to update. + * @param isJobSearchActive - A boolean flag indicating whether the job search is active. + * @returns {Promise} - A Promise resolving to void. + */ + async updateJobSearchAvailability( + organization: IOrganization | undefined, + employee: IEmployee, + isJobSearchActive: boolean + ): Promise { + try { + // Ensure the organization context is available before proceeding. + if (!organization) { + console.warn('No organization provided to update job search availability.'); + return; + } + + // Destructure organization properties for clarity. + const { id: organizationId, tenantId } = organization; + + // Update the job search status using the employeesService. + await this._jobService.updateJobSearchStatus(employee.id, { + isJobSearchActive, + organizationId, + tenantId + }); + + // Display a success toastr notification based on the job search status. + const toastrMessageKey = isJobSearchActive + ? 'TOASTR.MESSAGE.EMPLOYEE_JOB_STATUS_ACTIVE' + : 'TOASTR.MESSAGE.EMPLOYEE_JOB_STATUS_INACTIVE'; + + const fullName = employee.fullName.trim() || 'Unknown Employee'; + this._toastrService.success(toastrMessageKey, { name: fullName }); + } catch (error) { + // Display an error toastr notification in case of any exceptions. + const errorMessage = error?.message || 'An error occurred while updating the job search availability.'; + console.error('Error while updating job search availability:', error?.message); + this._toastrService.danger(errorMessage); + } + } +} diff --git a/packages/ui-core/core/src/lib/services/nav-builder/nav-builder-types.ts b/packages/ui-core/core/src/lib/services/nav-builder/nav-builder-types.ts index 9efa6067c2d..fb5b0002f19 100644 --- a/packages/ui-core/core/src/lib/services/nav-builder/nav-builder-types.ts +++ b/packages/ui-core/core/src/lib/services/nav-builder/nav-builder-types.ts @@ -1,47 +1,51 @@ -import { ActivatedRoute } from "@angular/router"; -import { NbMenuItem } from "@nebular/theme"; -import { FeatureEnum, PermissionsEnum } from "@gauzy/contracts"; +import { ActivatedRoute } from '@angular/router'; +import { NbMenuItem } from '@nebular/theme'; +import { FeatureEnum, PermissionsEnum } from '@gauzy/contracts'; // Define a type NavMenuBadgeType representing different types of badges. export type NavMenuBadgeType = 'basic' | 'primary' | 'info' | 'success' | 'warning' | 'danger' | 'control'; +// Define the possible menu categories +export type NavMenuCategory = 'main' | 'settings' | 'accordion'; + /** * A NavMenuSection is a grouping of links in the main (left-hand side) navigation menu bar. */ export interface NavMenuSectionItem extends NbMenuItem { - id: string; // Unique identifier for the section - class?: string; // Additional class for styling (optional) - items?: NavMenuSectionItem[]; // Array of NavMenuItem objects representing the links within the section (optional) - onClick?: (event: MouseEvent) => void; // Function to be called when the menu item is clicked (optional) - data: NavMenuItemData; // Data associated with the section + id: string; // Unique identifier for the section + menuCategory?: NavMenuCategory; // Category of the menu (optional) + class?: string; // Additional class for styling (optional) + items?: NavMenuSectionItem[]; // Array of NavMenuItem objects representing the links within the section (optional) + onClick?: (event: MouseEvent) => void; // Function to be called when the menu item is clicked (optional) + data: NavMenuItemData; // Data associated with the section } /** * Data associated with a NavMenuItem or NavMenuSection. */ export interface NavMenuItemData { - translationKey: string; // Translation key for the title, mandatory for all items - permissionKeys?: PermissionsEnum[]; // Permissions required to display the item (optional) - featureKey?: FeatureEnum; // Feature key required to display the item (optional) - hide?: () => boolean | boolean; // Function to determine if the item should be hidden (optional) - add?: string; + translationKey: string; // Translation key for the title, mandatory for all items + permissionKeys?: PermissionsEnum[]; // Permissions required to display the item (optional) + featureKey?: FeatureEnum; // Feature key required to display the item (optional) + hide?: () => boolean | boolean; // Function to determine if the item should be hidden (optional) + add?: string; } /** * Represents the configuration for navigation menu sections. */ export interface NavMenuSectionConfig { - config: NavMenuSectionItem; // Configuration for the navigation menu section - before?: string; // (Optional) Identifier of the section before which this section should be inserted + config: NavMenuSectionItem; // Configuration for the navigation menu section + before?: string; // (Optional) Identifier of the section before which this section should be inserted } /** * Represents the configuration for navigation menu items. */ export interface NavMenuItemsConfig { - config: NavMenuSectionItem; // Configuration for the navigation menu item - sectionId: string; // Identifier of the section to which this item belongs - before?: string; // (Optional) Identifier of the item before which this item should be inserted + config: NavMenuSectionItem; // Configuration for the navigation menu item + sectionId: string; // Identifier of the section to which this item belongs + before?: string; // (Optional) Identifier of the item before which this item should be inserted } /** diff --git a/packages/ui-core/core/src/lib/services/nav-builder/nav-menu-builder.service.ts b/packages/ui-core/core/src/lib/services/nav-builder/nav-menu-builder.service.ts index fce44168ed4..d1b2bd3ceb1 100644 --- a/packages/ui-core/core/src/lib/services/nav-builder/nav-menu-builder.service.ts +++ b/packages/ui-core/core/src/lib/services/nav-builder/nav-menu-builder.service.ts @@ -1,16 +1,11 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, combineLatest, map, Observable, of, shareReplay } from 'rxjs'; -import { - NavMenuSectionItem, - NavMenuItemsConfig, - NavMenuSectionConfig, -} from './nav-builder-types'; +import { NavMenuSectionItem, NavMenuItemsConfig, NavMenuSectionConfig } from './nav-builder-types'; @Injectable({ providedIn: 'root' }) export class NavMenuBuilderService { - // Declare an observable property menuConfig$ of type Observable public menuConfig$: Observable; @@ -78,8 +73,8 @@ export class NavMenuBuilderService { */ addNavMenuItem(config: NavMenuSectionItem, sectionId: string, before?: string) { // Check if the item already exists - const existingIndex = this.addedNavMenuItems.findIndex(item => - item.config.id === config.id && item.sectionId === sectionId + const existingIndex = this.addedNavMenuItems.findIndex( + (item) => item.config.id === config.id && item.sectionId === sectionId ); if (existingIndex !== -1) { @@ -103,8 +98,8 @@ export class NavMenuBuilderService { addNavMenuItems(configs: NavMenuSectionItem[], sectionId: string, before?: string) { configs.forEach((config: NavMenuSectionItem) => { // Check if the item already exists - const existingIndex = this.addedNavMenuItems.findIndex((item) => - item.config.id === config.id && item.sectionId === sectionId + const existingIndex = this.addedNavMenuItems.findIndex( + (item) => item.config.id === config.id && item.sectionId === sectionId ); if (existingIndex !== -1) { @@ -127,10 +122,14 @@ export class NavMenuBuilderService { * @param sectionId The identifier of the section from which the item should be removed. */ removeNavMenuItem(itemId: string, sectionId: string): void { - const itemIndex = this.addedNavMenuItems.findIndex((item) => item.config.id === itemId && item.sectionId === sectionId); + const itemIndex = this.addedNavMenuItems.findIndex( + (item) => item.config.id === itemId && item.sectionId === sectionId + ); if (itemIndex !== -1) { // Check if the item is already present in the removedNavMenuItems array - const existingIndex = this.removedNavMenuItems.findIndex((item) => item.config.id === itemId && item.sectionId === sectionId); + const existingIndex = this.removedNavMenuItems.findIndex( + (item) => item.config.id === itemId && item.sectionId === sectionId + ); if (existingIndex === -1) { // Push the removed item into the removedNavMenuItems array this.removedNavMenuItems.push(this.addedNavMenuItems[itemIndex]); @@ -188,7 +187,7 @@ export class NavMenuBuilderService { return [...configMap.values()]; }), - shareReplay(1), + shareReplay(1) ); // Combine the combined configuration with item additions to produce the final menu configuration @@ -230,7 +229,7 @@ export class NavMenuBuilderService { } } } else { - console.error(`Could not add menu item "${item.config.id}", section "${item.sectionId}" does not exist`); + this.logMenuWarning(item.config.id, item.sectionId); } }); @@ -238,4 +237,17 @@ export class NavMenuBuilderService { }) ); } + + /** + * Logs a warning message about an inability to add a menu item. + * + * @param itemId - The ID of the menu item that could not be added. + * @param sectionId - The ID of the section where the item was to be added. + * @param level - Optional logging level; defaults to 'warn'. + */ + private logMenuWarning(itemId: string, sectionId: string, level: 'warn' | 'info' | 'error' = 'warn') { + const message = `Unable to add menu item "${itemId}". Section "${sectionId}" does not exist. Please ensure the section is defined before adding items.`; + const logFn = console[level]; + logFn(message); + } } diff --git a/packages/ui-core/core/src/lib/services/page/page-data-table-registry.service.ts b/packages/ui-core/core/src/lib/services/page/page-data-table-registry.service.ts index 3c9d3f50a80..cdfcc4a640a 100644 --- a/packages/ui-core/core/src/lib/services/page/page-data-table-registry.service.ts +++ b/packages/ui-core/core/src/lib/services/page/page-data-table-registry.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { Cell, IColumn, IColumns } from 'angular2-smart-table'; +import { IColumn, IColumns } from 'angular2-smart-table'; import { PageDataTableRegistryId } from '../../common/component-registry.types'; import { IPageDataTableRegistry, PageDataTableRegistryConfig } from './page-data-table-registry.types'; @@ -88,11 +88,46 @@ export class PageDataTableRegistryService implements IPageDataTableRegistry { * `PageDataTableRegistryId`. If any configurations are found, they are sorted based on their `order` property in * ascending order. If no configurations are found, an empty array is returned. * - * @param location - The identifier used to look up the data table column configurations. - * @returns An array of `PageDataTableRegistryConfig` objects sorted by the `order` property, or an empty array if none are found. + * @param dataTableId The identifier for the data table. + * @returns An array of `PageDataTableRegistryConfig` objects associated with the specified `dataTableId`, + * sorted by the `order` property in ascending order. */ - private getDataTableColumnsByOrder(location: PageDataTableRegistryId): PageDataTableRegistryConfig[] { - return this.registry.get(location)?.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) || []; + public getColumnsByDataTableId(dataTableId: PageDataTableRegistryId): PageDataTableRegistryConfig[] { + const columns = this.registry.get(dataTableId) || []; + + // Sort the columns by the 'order' property in ascending order + return [...columns].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); + } + + /** + * Maps a PageDataTableRegistryConfig object to an IColumn object. + * + * @param config The PageDataTableRegistryConfig object to map. + * @returns The corresponding IColumn object. + */ + private mapConfigToColumn(config: PageDataTableRegistryConfig): IColumn { + const column: IColumn = { + ...(config.title && { title: typeof config.title === 'function' ? config.title() : config.title }), + type: config.type, + width: config.width, + isSortable: config.isSortable ?? false, + isEditable: config.isEditable ?? false, + isFilterable: config.isFilterable ?? false, + hide: config.hide ?? false, + ...(config.editor && { editor: config.editor }), + ...(config.renderComponent && { renderComponent: config.renderComponent }), + ...(config.valuePrepareFunction && { valuePrepareFunction: config.valuePrepareFunction }), + ...(config.componentInitFunction && { componentInitFunction: config.componentInitFunction }), + ...(config.filter && { filter: config.filter }), + ...(config.filterFunction && { filterFunction: config.filterFunction }) + }; + + // Check if the column configuration has additional column options + if (config.column) { + Object.assign(column, config.column); + } + + return column; // Return the mapped IColumn object } /** @@ -111,7 +146,7 @@ export class PageDataTableRegistryService implements IPageDataTableRegistry { */ public getPageDataTableColumns(dataTableId: PageDataTableRegistryId): IColumns { // Get all registered columns for the specified location - let columns = this.getDataTableColumnsByOrder(dataTableId); + let columns = this.getColumnsByDataTableId(dataTableId); // Use a Set to track unique location-id combinations const dataTableIds = new Set(); @@ -122,7 +157,7 @@ export class PageDataTableRegistryService implements IPageDataTableRegistry { const identifier = `${config.dataTableId}-${config.columnId}`; // Check if the unique identifier is already in the Set - if (dataTableIds.has(identifier)) { + if (dataTableIds.has(identifier) || config.hide) { return false; // Duplicate found, filter it out } @@ -133,37 +168,26 @@ export class PageDataTableRegistryService implements IPageDataTableRegistry { // Map each unique configuration to an IColumn object return columns.reduce((acc: IColumns, config: PageDataTableRegistryConfig) => { - // Create and return a new IColumn object - const column: IColumn = { - ...(config.title && { - title: typeof config.title === 'function' ? config.title() : config.title - }), - type: config.type, - width: config.width, - isSortable: config.isSortable ?? false, - isEditable: config.isEditable ?? false, - ...(config.editor && { editor: config.editor }), - ...(config.renderComponent && { renderComponent: config.renderComponent }), - ...(config.valuePrepareFunction && { - valuePrepareFunction: (rawValue: any, cell: Cell) => config.valuePrepareFunction(rawValue, cell) - }), - ...(config.componentInitFunction && { - componentInitFunction: (component: any, cell: Cell) => config.componentInitFunction(component, cell) - }) - }; - - // Check if the column configuration has additional column options - if (config.column) { - Object.assign(column, config.column); - } - - // Add the column configuration to the accumulator object with the columnId as the key - acc[config.columnId] = column; - + const column = this.mapConfigToColumn(config); // Use the mapping function + acc[config.columnId] = column; // Add the column to the accumulator return acc; }, {}); } + /** + * Retrieves a specific column configuration by its dataTableId and columnId. + * + * @param dataTableId The identifier for the data table. + * @param columnId The identifier for the column. + * @returns The `IColumn` object for the specified column, or `null` if not found. + */ + public getColumnById(dataTableId: PageDataTableRegistryId, columnId: string): IColumns | null { + const columns = this.registry.get(dataTableId) || []; + const config = columns.find((column) => column.columnId === columnId); + + return config ? { [columnId]: this.mapConfigToColumn(config) } : null; + } + /** * Deletes a data table from the registry. * diff --git a/packages/ui-core/core/src/lib/utils/smart-table/server.data-source.ts b/packages/ui-core/core/src/lib/utils/smart-table/server.data-source.ts index 66bc462d75b..68fcb3a7622 100644 --- a/packages/ui-core/core/src/lib/utils/smart-table/server.data-source.ts +++ b/packages/ui-core/core/src/lib/utils/smart-table/server.data-source.ts @@ -104,16 +104,40 @@ export class ServerDataSource extends LocalDataSource { return toParams(requestParams); } - protected addSortRequestParams() { + /** + * Adds sorting parameters to the request based on the sorting configuration. + * + * This function processes the `sortConf` configuration and extracts the field + * and its direction (ascending or descending) to create a sorting object. + * If a field does not have a valid direction, it will be skipped, and a warning + * will be logged. The resulting sorting parameters are returned as part of an + * object that can be used in a request. + * + * @returns {Object} An object containing the sorting parameters. + */ + protected addSortRequestParams(): { [key: string]: any } { if (this.sortConf) { - const orders: any = {}; + // Initialize an object to hold sorting orders + const orders: { [key: string]: string } = {}; + + // Iterate through the sort configuration array this.sortConf.forEach((fieldConf) => { - orders[fieldConf.field] = fieldConf.direction.toUpperCase(); + // Ensure the field configuration has a valid direction + if (fieldConf.direction) { + // Convert direction to uppercase (e.g., ASC or DESC) and add it to orders + orders[fieldConf.field] = fieldConf.direction.toUpperCase(); + } else { + // Log a warning if the direction is not defined + console.warn(`Direction is not defined for field: ${fieldConf.field}`); + } }); + + // Return the sorting orders wrapped in the expected format return { [this.conf.sortDirKey]: orders }; } else { + // Return an empty object if there is no sorting configuration return {}; } } diff --git a/packages/ui-core/i18n/assets/i18n/bg.json b/packages/ui-core/i18n/assets/i18n/bg.json index 65118a4ddf2..222b4e8d9c2 100644 --- a/packages/ui-core/i18n/assets/i18n/bg.json +++ b/packages/ui-core/i18n/assets/i18n/bg.json @@ -178,45 +178,45 @@ }, "SM_TABLE": { "NO_DATA": { - "LOADING": "Зареждане, моля, изчакайте...", + "LOADING": "Зареждане, моля изчакайте...", "RECEIVE_ESTIMATE": "Не сте получили никакви оценки.", - "INCOME": "Не сте създали никакви доходи.", + "INCOME": "Не сте създали никакви приходи.", "EXPENSE_CATEGORY": "Не сте създали никакви категории разходи.", "REPORT": "Не сте създали никакви отчети.", "CONTRACT": "Не сте създали никакви договори.", "TEAM": "Не сте създали никакви екипи.", "HISTORY_RECORD": "Не сте създали никакви записи.", - "PROFIT_HISTORY": "Не сте създали никаква история на печалбата.", + "PROFIT_HISTORY": "Не сте създали никаква история на печалбите.", "EMPLOYEE": "Не сте създали никакви служители.", "EXPENSE": "Не сте създали никакви разходи.", "PAYMENT": "Не сте получили никакви плащания.", "PROPOSAL_TEMPLATE": "Не сте създали никакви шаблони за предложения.", "PROPOSAL": "Не сте създали никакви предложения.", - "PIPELINE": "Не сте създали никакви тръбопроводи.", + "PIPELINE": "Не сте създали никакви канали.", "TASK": "Не сте създали никакви задачи.", "INVITE": "Не сте поканили никакви потребители.", "APPROVAL_REQUEST": "Не сте създали никакви заявки за одобрение.", "APPROVAL_POLICY": "Не сте създали никакви политики за одобрение.", - "TIME_OFF": "Не сте създали никакво време за отпуск.", - "TIME_OFF_POLICY": "Не сте създали политики за отпуск.", + "TIME_OFF": "Не сте създали никакви отпуски.", + "TIME_OFF_POLICY": "Не сте създали никакви политики за отпуск.", "CANDIDATE": "Не сте създали никакви кандидати.", "INTERVIEW": "Не сте създали никакви интервюта.", "EQUIPMENT": "Не сте създали никакво оборудване.", "EQUIPMENT_SHARING": "Не сте създали никакви записи за споделяне на оборудване.", "EQUIPMENT_SHARING_POLICY": "Не сте създали никакви политики за споделяне на оборудване.", - "INVENTORY": "Не сте създали никакви инвентари.", + "INVENTORY": "Не сте създали никакви инвентаризации.", "MERCHANT": "Не сте създали никакви търговци.", "WAREHOUSE": "Не сте създали никакви складове.", - "WAREHOUSE_PRODUCT": "Не сте създали никакви складови продукти.", - "PRODUCT_CATEGORY": "Не сте създали никакви категории продукти.", + "WAREHOUSE_PRODUCT": "Не сте създали никакви продукти за склад.", + "PRODUCT_CATEGORY": "Не сте създали никакви продуктови категории.", "TAGS": "Не сте създали никакви етикети.", "PROJECT": "Не сте създали никакви проекти.", "DEPARTMENT": "Не сте създали никакви отдели.", "CONTACT": "Не сте създали никакви контакти.", "CLIENT": "Не сте създали никакви клиенти.", "LEAD": "Не сте създали никакви потенциални клиенти.", - "TIME_FRAME": "Не сте създали никакви времеви периоди.", - "KPI": "Не сте създали никакви КПИ.", + "TIME_FRAME": "Не сте създали никакви времеви рамки.", + "KPI": "Не сте създали никакви KPI.", "INVOICE": "Не сте създали никакви фактури.", "ESTIMATE": "Не сте създали никакви оценки.", "EVENT_TYPE": "Не сте създали никакви типове събития.", @@ -229,10 +229,10 @@ "DATE": "Дата", "TITLE": "Заглавие", "STAGE": "Етап", - "START_DATE": "Начална дата", - "END_DATE": "Крайна дата", - "CLIENT_NAME": "Клиентско име", - "CONTACT_NAME": "Име на контакт", + "START_DATE": "Начална Дата", + "END_DATE": "Крайна Дата", + "CLIENT_NAME": "Име на Клиента", + "CONTACT_NAME": "Име на Контакта", "NAME": "Име", "VENDOR": "Доставчик", "CATEGORY": "Категория", @@ -241,26 +241,25 @@ "NOTES": "Бележки", "EMPLOYEE": "Служител", "EMPLOYEES": "Служители", - "FULL_NAME": "Пълно име", - "EMAIL": "Електронна поща", - "INCOME": "Доход (Средно)", + "FULL_NAME": "Пълно Име", + "EMAIL": "Имейл", + "INCOME": "Приходи (Средно)", "EXPENSES": "Разходи (Средно)", "BONUS": "Бонус", "BONUS_AVG": "Бонус (Средно)", - "PROFIT_BASED_BONUS": "Бонус въз основа на печалбата", - "REVENUE_BASED_BONUS": "Бонус въз основа на приходите", + "PROFIT_BASED_BONUS": "Бонус на база печалба", + "REVENUE_BASED_BONUS": "Бонус на база приходи", "STATUS": "Статус", "SOURCE": "Източник", - "WORK_STATUS": "Работен статус", "TODAY": "Днес", - "END_OF_MONTH": "Края на месеца", - "START_OF_MONTH": "Началото на месеца", - "RATE": "Почасова ставка", - "FLAT_FEE": "Фиксирана такса", - "MILESTONES": "Етапи", - "JOB_TITLE": "Заглавие на работа", - "JOB_POST_URL": "URL на работната обява", - "LINK_TO_JOBPOST": "Връзка към работната обява", + "END_OF_MONTH": "Край на Месеца", + "START_OF_MONTH": "Начало на Месеца", + "RATE": "Ставка", + "FLAT_FEE": "Фиксирана Такса", + "MILESTONES": "Милстоунове", + "JOB_TITLE": "Заглавие на Работата", + "JOB_POST_URL": "URL на Работното Обявление", + "LINK_TO_JOBPOST": "Връзка към Работното Обявление", "AUTHOR": "Автор", "MONDAY": "Понеделник", "TUESDAY": "Вторник", @@ -269,12 +268,12 @@ "FRIDAY": "Петък", "SATURDAY": "Събота", "SUNDAY": "Неделя", - "NONE": "Нито един", + "NONE": "Няма", "ROLE": "Роля", "PROJECTS": "Проекти", "PROJECT": "Проект", "INVITED_BY": "Поканен от", - "EXPIRE_DATE": "Дата на изтичане", + "EXPIRE_DATE": "Изтича на", "CLIENTS": "Клиенти", "CONTACTS": "Контакти", "CONTACT": "Контакт", @@ -284,12 +283,12 @@ "APPLIED": "Кандидатствал", "HIRED": "Нает", "REJECTED": "Отхвърлен", - "NO_RESULT": "Няма резултат", + "NO_RESULT": "Няма Резултат", "CLIENT": "Клиент", "INTERNAL": "Вътрешен", "START": "Начало", "END": "Край", - "REQUEST_DATE": "Дата на заявка", + "REQUEST_DATE": "Дата на Заявка", "REGION": { "BG": "Български (България)", "EN": "English (United States)", @@ -307,16 +306,28 @@ }, "CURRENT_VALUE": "Текуща стойност", "TARGET_VALUE": "Целева стойност", - "LAST_UPDATED": "Последна актуализация", + "LAST_UPDATED": "Последно обновено", + "LAST_SYNC_DATE": "Дата на последна синхронизация", "CREATED_BY": "Създадено от", "NO_DATA_MESSAGE": "Няма данни", "TAGS": "Тагове", + "LABELS": "Етикети", "CREATED": "Създадено", "APPLIED_DATE": "Дата на кандидатстване", "HIRED_DATE": "Дата на наемане", "REJECTED_DATE": "Дата на отхвърляне", "TIME_TRACKING": "Проследяване на време", - "CREATED_AT": "Създадено на" + "CREATED_AT": "Създадено на", + "SCREEN_CAPTURE": "Заснемане на екрана", + "NUMBER": "Номер", + "PROVIDER": "Доставчик", + "GITHUB_REPOSITORY": "GitHub Репозитори", + "ISSUES_SYNC": "Синхронизиране на проблеми", + "ISSUES_SYNC_COUNT": "{{ count }} Проблеми синхронизирани", + "RESYNC_ISSUES": "Повторна синхронизация на проблеми", + "ENABLED_DISABLED_SYNC": "Активирана / Деактивирана синхронизация", + "ENABLE_DISABLE_INTEGRATION": "Активиране / Деактивиране на интеграция", + "ACTIONS": "Действия" }, "FORM": { "USERNAME": "Потребителско име", @@ -1210,46 +1221,49 @@ "STANDARD_WORK_HOURS": "Стандартни работни часове ({{hours}})" }, "INTEGRATIONS": { + "TITLE": "Интеграции", "AVAILABLE_INTEGRATIONS": "Налични приложения и интеграции", - "ADDED_UPWORK_TRANSACTION": "Добавена транзакция от Upwork", - "TOTAL_UPWORK_TRANSACTIONS_SUCCEED": "Общо успешни транзакции за разходи: {{ totalExpenses }}. Общо успешни транзакции за приходи: {{ totalIncomes }}", + "ADDED_UPWORK_TRANSACTION": "Добавена Upwork транзакция", + "TOTAL_UPWORK_TRANSACTIONS_SUCCEED": "Общо успешни разходни транзакции: {{ totalExpenses }}. Общо успешни приходи транзакции: {{ totalIncomes }}", "HUBSTAFF_PAGE": { "TITLE": "Hubstaff", "SELECT_ORGANIZATION": "Изберете организация", "SYNCED_PROJECTS": "Синхронизирани проекти", - "SETTINGS_UPDATED": "Актуализирани настройки за интеграция", - "SYNCED_ENTITIES": "Автоматично синхронизирани единици", - "TOOLTIP_ACTIVITY_INFO": "Ограничение на периода: 7 дни Най-ранна дата: 6 пълни месеца", + "SETTINGS_UPDATED": "Настройките за интеграцията са актуализирани", + "SYNCED_ENTITIES": "Автоматично синхронизирани обекти", + "TOOLTIP_ACTIVITY_INFO": "Ограничение на обхвата на датите: 7 дни Най-ранна дата: 6 пълни месеца", "DATE_RANGE_PLACEHOLDER": "Изберете дата между", "CLIENT_ID": "Hubstaff клиентски идентификатор", - "CLIENT_SECRET": "Hubstaff клиентска тайна", - "GRANT_PERMISSION": "След това ще бъдете пренасочени към Hubstaff, за да предоставите разрешение на Gauzy.", - "ENTER_CLIENT_SECRET": "Въведете клиентския тайния ключ, за да получите достъп до токен." + "CLIENT_SECRET": "Hubstaff клиентски таен ключ", + "GRANT_PERMISSION": "Следващата стъпка е да бъдете прехвърлени в Hubstaff за предоставяне на разрешение на Gauzy.", + "ENTER_CLIENT_SECRET": "Въведете таен ключ, за да получите достъп до токен.", + "DESCRIPTION": "Активирайте Hubstaff интеграцията за подобрено управление на работната сила." }, "UPWORK_PAGE": { - "ACTIVITIES": "Активности", - "REPORTS": "Доклади", + "ACTIVITIES": "Дейности", + "REPORTS": "Отчети", "TRANSACTIONS": "Транзакции", - "SUCCESSFULLY_AUTHORIZED": "Успешно упълномощен", + "SUCCESSFULLY_AUTHORIZED": "Успешно оторизиран", "API_KEY": "Upwork API ключ", "SECRET": "Upwork таен ключ", - "NEXT_STEP_INFO": "След това ще бъдете пренасочени към Upwork, за да предоставите разрешение на Gauzy.", + "NEXT_STEP_INFO": "Следващата стъпка е да бъдете прехвърлени в Upwork за предоставяне на разрешение на Gauzy.", "CONTRACTS": "Договори", "SYNCED_CONTRACTS": "Синхронизирани договори", "SELECT_DATE": "Изберете дата", "ONLY_CONTRACTS": "Само договори", - "CONTRACTS_RELATED_DATA": "Синхронизирани данни, свързани с договори", + "CONTRACTS_RELATED_DATA": "Синхронизирани обекти, свързани с договори", "DATE_RANGE_PLACEHOLDER": "Изберете дата между", - "HOURLY": "Часов" + "HOURLY": "По час", + "DESCRIPTION": "Активирайте Upwork интеграцията за управление на фрийланс работна сила." }, "GAUZY_AI_PAGE": { - "TITLE": "Интеграция на Gauzy AI", + "TITLE": "Gauzy AI Интеграция", "API_KEY": "Gauzy AI ключ", "API_SECRET": "Gauzy AI таен ключ", - "OPEN_AI_API_SECRET_KEY": "Тайният ключ на OpenAI", - "OPEN_AI_ORGANIZATION_ID": "ID на OpenAI организация", - "DESCRIPTION": "Активирайте интеграцията с Gauzy AI за по-интелигентно търсене на работа.", - "CONSUMER_KEYS": "Потребителски ключове", + "OPEN_AI_API_SECRET_KEY": "Open AI таен ключ", + "OPEN_AI_ORGANIZATION_ID": "Open AI идентификатор на организация", + "DESCRIPTION": "Активирайте Gauzy AI интеграцията за по-умно търсене на работа.", + "CONSUMER_KEYS": "Ключове на потребители", "OPEN_AI_API_KEYS": "Open AI API ключове", "GENERATED": "Генерирани", "TAB": { @@ -1257,43 +1271,46 @@ "SETTINGS": "Настройки" }, "TOOLTIP": { - "API_KEY": "API ключът служи като идентификатор на вашето приложение за API заявки, оставайки постоянно скрит с някои видими символи.", - "API_SECRET": "API тайният ключ служи като идентификатор на вашето приложение за API заявки, оставайки постоянно скрит с някои видими символи.", - "OPEN_AI_API_SECRET_KEY": "Тайният API ключ на OpenAI служи като идентификатор на вашето приложение за API заявки, оставайки постоянно скрит с някои видими символи.", - "OPEN_AI_ORGANIZATION_ID": "Незадължителен идентификатор за организационни цели при взаимодействие с OpenAI API. Помага за организиране и разграничаване на различни обекти или проекти във вашата организация.", - "ENABLE_JOBS_SEARCH_MATCHING_ANALYSIS": "Активира разширен анализ на търсенето на работа и съвпадение за по-голяма точност. При изключване скрива пунктовете 'Преглед' и 'Съвпадение' за по-лесен интерфейс.", - "ENABLE_EMPLOYEE_PERFORMANCE_ANALYSIS": "Управлява предаването на метрики за работата на служителите, включително клавишни въвеждания, движения на мишката и снимки на екрана, за анализ от Gauzy AI. Включването позволява изчерпателен анализ на производителността. При изключване Gauzy AI няма да получава и анализира снимки или други подробни данни, за да гарантира строга конфиденциалност и контрол върху споделянето на данни." + "API_KEY": "API ключът служи като идентификатор на вашето приложение за API заявки и остава скрит, с видими само някои символи.", + "API_SECRET": "API тайният ключ служи като идентификатор на вашето приложение за API заявки и остава скрит, с видими само някои символи.", + "OPEN_AI_API_SECRET_KEY": "OpenAI API тайният ключ служи като идентификатор на вашето приложение за API заявки и остава скрит, с видими само някои символи.", + "OPEN_AI_ORGANIZATION_ID": "Незадължителен идентификатор за организационни цели при взаимодействие с OpenAI API. Той помага за организиране и разграничаване на различни обекти или проекти във вашата организация.", + "ENABLE_JOBS_SEARCH_MATCHING_ANALYSIS": "Активира анализ на търсенето и съвпадението на работни места за по-голяма точност. Деактивирането скрива елементите от менюто 'Browse' и 'Matching' за по-опростен интерфейс.", + "ENABLE_EMPLOYEE_PERFORMANCE_ANALYSIS": "Управлява предаването на работните метрики на служителите, включително клавиатурни въведения, движения на мишката и екранни снимки, за анализ от Gauzy AI. Активирайте за всеобхватен анализ на ефективността; деактивирайте за строга поверителност и контрол на данните." + }, + "MESSAGE": { + "JOBS_SEARCH_MATCHING_ENABLED": "Анализът на търсенето и съвпадението на работни места е активиран." } }, "GITHUB_PAGE": { - "TITLE": "GitHub Интеграция", - "AUTO_SYNC_TABLE_LABEL": "Синхронизиране на GitHub репозитории и проекти", - "SELECT_REPOSITORY": "Изберете репозитория", - "SEARCH_REPOSITORY": "Напишете, за да търсите репозитория", - "SYNCED_ISSUES": "'{{repository}}' проблеми и етикети успешно синхронизирани", - "HAS_SYNCED_ENABLED": "'{{repository}}' синхронизация успешно активирана", - "HAS_SYNCED_DISABLED": "'{{repository}}' синхронизация успешно деактивирана", - "DESCRIPTION": "Активирайте интеграцията с GitHub за синхронизиране на проекти и репозитории.", + "TITLE": "Интеграция с GitHub", + "AUTO_SYNC_TABLE_LABEL": "Синхронизиране на GitHub хранилища и проекти", + "SELECT_REPOSITORY": "Изберете хранилище", + "SEARCH_REPOSITORY": "Въведете за търсене на хранилище", + "SYNCED_ISSUES": "Задачи и етикети на '{{repository}}' синхронизирани успешно", + "HAS_SYNCED_ENABLED": "Синхронизацията на '{{repository}}' е успешно активирана", + "HAS_SYNCED_DISABLED": "Синхронизацията на '{{repository}}' е успешно деактивирана", + "DESCRIPTION": "Активирайте GitHub интеграцията за синхронизация на проекти и хранилища.", "TAB": { - "AUTO_SYNC": "Автоматична синхронизация", + "AUTO_SYNC": "Авто синхронизация", "MANUAL_SYNC": "Ръчна синхронизация" } }, - "COMING_SOON": "Скоро", - "RE_INTEGRATE": "Реинтеграция", + "COMING_SOON": "Очаквайте скоро", + "RE_INTEGRATE": "Повторно интегриране", "SETTINGS": "Настройки", "SELECT_GROUPS": "Изберете групи", - "FILTER_INTEGRATIONS": "Филтрирай интеграции", + "FILTER_INTEGRATIONS": "Филтриране на интеграции", "SEARCH_INTEGRATIONS": "Търсене на интеграции", "PAID": "Платено", "INTEGRATION": "Интеграция", "MESSAGE": { - "SETTINGS_UPDATED": "Актуализирани настройки за интеграция '{{provider}}'", - "INTEGRATION_DELETED": "Интеграция '{{provider}}' успешно изтрита", + "SETTINGS_UPDATED": "Настройките за интеграцията с '{{provider}}' бяха актуализирани", + "INTEGRATION_DELETED": "Интеграцията с '{{provider}}' беше успешно изтрита", "NO_INTEGRATIONS": "Не сте конфигурирали никакви интеграции", - "INTEGRATION_ENABLED": "Интеграция '{{provider}}' успешно активирана", - "INTEGRATION_DISABLED": "Интеграция '{{provider}}' успешно деактивирана", - "INTEGRATION_ADDED": "Интеграция '{{provider}}' е добавена към '{{organization}}'" + "INTEGRATION_ENABLED": "Интеграцията с '{{provider}}' беше успешно активирана", + "INTEGRATION_DISABLED": "Интеграцията с '{{provider}}' беше успешно деактивирана", + "INTEGRATION_ADDED": "Интеграцията с '{{provider}}' беше добавена към '{{organization}}'" }, "ENABLED": "Активирано", "DISABLED": "Деактивирано" diff --git a/packages/ui-core/package.json b/packages/ui-core/package.json index 2db0a57d70c..dc22c9fbb5f 100644 --- a/packages/ui-core/package.json +++ b/packages/ui-core/package.json @@ -77,7 +77,7 @@ "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", "@sentry/angular-ivy": "^7.101.1", - "angular2-smart-table": "^3.2.0", + "angular2-smart-table": "^3.3.0", "bootstrap": "^4.3.1", "camelcase": "^6.3.0", "chart.js": "^4.4.1", diff --git a/packages/ui-core/shared/src/lib/directives/time-tracking-authorized-directive.ts b/packages/ui-core/shared/src/lib/directives/time-tracking-authorized-directive.ts index 954b1d0f4e7..fbb6c2f4045 100644 --- a/packages/ui-core/shared/src/lib/directives/time-tracking-authorized-directive.ts +++ b/packages/ui-core/shared/src/lib/directives/time-tracking-authorized-directive.ts @@ -1,8 +1,8 @@ import { ChangeDetectorRef, Directive, Input, OnInit, TemplateRef, ViewContainerRef } from '@angular/core'; -import { filter, tap } from 'rxjs/operators'; +import { filter, tap, map, switchMap, distinctUntilChanged } from 'rxjs/operators'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import * as camelcase from 'camelcase'; -import { IOrganization } from '@gauzy/contracts'; +import { IOrganization, IUser } from '@gauzy/contracts'; import { distinctUntilChange } from '@gauzy/ui-core/common'; import { Store } from '@gauzy/ui-core/core'; @@ -11,19 +11,23 @@ import { Store } from '@gauzy/ui-core/core'; selector: '[ngxTimeTrackingAuthorized]' }) export class TimeTrackingAuthorizedDirective implements OnInit { - /* - * Getter & Setter for dynamic permission + private _permission: string | string[] = []; // Default initialization + /** + * Setter for dynamic permission. + * @param permission - The permission(s) to be set. */ - _permission: string | string[]; - get permission(): string | string[] { - return this._permission; - } @Input() set permission(permission: string | string[]) { if (!permission) { - throw false; + throw new Error('Permission must be provided'); } this._permission = permission; } + /** + * Getter for dynamic permission. + */ + get permission(): string | string[] { + return this._permission; + } @Input() permissionElse: TemplateRef; @@ -39,10 +43,27 @@ export class TimeTrackingAuthorizedDirective implements OnInit { .pipe( distinctUntilChange(), filter((organization: IOrganization) => !!organization), - filter((organization: IOrganization) => camelcase(this.permission) in organization), - tap(() => this._viewContainer.clear()), - tap((organization: IOrganization) => { - if (organization[camelcase(this.permission)]) { + switchMap((organization: IOrganization) => + this._store.user$.pipe( + filter((user: IUser) => !!user), + map((user: IUser) => { + // Determine permission based on employee existence + const hasPermission = user.employee + ? camelcase(this.permission) in organization && + organization[camelcase(this.permission)] && + camelcase(this.permission) in user.employee && + user.employee[camelcase(this.permission)] + : camelcase(this.permission) in organization && + organization[camelcase(this.permission)]; + + return hasPermission; + }), + distinctUntilChanged() // Only emit when permission status changes + ) + ), + tap((hasPermission: boolean) => { + if (hasPermission) { + this._viewContainer.clear(); // Clear the container once per status change this._viewContainer.createEmbeddedView(this._templateRef); } else { this.showTemplateBlockInView(this.permissionElse); @@ -60,10 +81,11 @@ export class TimeTrackingAuthorizedDirective implements OnInit { * @returns */ showTemplateBlockInView(template: TemplateRef) { - this._viewContainer.clear(); + this._viewContainer.clear(); // Clear the container once per status change if (!template) { return; } + this._viewContainer.createEmbeddedView(template); this._cdr.markForCheck(); } diff --git a/packages/ui-core/shared/src/lib/pipes/date-format.pipe.ts b/packages/ui-core/shared/src/lib/pipes/date-format.pipe.ts index 07aad42a282..8bd89ca8a2a 100644 --- a/packages/ui-core/shared/src/lib/pipes/date-format.pipe.ts +++ b/packages/ui-core/shared/src/lib/pipes/date-format.pipe.ts @@ -2,8 +2,8 @@ import { Pipe, PipeTransform } from '@angular/core'; import { filter, tap } from 'rxjs/operators'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import * as moment from 'moment'; -import { IOrganization, RegionsEnum } from '@gauzy/contracts'; -import { distinctUntilChange, isEmpty } from '@gauzy/ui-core/common'; +import { IOrganization, LanguagesEnum, RegionsEnum } from '@gauzy/contracts'; +import { distinctUntilChange } from '@gauzy/ui-core/common'; import { Store } from '@gauzy/ui-core/core'; @UntilDestroy({ checkProperties: true }) @@ -14,6 +14,7 @@ import { Store } from '@gauzy/ui-core/core'; export class DateFormatPipe implements PipeTransform { dateFormat: string = 'd MMMM, y'; regionCode: string = RegionsEnum.EN; + locale: string; constructor(private readonly store: Store) { this.store.selectedOrganization$ @@ -27,6 +28,17 @@ export class DateFormatPipe implements PipeTransform { untilDestroyed(this) ) .subscribe(); + + this.store.preferredLanguage$ + .pipe( + distinctUntilChange(), + filter((preferredLanguage: LanguagesEnum) => !!preferredLanguage), + tap((preferredLanguage: LanguagesEnum) => { + this.locale = preferredLanguage; + }), + untilDestroyed(this) + ) + .subscribe(); } /** @@ -35,35 +47,32 @@ export class DateFormatPipe implements PipeTransform { * @param {Date | string | number | null | undefined} value - The value to transform. Can be a Date object, string, number, or null/undefined. * @param {string} [locale] - The locale to use for formatting. If not provided, the default region code will be used. * @param {string} [defaultFormat] - The format to apply to the date. If not provided, the default date format will be used. - * @return {string | undefined} The formatted date string, or undefined if the value is falsy. + * @return {string | undefined} The formatted date string, or undefined if the value is falsy or invalid. */ transform( value: Date | string | number | null | undefined, locale?: string, defaultFormat?: string ): string | undefined { - if (!value) { - return; - } + // Return undefined if no value provided + if (!value) return; + // Parse date and check if it's valid let date = moment(new Date(value)); if (!date.isValid()) { date = moment.utc(value); } - if (isEmpty(locale)) { - locale = this.regionCode; - } + // If still invalid, return undefined + if (!date.isValid()) return; - if (date && defaultFormat) { - /** - * Override default format to organization date format as a priority format - */ - return date.locale(locale).format(defaultFormat); - } else if (date && this.dateFormat) { - return date.locale(locale).format(this.dateFormat); - } + // Set locale to the given locale or fallback to instance's locale or region code + locale = locale || this.locale || this.regionCode; + + // Determine the format to use: defaultFormat, or fallback to instance date format + const format = defaultFormat || this.dateFormat; - return; + // Return formatted date based on locale and format + return date.locale(locale).format(format); } } diff --git a/packages/ui-core/shared/src/lib/smart-data-layout/index.ts b/packages/ui-core/shared/src/lib/smart-data-layout/index.ts index 9b4429ecf12..4858e8493bd 100644 --- a/packages/ui-core/shared/src/lib/smart-data-layout/index.ts +++ b/packages/ui-core/shared/src/lib/smart-data-layout/index.ts @@ -4,7 +4,6 @@ export * from './no-data-message/no-data-message.module'; // Components export * from './no-data-message/no-data-message.component'; -export * from './smart-table-toggle/smart-table-toggle.component'; export * from './pagination/pagination.component'; export * from './pagination/pagination-filter-base.component'; export * from './pagination/pagination-v2/pagination-v2.component'; diff --git a/packages/ui-core/shared/src/lib/smart-data-layout/smart-data-view-layout.module.ts b/packages/ui-core/shared/src/lib/smart-data-layout/smart-data-view-layout.module.ts index 698d24bd796..a8269b53376 100644 --- a/packages/ui-core/shared/src/lib/smart-data-layout/smart-data-view-layout.module.ts +++ b/packages/ui-core/shared/src/lib/smart-data-layout/smart-data-view-layout.module.ts @@ -1,5 +1,4 @@ import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; import { NbIconModule, NbSelectModule, NbToggleModule } from '@nebular/theme'; import { TranslateModule } from '@ngx-translate/core'; import { Angular2SmartTableModule } from 'angular2-smart-table'; @@ -9,19 +8,16 @@ import { GauzyButtonActionModule } from '../gauzy-button-action/gauzy-button-act import { NoDataMessageModule } from './no-data-message/no-data-message.module'; import { PaginationComponent } from './pagination/pagination.component'; import { PaginationV2Component } from './pagination/pagination-v2/pagination-v2.component'; -import { SmartTableToggleComponent } from './smart-table-toggle/smart-table-toggle.component'; - -// Nebular Modules -const NB_MODULES = [NbToggleModule, NbIconModule, NbSelectModule]; // Components -const COMPONENTS = [PaginationComponent, PaginationV2Component, SmartTableToggleComponent]; +const COMPONENTS = [PaginationComponent, PaginationV2Component]; @NgModule({ declarations: [...COMPONENTS], imports: [ - CommonModule, - ...NB_MODULES, + NbToggleModule, + NbIconModule, + NbSelectModule, TranslateModule.forChild(), NgxPermissionsModule.forChild(), Angular2SmartTableModule, diff --git a/packages/ui-core/shared/src/lib/smart-data-layout/smart-table-toggle/smart-table-toggle.component.html b/packages/ui-core/shared/src/lib/smart-data-layout/smart-table-toggle/smart-table-toggle.component.html deleted file mode 100644 index 8b830e016d5..00000000000 --- a/packages/ui-core/shared/src/lib/smart-data-layout/smart-table-toggle/smart-table-toggle.component.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - -
    diff --git a/packages/ui-core/shared/src/lib/smart-data-layout/smart-table-toggle/smart-table-toggle.component.spec.ts b/packages/ui-core/shared/src/lib/smart-data-layout/smart-table-toggle/smart-table-toggle.component.spec.ts deleted file mode 100644 index 0f0476daeba..00000000000 --- a/packages/ui-core/shared/src/lib/smart-data-layout/smart-table-toggle/smart-table-toggle.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { SmartTableToggleComponent } from './smart-table-toggle.component'; - -describe('SmartTableToggleComponent', () => { - let component: SmartTableToggleComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [SmartTableToggleComponent], - teardown: { destroyAfterEach: false } - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(SmartTableToggleComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/packages/ui-core/shared/src/lib/smart-data-layout/smart-table-toggle/smart-table-toggle.component.ts b/packages/ui-core/shared/src/lib/smart-data-layout/smart-table-toggle/smart-table-toggle.component.ts deleted file mode 100644 index 6aeef4a914f..00000000000 --- a/packages/ui-core/shared/src/lib/smart-data-layout/smart-table-toggle/smart-table-toggle.component.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; - -@Component({ - selector: 'ngx-smart-table-toggle-switcher', - templateUrl: './smart-table-toggle.component.html', - styleUrls: ['./smart-table-toggle.component.scss'] -}) -export class SmartTableToggleComponent { - private _checked$: BehaviorSubject = new BehaviorSubject(false); - public get checked$(): Observable { - return this._checked$.asObservable(); - } - - /** - * Set the value of the toggle. - * @param checked The value to set the toggle to. - */ - @Input() public set value(checked: boolean) { - this._checked$.next(checked); - } - - /** - * Get the value of the toggle. - */ - @Output() toggleChange: EventEmitter = new EventEmitter(); - - /** - * Set the value of the toggle. - * @param isChecked - */ - onCheckedChange(isChecked: boolean) { - this.toggleChange.emit(isChecked); - this._checked$.next(isChecked); - } -} diff --git a/packages/ui-core/shared/src/lib/table-components/editors/index.ts b/packages/ui-core/shared/src/lib/table-components/editors/index.ts index 790692b98b1..4579c69546c 100644 --- a/packages/ui-core/shared/src/lib/table-components/editors/index.ts +++ b/packages/ui-core/shared/src/lib/table-components/editors/index.ts @@ -1,2 +1,4 @@ export * from './number-editor.component'; export * from './employee-link-editor.component'; +export * from './job-search-availability-editor.component'; +export * from './non-editable-number-editor.component'; diff --git a/packages/ui-core/shared/src/lib/table-components/editors/job-search-availability-editor.component.ts b/packages/ui-core/shared/src/lib/table-components/editors/job-search-availability-editor.component.ts new file mode 100644 index 00000000000..411ac536866 --- /dev/null +++ b/packages/ui-core/shared/src/lib/table-components/editors/job-search-availability-editor.component.ts @@ -0,0 +1,74 @@ +import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, ViewChild } from '@angular/core'; +import { filter, tap } from 'rxjs'; +import { Cell, DefaultEditor } from 'angular2-smart-table'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { IEmployee, IOrganization } from '@gauzy/contracts'; +import { distinctUntilChange } from '@gauzy/ui-core/common'; +import { JobSearchStoreService, Store } from '@gauzy/ui-core/core'; +import { ToggleSwitcherComponent } from '../toggle-switcher/toggle-switcher.component'; + +@UntilDestroy({ checkProperties: true }) +@Component({ + template: ` + + ` +}) +export class JobSearchAvailabilityEditorComponent extends DefaultEditor implements AfterViewInit, OnInit { + public organization: IOrganization; + public employee: IEmployee; + + // Reference to the cell object + @Input() cell!: Cell; + + // Reference to the ToggleSwitcherComponent instance + @ViewChild(ToggleSwitcherComponent) switcher!: ToggleSwitcherComponent; + + constructor( + private readonly _cdr: ChangeDetectorRef, + private readonly _store: Store, + private readonly _jobSearchStoreService: JobSearchStoreService + ) { + super(); + } + + ngOnInit() { + this._store.selectedOrganization$ + .pipe( + distinctUntilChange(), + filter((organization: IOrganization) => !!organization), + tap((organization: IOrganization) => { + this.organization = organization; + this.employee = this.cell.getRow()?.getData(); + }), + untilDestroyed(this) + ) + .subscribe(); + } + + ngAfterViewInit(): void { + if (!this.switcher) { + return; + } + this.switcher.value = this.employee?.isJobSearchActive || false; + this._cdr.detectChanges(); // Force change detection to update the UI + } + + /** + * Updates the job search availability status of an employee within the organization. + * + * @param isJobSearchActive - A boolean flag indicating whether the job search is active. + */ + updateJobSearchAvailability(isJobSearchActive: boolean): void { + try { + // Call the service to update the job search availability status + this._jobSearchStoreService.updateJobSearchAvailability( + this.organization, + this.employee, + isJobSearchActive + ); + } catch (error) { + // Log the error for debugging purposes + console.log('Error while updating job search availability:', error); + } + } +} diff --git a/packages/ui-core/shared/src/lib/table-components/editors/non-editable-number-editor.component.ts b/packages/ui-core/shared/src/lib/table-components/editors/non-editable-number-editor.component.ts new file mode 100644 index 00000000000..c8b85b59518 --- /dev/null +++ b/packages/ui-core/shared/src/lib/table-components/editors/non-editable-number-editor.component.ts @@ -0,0 +1,28 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Cell, DefaultEditor } from 'angular2-smart-table'; + +@Component({ + template: ` +
    + {{ cellValue }} +
    + ` +}) +export class NonEditableNumberEditorComponent extends DefaultEditor implements OnInit { + cellValue!: string | number; + + @Input() cell!: Cell; + + ngOnInit() { + const value = this.cell.getValue(); + if (value === null || value === undefined) { + console.warn('Cell value is null or undefined'); + this.cellValue = ''; + } else if (typeof value === 'number' || typeof value === 'string') { + this.cellValue = value; + } else { + console.error('Unexpected cell value type:', typeof value); + this.cellValue = ''; + } + } +} diff --git a/packages/ui-core/shared/src/lib/table-components/index.ts b/packages/ui-core/shared/src/lib/table-components/index.ts index b79500375cd..d92662303b5 100644 --- a/packages/ui-core/shared/src/lib/table-components/index.ts +++ b/packages/ui-core/shared/src/lib/table-components/index.ts @@ -11,6 +11,7 @@ export * from './document-date/document-date.component'; export * from './document-url/document-url.component'; export * from './editors/number-editor.component'; export * from './editors/employee-link-editor.component'; +export * from './editors/job-search-availability-editor.component'; export * from './email/email.component'; export * from './employee-links/employee-links.component'; export * from './employee-with-links/employee-with-links.component'; @@ -35,10 +36,11 @@ export * from './status-view/status-view.component'; export * from './tags-only/tags-only.component'; export * from './task-estimate/task-estimate.component'; export * from './task-teams/task-teams.component'; -export * from './toggle-switch/toggle-switch.component'; +export * from './toggle-switcher/toggle-switcher.component'; export * from './trust-html/trust-html.component'; export * from './value-with-units/value-with-units.component'; export * from './visibility/visibility.component'; +export * from './editors/non-editable-number-editor.component'; -// +// Export the table components main module export * from './table-components.module'; diff --git a/packages/ui-core/shared/src/lib/table-components/table-components.module.ts b/packages/ui-core/shared/src/lib/table-components/table-components.module.ts index ccb528adb55..cffde8e7716 100644 --- a/packages/ui-core/shared/src/lib/table-components/table-components.module.ts +++ b/packages/ui-core/shared/src/lib/table-components/table-components.module.ts @@ -5,7 +5,12 @@ import { NbIconModule, NbTooltipModule, NbBadgeModule, NbToggleModule, NbButtonM import { TranslateModule } from '@ngx-translate/core'; import { ComponentsModule } from '../components/components.module'; import { PipesModule } from '../pipes/pipes.module'; -import { EmployeeLinkEditorComponent, NumberEditorComponent } from './editors'; +import { + EmployeeLinkEditorComponent, + JobSearchAvailabilityEditorComponent, + NonEditableNumberEditorComponent, + NumberEditorComponent +} from './editors'; import { AllowScreenshotCaptureComponent } from './allow-screenshot-capture/allow-screenshot-capture.component'; import { AssignedToComponent } from './assigned-to/assigned-to.component'; import { ClickableLinkComponent } from './clickable-link/clickable-link.component'; @@ -41,7 +46,7 @@ import { StatusViewComponent } from './status-view/status-view.component'; import { TagsOnlyComponent } from './tags-only/tags-only.component'; import { TaskEstimateComponent } from './task-estimate/task-estimate.component'; import { TaskTeamsComponent } from './task-teams/task-teams.component'; -import { ToggleSwitchComponent } from './toggle-switch/toggle-switch.component'; +import { ToggleSwitcherComponent } from './toggle-switcher/toggle-switcher.component'; import { TrustHtmlLinkComponent } from './trust-html/trust-html.component'; import { ValueWithUnitComponent } from './value-with-units/value-with-units.component'; import { VisibilityComponent } from './visibility/visibility.component'; @@ -101,10 +106,12 @@ import { TaskBadgeViewComponentModule } from '../tasks/task-badge-view/task-badg TagsOnlyComponent, TaskEstimateComponent, TaskTeamsComponent, - ToggleSwitchComponent, + ToggleSwitcherComponent, TrustHtmlLinkComponent, ValueWithUnitComponent, - VisibilityComponent + VisibilityComponent, + NonEditableNumberEditorComponent, + JobSearchAvailabilityEditorComponent ], exports: [ AllowScreenshotCaptureComponent, @@ -129,6 +136,8 @@ import { TaskBadgeViewComponentModule } from '../tasks/task-badge-view/task-badg InvoiceTotalValueComponent, NotesWithTagsComponent, NumberEditorComponent, + JobSearchAvailabilityEditorComponent, + NonEditableNumberEditorComponent, OrganizationWithTagsComponent, PhoneUrlComponent, PictureNameTagsComponent, @@ -143,7 +152,7 @@ import { TaskBadgeViewComponentModule } from '../tasks/task-badge-view/task-badg TagsOnlyComponent, TaskEstimateComponent, TaskTeamsComponent, - ToggleSwitchComponent, + ToggleSwitcherComponent, TrustHtmlLinkComponent, ValueWithUnitComponent, VisibilityComponent diff --git a/packages/ui-core/shared/src/lib/table-components/toggle-switch/toggle-switch.component.html b/packages/ui-core/shared/src/lib/table-components/toggle-switch/toggle-switch.component.html deleted file mode 100644 index 42fa0be455b..00000000000 --- a/packages/ui-core/shared/src/lib/table-components/toggle-switch/toggle-switch.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - - {{ ((toggle_switch$ | async) ? 'BUTTONS.ENABLED' : 'BUTTONS.DISABLED') | translate }} - - diff --git a/packages/ui-core/shared/src/lib/table-components/toggle-switcher/toggle-switcher.component.html b/packages/ui-core/shared/src/lib/table-components/toggle-switcher/toggle-switcher.component.html new file mode 100644 index 00000000000..66e06c8acf3 --- /dev/null +++ b/packages/ui-core/shared/src/lib/table-components/toggle-switcher/toggle-switcher.component.html @@ -0,0 +1,12 @@ +
    + + + {{ ((switcher$ | async) ? 'BUTTONS.ENABLED' : 'BUTTONS.DISABLED') | translate }} + + +
    diff --git a/packages/ui-core/shared/src/lib/table-components/toggle-switch/toggle-switch.component.ts b/packages/ui-core/shared/src/lib/table-components/toggle-switcher/toggle-switcher.component.ts similarity index 70% rename from packages/ui-core/shared/src/lib/table-components/toggle-switch/toggle-switch.component.ts rename to packages/ui-core/shared/src/lib/table-components/toggle-switcher/toggle-switcher.component.ts index 90e17345c9c..0b9b61739a3 100644 --- a/packages/ui-core/shared/src/lib/table-components/toggle-switch/toggle-switch.component.ts +++ b/packages/ui-core/shared/src/lib/table-components/toggle-switcher/toggle-switcher.component.ts @@ -1,28 +1,25 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; -import { Observable } from 'rxjs/internal/Observable'; -import { tap } from 'rxjs/operators'; +import { BehaviorSubject, Observable, tap } from 'rxjs'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; @UntilDestroy({ checkProperties: true }) @Component({ - selector: 'ngx-toggle-switch', - templateUrl: './toggle-switch.component.html' + selector: 'ngx-toggle-switcher', + templateUrl: './toggle-switcher.component.html' }) -export class ToggleSwitchComponent implements OnInit { - +export class ToggleSwitcherComponent implements OnInit { /** * A class member that represents a boolean switch or toggle using a BehaviorSubject. */ - private _toggle_switch$: BehaviorSubject = new BehaviorSubject(false); + private _switcher$: BehaviorSubject = new BehaviorSubject(false); /** * Getter method for retrieving the toggle switch state as an Observable. * * @returns An Observable that emits the current state and subsequent changes of the toggle switch. */ - public get toggle_switch$(): Observable { - return this._toggle_switch$.asObservable(); + public get switcher$(): Observable { + return this._switcher$.asObservable(); } /** @@ -45,7 +42,7 @@ export class ToggleSwitchComponent implements OnInit { */ @Input() set value(value: any) { // Updates the dynamic element's value using a BehaviorSubject or similar mechanism. - this._toggle_switch$.next(value); + this._switcher$.next(value); // Stores the value in the local variable for future reference. this._value = value; @@ -86,22 +83,22 @@ export class ToggleSwitchComponent implements OnInit { * * This is used to create a custom event named 'switched' that can be listened to by external components. */ - @Output() switched: EventEmitter = new EventEmitter(); - + @Output() onSwitched: EventEmitter = new EventEmitter(); - constructor() { } + constructor() {} /** * The ngOnInit lifecycle hook is called when the component is initialized. - * This method subscribes to the 'switched' Observable, and upon changes, updates the '_toggle_switch$' BehaviorSubject. + * This method subscribes to the 'switched' Observable, and upon changes, updates the '_switcher$' BehaviorSubject. */ ngOnInit(): void { - this.switched.pipe( - // The 'tap' operator allows side-effects without changing the emitted values. - tap((enable: boolean) => this._toggle_switch$.next(enable)), - // The 'untilDestroyed' operator helps to automatically unsubscribe when the component is destroyed. - untilDestroyed(this) - ).subscribe(); // Subscribe to the Observable but perform actions in 'tap'. + this.onSwitched + .pipe( + tap((enable: boolean) => this._switcher$.next(enable)), + // The 'untilDestroyed' operator helps to automatically unsubscribe when the component is destroyed. + untilDestroyed(this) + ) + .subscribe(); // Subscribe to the Observable but perform actions in 'tap'. } /** @@ -111,6 +108,6 @@ export class ToggleSwitchComponent implements OnInit { */ onCheckedChange(event: boolean) { // Emits the provided boolean 'event' using the 'switched' EventEmitter. - this.switched.emit(event); + this.onSwitched.emit(event); } } diff --git a/packages/ui-core/shared/src/lib/tasks/add-task-dialog/add-task-dialog.component.ts b/packages/ui-core/shared/src/lib/tasks/add-task-dialog/add-task-dialog.component.ts index b72717dcfa8..a86e6cffe27 100644 --- a/packages/ui-core/shared/src/lib/tasks/add-task-dialog/add-task-dialog.component.ts +++ b/packages/ui-core/shared/src/lib/tasks/add-task-dialog/add-task-dialog.component.ts @@ -115,7 +115,7 @@ export class AddTaskDialogComponent extends TranslationBaseComponent implements distinctUntilChange(), filter((employee: ISelectedEmployee) => !!employee && !!employee.id), tap((employee: ISelectedEmployee) => { - if (!this.task) { + if (!this.selectedTask) { this.selectedMembers.push(employee.id); } }), @@ -127,7 +127,7 @@ export class AddTaskDialogComponent extends TranslationBaseComponent implements distinctUntilChange(), filter((project: IOrganizationProject) => !!project && !!project.id), tap((project: IOrganizationProject) => { - if (!this.task) { + if (!this.selectedTask) { this.form.get('project').setValue(project); this.form.get('projectId').setValue(project.id); this.form.updateValueAndValidity(); diff --git a/packages/ui-core/theme/src/lib/components/gauzy-logo/gauzy-logo.component.html b/packages/ui-core/theme/src/lib/components/gauzy-logo/gauzy-logo.component.html index 5c347691c48..aba74b3f00c 100644 --- a/packages/ui-core/theme/src/lib/components/gauzy-logo/gauzy-logo.component.html +++ b/packages/ui-core/theme/src/lib/components/gauzy-logo/gauzy-logo.component.html @@ -1,37 +1,38 @@ - +
    -
    {{ organization?.name }}
    -
    {{ organization?.website }}
    +
    + {{ organization?.name }} +
    +
    + {{ organization?.website }} +
    - + - +
    - +
    - +
    +
    +
    0 @@ -494,10 +498,14 @@ export class HeaderComponent extends TranslationBaseComponent implements OnInit, // Extract organization and tenant IDs const { id: organizationId, tenantId } = this.organization; + // Include member if employeeId or store user's employeeId is provided + const employeeId = this.store.user.employee?.id || this.store.selectedEmployee?.id; + // Get team count const count = await this.organizationTeamsService.getCount({ organizationId, - tenantId + tenantId, + ...(employeeId && { members: { employeeId } }) }); // Show team selector if count > 0 diff --git a/packages/ui-core/theme/src/lib/themes/gauzy/theme.gauzy-dark.ts b/packages/ui-core/theme/src/lib/themes/gauzy/theme.gauzy-dark.ts index 698f56a65a1..9bf0e771154 100644 --- a/packages/ui-core/theme/src/lib/themes/gauzy/theme.gauzy-dark.ts +++ b/packages/ui-core/theme/src/lib/themes/gauzy/theme.gauzy-dark.ts @@ -50,7 +50,7 @@ export const GAUZY_DARK = { ...theme, temperature: { - arcFill: [theme.primary, theme.primary, theme.primary, theme.primary, theme.primary], + arcFill: Array(5).fill(theme.primary), arcEmpty: theme.bg2, thumbBg: theme.bg2, thumbBorder: theme.primary diff --git a/packages/ui-core/theme/src/lib/themes/gauzy/theme.gauzy-light.ts b/packages/ui-core/theme/src/lib/themes/gauzy/theme.gauzy-light.ts index 7d835b07239..0bce727a5fa 100644 --- a/packages/ui-core/theme/src/lib/themes/gauzy/theme.gauzy-light.ts +++ b/packages/ui-core/theme/src/lib/themes/gauzy/theme.gauzy-light.ts @@ -50,7 +50,7 @@ export const GAUZY_LIGHT = { ...theme, temperature: { - arcFill: [theme.primary, theme.primary, theme.primary, theme.primary, theme.primary], + arcFill: Array(5).fill(theme.primary), arcEmpty: theme.bg2, thumbBg: theme.bg2, thumbBorder: theme.primary diff --git a/packages/ui-core/theme/src/lib/themes/material/theme.material-dark.ts b/packages/ui-core/theme/src/lib/themes/material/theme.material-dark.ts index b598c295e91..7f7f639683e 100644 --- a/packages/ui-core/theme/src/lib/themes/material/theme.material-dark.ts +++ b/packages/ui-core/theme/src/lib/themes/material/theme.material-dark.ts @@ -54,13 +54,7 @@ export const MATERIAL_DARK_THEME = { base: 'default', variables: { temperature: { - arcFill: [ - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary - ], + arcFill: Array(5).fill(baseThemeVariables.primary), arcEmpty: baseThemeVariables.bg2, thumbBg: baseThemeVariables.bg2, thumbBorder: baseThemeVariables.primary diff --git a/packages/ui-core/theme/src/lib/themes/material/theme.material-light.ts b/packages/ui-core/theme/src/lib/themes/material/theme.material-light.ts index 3a174c63477..663fd07e371 100644 --- a/packages/ui-core/theme/src/lib/themes/material/theme.material-light.ts +++ b/packages/ui-core/theme/src/lib/themes/material/theme.material-light.ts @@ -54,13 +54,7 @@ export const MATERIAL_LIGHT_THEME = { base: 'default', variables: { temperature: { - arcFill: [ - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary, - baseThemeVariables.primary - ], + arcFill: Array(5).fill(baseThemeVariables.primary), arcEmpty: baseThemeVariables.bg2, thumbBg: baseThemeVariables.bg2, thumbBorder: baseThemeVariables.primary diff --git a/packages/ui-core/theme/src/lib/themes/theme.dark.ts b/packages/ui-core/theme/src/lib/themes/theme.dark.ts index 983d147d3f4..63e247fa5b4 100644 --- a/packages/ui-core/theme/src/lib/themes/theme.dark.ts +++ b/packages/ui-core/theme/src/lib/themes/theme.dark.ts @@ -49,13 +49,7 @@ export const DARK_THEME = { ...theme, temperature: { - arcFill: [ - theme.primary, - theme.primary, - theme.primary, - theme.primary, - theme.primary - ], + arcFill: Array(5).fill(theme.primary), arcEmpty: theme.bg2, thumbBg: theme.bg2, thumbBorder: theme.primary diff --git a/packages/ui-core/theme/src/lib/themes/theme.default.ts b/packages/ui-core/theme/src/lib/themes/theme.default.ts index 5caf7260d54..1f1cc57667b 100644 --- a/packages/ui-core/theme/src/lib/themes/theme.default.ts +++ b/packages/ui-core/theme/src/lib/themes/theme.default.ts @@ -49,7 +49,7 @@ export const DEFAULT_THEME = { ...theme, temperature: { - arcFill: [theme.primary, theme.primary, theme.primary, theme.primary, theme.primary], + arcFill: Array(5).fill(theme.primary), arcEmpty: theme.bg2, thumbBg: theme.bg2, thumbBorder: theme.primary diff --git a/yarn.lock b/yarn.lock index 3b27aa15432..d6b4e226314 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12964,12 +12964,12 @@ analytics@0.8.9: "@analytics/core" "^0.12.7" "@analytics/storage-utils" "^0.4.2" -angular2-smart-table@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/angular2-smart-table/-/angular2-smart-table-3.2.0.tgz#85f7e2d31c3e74c41a8d30ae5ad7a3aa87ada16b" - integrity sha512-4UPYY1kYpZbRMZFhCQaVRpTKhesbt42oX4APz09dbpusD3wlhOmPvSG4+U+Fxqy/7+SHtdzlVEf/L++NqCRWwQ== +angular2-smart-table@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/angular2-smart-table/-/angular2-smart-table-3.3.0.tgz#d5781ee3ec6e319d055d175dfb18aea123a8bd54" + integrity sha512-J2XiiiB7ugcn9jCuamA30UsphJ8dWtMA2gY2FX0NxGHFRzzKkP5KERIDEcMLDqMIY/QgqQJvn+XjWEETmMx76A== dependencies: - tslib "^2.6.0" + tslib "~2.7.0" angular2-toaster@^11.0.1: version "11.0.1" @@ -22539,9 +22539,9 @@ http-proxy-agent@^7.0.0: debug "^4.3.4" http-proxy-middleware@^2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" - integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + version "2.0.7" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6" + integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1" @@ -36087,7 +36087,7 @@ tslib@2.6.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== -tslib@2.6.2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0, tslib@^2.6.0, tslib@^2.6.1, tslib@^2.6.2: +tslib@2.6.2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0, tslib@^2.6.1, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -36097,6 +36097,11 @@ tslib@^1.10.0, tslib@^1.11.1, tslib@^1.13.0, tslib@^1.8.0, tslib@^1.8.1, tslib@^ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@~2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== + tslint-config-prettier@^1.18.0: version "1.18.0" resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37"