From 45966e727c447859e7b00264593687cec100d5bb Mon Sep 17 00:00:00 2001 From: samuelmbabhazi Date: Fri, 27 Sep 2024 15:31:02 +0200 Subject: [PATCH 01/14] [Bug] Roles / Permissions: Pre-Selected Toggle Buttons Not Retaining Enabled Permissions --- .../roles-permissions.component.html | 72 ++++--------------- .../roles-permissions.component.ts | 11 ++- 2 files changed, 23 insertions(+), 60 deletions(-) diff --git a/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.html b/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.html index 59754e1fccc..96f26ccd965 100644 --- a/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.html +++ b/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.html @@ -27,10 +27,7 @@

> - + -
+
- +
{{ 'BUTTONS.DELETE_EXISTING_ROLE' | translate : { name: role.name } }}
@@ -85,35 +74,20 @@

- {{ - 'ORGANIZATIONS_PAGE.PERMISSIONS.GROUPS.GENERAL' - | translate - }} + {{ 'ORGANIZATIONS_PAGE.PERMISSIONS.GROUPS.GENERAL' | translate }}
- {{ - 'ORGANIZATIONS_PAGE.PERMISSIONS.' + - permission | translate - }} - {{ - 'ORGANIZATIONS_PAGE.PERMISSIONS.' + - permission | translate - }} + {{ 'ORGANIZATIONS_PAGE.PERMISSIONS.' + permission | translate }} + {{ 'ORGANIZATIONS_PAGE.PERMISSIONS.' + permission | translate }}
@@ -122,15 +96,9 @@

{{ - 'ORGANIZATIONS_PAGE.PERMISSIONS.GROUPS.ADMINISTRATION' - | translate - }} + >{{ 'ORGANIZATIONS_PAGE.PERMISSIONS.GROUPS.ADMINISTRATION' | translate }} @@ -138,30 +106,18 @@

- {{ - 'ORGANIZATIONS_PAGE.PERMISSIONS.' + - permission | translate - }} - {{ - 'ORGANIZATIONS_PAGE.PERMISSIONS.' + - permission | translate - }} + {{ 'ORGANIZATIONS_PAGE.PERMISSIONS.' + permission | translate }} + {{ 'ORGANIZATIONS_PAGE.PERMISSIONS.' + permission | translate }}
diff --git a/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.ts b/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.ts index 1336d9b36f9..042227c5c37 100644 --- a/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.ts +++ b/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.ts @@ -145,9 +145,16 @@ export class RolesPermissionsComponent extends TranslationBaseComponent implemen }) .finally(() => (this.loading = false)) ).items; - + console.log(this.permissions[0].enabled); this.permissions.forEach((p) => { - this.enabledPermissions[p.permission] = p.enabled; + // Vérification si la permission existe déjà dans l'objet + if (this.enabledPermissions.hasOwnProperty(p.permission)) { + // Log des doublons rencontrés + console.log(`Doublon détecté pour la permission: ${p.permission}`); + } else { + // Si la permission n'existe pas, on l'ajoute + this.enabledPermissions[p.permission] = p.enabled; + } }); } From fb73b6bd2966a586e32db7c59cb95bdcf2a8a307 Mon Sep 17 00:00:00 2001 From: samuelmbabhazi Date: Fri, 27 Sep 2024 19:19:52 +0200 Subject: [PATCH 02/14] [Bug] "End Work" Status: Inactive User/Employee Record Not Updated --- .../src/app/pages/users/users.component.ts | 18 ++++++++++++++---- packages/core/src/employee/employee.service.ts | 4 ++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/apps/gauzy/src/app/pages/users/users.component.ts b/apps/gauzy/src/app/pages/users/users.component.ts index a2084f75a55..e90efaa0500 100644 --- a/apps/gauzy/src/app/pages/users/users.component.ts +++ b/apps/gauzy/src/app/pages/users/users.component.ts @@ -30,6 +30,7 @@ import { } from '@gauzy/contracts'; import { ComponentEnum, distinctUntilChange } from '@gauzy/ui-core/common'; import { + DateFormatPipe, DeleteConfirmationComponent, EmailComponent, IPaginationBase, @@ -78,7 +79,8 @@ export class UsersComponent extends PaginationFilterBaseComponent implements OnI private readonly route: ActivatedRoute, public readonly translateService: TranslateService, private readonly userOrganizationsService: UsersOrganizationsService, - private readonly employeesService: EmployeesService + private readonly employeesService: EmployeesService, + private readonly _dateFormatPipe: DateFormatPipe ) { super(translateService); this.setView(); @@ -358,7 +360,7 @@ export class UsersComponent extends PaginationFilterBaseComponent implements OnI tags: user.tags, imageUrl: user.imageUrl, role: user.role, - isActive, + isActive: user.employeeId ? user.employee.isActive : user.isActive, userOrganizationId, ...this.employeeMapper(user.employee) })); @@ -527,16 +529,24 @@ export class UsersComponent extends PaginationFilterBaseComponent implements OnI * @returns An object containing mapped employee properties. */ private employeeMapper(employee: IEmployee): any { + console.log(employee); + if (!employee) { return {}; } - const { endWork, startedWorkOn, isTrackingEnabled, id } = employee; + /** + * "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 { employeeId: id, endWork: endWork ? new Date(endWork) : null, - workStatus: endWork ? this.formatDate(new Date(endWork)) : '', + workStatus: endWork ? workStatus : '', startedWorkOn, isTrackingEnabled }; diff --git a/packages/core/src/employee/employee.service.ts b/packages/core/src/employee/employee.service.ts index e5b4d9c51f1..73a5d4809b6 100644 --- a/packages/core/src/employee/employee.service.ts +++ b/packages/core/src/employee/employee.service.ts @@ -105,8 +105,8 @@ export class EmployeeService extends TenantAwareCrudService { // Construct the base where clause for querying employees by user IDs const whereClause = { userId: In(userIds), // Find employees with matching user IDs - isActive: true, // Only active employees - isArchived: false, // Exclude archived employees + // isActive: true, // Only active employees + // isArchived: false, // Exclude archived employees ...(tenantId && { tenantId }) // Include tenant ID if available }; From 3285b11e2954ae20e4e5abfc1b4166552e2694b8 Mon Sep 17 00:00:00 2001 From: samuelmbabhazi Date: Fri, 27 Sep 2024 19:33:30 +0200 Subject: [PATCH 03/14] remove comment --- .../roles-permissions/roles-permissions.component.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.ts b/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.ts index 042227c5c37..e795c42d08e 100644 --- a/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.ts +++ b/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.ts @@ -145,14 +145,9 @@ export class RolesPermissionsComponent extends TranslationBaseComponent implemen }) .finally(() => (this.loading = false)) ).items; - console.log(this.permissions[0].enabled); + this.permissions.forEach((p) => { - // Vérification si la permission existe déjà dans l'objet - if (this.enabledPermissions.hasOwnProperty(p.permission)) { - // Log des doublons rencontrés - console.log(`Doublon détecté pour la permission: ${p.permission}`); - } else { - // Si la permission n'existe pas, on l'ajoute + if (!this.enabledPermissions.hasOwnProperty(p.permission)) { this.enabledPermissions[p.permission] = p.enabled; } }); From 7c77f7682fb15b6ef40950152f5cac9d17db5f5b Mon Sep 17 00:00:00 2001 From: Ruslan Konviser Date: Fri, 27 Sep 2024 21:27:07 +0200 Subject: [PATCH 04/14] Update users.component.ts --- apps/gauzy/src/app/pages/users/users.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/gauzy/src/app/pages/users/users.component.ts b/apps/gauzy/src/app/pages/users/users.component.ts index e90efaa0500..1f400f20bfc 100644 --- a/apps/gauzy/src/app/pages/users/users.component.ts +++ b/apps/gauzy/src/app/pages/users/users.component.ts @@ -529,8 +529,6 @@ export class UsersComponent extends PaginationFilterBaseComponent implements OnI * @returns An object containing mapped employee properties. */ private employeeMapper(employee: IEmployee): any { - console.log(employee); - if (!employee) { return {}; } From a3744b9d645dc8f0ead3c9e0406a41bd12b530cf Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Sat, 28 Sep 2024 16:22:06 +0530 Subject: [PATCH 05/14] fix: terminate service for specific organization --- .../src/app/pages/users/users.component.ts | 6 +- .../core/src/employee/employee.service.ts | 26 ++- .../core/src/employee/employee.subscriber.ts | 173 ++++++++++++------ .../user-organization.entity.ts | 8 +- .../user-organization.services.ts | 22 +-- 5 files changed, 146 insertions(+), 89 deletions(-) diff --git a/apps/gauzy/src/app/pages/users/users.component.ts b/apps/gauzy/src/app/pages/users/users.component.ts index 1f400f20bfc..3d7ca14a997 100644 --- a/apps/gauzy/src/app/pages/users/users.component.ts +++ b/apps/gauzy/src/app/pages/users/users.component.ts @@ -336,9 +336,7 @@ export class UsersComponent extends PaginationFilterBaseComponent implements OnI ); // Filter out user organizations that are not active or don't have a user with a role - return userOrganizations.items.filter( - (organization: IUserOrganization) => organization.isActive && organization.user?.role - ); + return userOrganizations.items.filter((organization: IUserOrganization) => organization.user?.role); } /** @@ -360,7 +358,7 @@ export class UsersComponent extends PaginationFilterBaseComponent implements OnI tags: user.tags, imageUrl: user.imageUrl, role: user.role, - isActive: user.employeeId ? user.employee.isActive : user.isActive, + isActive: isActive, userOrganizationId, ...this.employeeMapper(user.employee) })); diff --git a/packages/core/src/employee/employee.service.ts b/packages/core/src/employee/employee.service.ts index 323b7177bf5..62a41e88f2e 100644 --- a/packages/core/src/employee/employee.service.ts +++ b/packages/core/src/employee/employee.service.ts @@ -128,33 +128,31 @@ export class EmployeeService extends TenantAwareCrudService { /** * Finds employees based on an array of user IDs. + * * @param userIds An array of user IDs. + * @param tenantId The ID of the tenant to filter employees. * @returns A promise resolving to an array of employees. */ - async findEmployeesByUserIds(userIds: ID[]): Promise { + async findEmployeesByUserIds(userIds: ID[], tenantId: ID): Promise { try { - // Get the tenant ID from the current request context - const tenantId = RequestContext.currentTenantId(); - - // Construct the base where clause for querying employees by user IDs - const whereClause = { - userId: In(userIds), // Find employees with matching user IDs - // isActive: true, // Only active employees - // isArchived: false, // Exclude archived employees - ...(tenantId && { tenantId }) // Include tenant ID if available + // Define the options for the query + const options: FindManyOptions = { + // Construct the base where clause for querying employees by user IDs + where: { + userId: In(userIds), // Find employees with matching user IDs + tenantId // Find employees in the same tenant + } }; // Execute the query based on the ORM type switch (this.ormType) { case MultiORMEnum.MikroORM: { - const { where, mikroOptions } = parseTypeORMFindToMikroOrm({ - where: whereClause - } as FindManyOptions); + const { where, mikroOptions } = parseTypeORMFindToMikroOrm(options); const employees = await this.mikroOrmRepository.find(where, mikroOptions); return employees.map((entity: Employee) => this.serialize(entity)) as Employee[]; } case MultiORMEnum.TypeORM: { - return await this.typeOrmRepository.find({ where: whereClause }); + return await this.typeOrmRepository.find(options); } default: throw new Error(`Method not implemented for ORM type: ${this.ormType}`); diff --git a/packages/core/src/employee/employee.subscriber.ts b/packages/core/src/employee/employee.subscriber.ts index 576ceb7ba77..8ed44b3868b 100644 --- a/packages/core/src/employee/employee.subscriber.ts +++ b/packages/core/src/employee/employee.subscriber.ts @@ -2,7 +2,7 @@ import { EventSubscriber } from 'typeorm'; import { retrieveNameFromEmail, sluggable } from '@gauzy/common'; import { Employee } from './employee.entity'; import { getUserDummyImage } from '../core/utils'; -import { Organization } from '../core/entities/internal'; +import { Organization, UserOrganization } from '../core/entities/internal'; import { BaseEntityEventSubscriber } from '../core/entities/subscribers/base-entity-event.subscriber'; import { MikroOrmEntityManager, @@ -52,7 +52,7 @@ export class EmployeeSubscriber extends BaseEntityEventSubscriber { * * @param entity */ - async beforeEntityCreate(entity: Employee): Promise { + async beforeEntityCreate(entity: Employee, em?: MultiOrmEntityManager): Promise { try { // Set fullName from the associated user's name, if available if (Object.prototype.hasOwnProperty.call(entity, 'user')) { @@ -63,7 +63,7 @@ export class EmployeeSubscriber extends BaseEntityEventSubscriber { entity.user.imageUrl = entity.user.imageUrl ?? getUserDummyImage(entity.user); // Updates the employee's status based on the start and end work dates. - this.updateEmployeeStatus(entity); + this.updateEmployeeStatus(entity, em); } catch (error) { console.error( 'EmployeeSubscriber: An error occurred during the beforeEntityCreate process:', @@ -77,10 +77,10 @@ export class EmployeeSubscriber extends BaseEntityEventSubscriber { * * @param entity - The employee entity to be updated. */ - async beforeEntityUpdate(entity: Employee): Promise { + async beforeEntityUpdate(entity: Employee, em?: MultiOrmEntityManager): Promise { try { // Updates the employee's status based on the start and end work dates. - this.updateEmployeeStatus(entity); + this.updateEmployeeStatus(entity, em); } catch (error) { console.error( 'EmployeeSubscriber: An error occurred during the beforeEntityUpdate process:', @@ -88,17 +88,16 @@ export class EmployeeSubscriber extends BaseEntityEventSubscriber { ); } } - /** - * Called after entity is inserted/created to the database. + * Called after an entity is inserted/created in the database. * - * @param entity - * @param em + * @param {Employee} entity - The employee entity that was created. + * @param {MultiOrmEntityManager} em - The entity manager, either TypeORM's or MikroORM's. */ async afterEntityCreate(entity: Employee, em?: MultiOrmEntityManager): Promise { try { if (entity) { - await this.calculateTotalEmployees(entity, em); + await this.calculateTotalEmployees(entity, em); // Calculate and update the total number of employees for the organization } } catch (error) { console.error('EmployeeSubscriber: An error occurred during the afterEntityCreate process:', error.message); @@ -106,15 +105,15 @@ export class EmployeeSubscriber extends BaseEntityEventSubscriber { } /** - * Called after entity is removed from the database. + * Called after an entity is removed from the database. * - * @param entity - * @param em + * @param {Employee} entity - The employee entity that was deleted. + * @param {MultiOrmEntityManager} em - The entity manager, either TypeORM's or MikroORM's. */ async afterEntityDelete(entity: Employee, em?: MultiOrmEntityManager): Promise { try { if (entity) { - await this.calculateTotalEmployees(entity, em); + await this.calculateTotalEmployees(entity, em); // Calculate and update the total number of employees for the organization } } catch (error) { console.error('EmployeeSubscriber: An error occurred during the afterEntityDelete process:', error); @@ -126,26 +125,24 @@ export class EmployeeSubscriber extends BaseEntityEventSubscriber { * The slug is generated using the first and last name, username, or email, in that order of preference. * * @param {Employee} entity - The Employee entity for which to create the slug. + * @returns {Promise} - Returns a promise indicating the completion of the slug creation process. */ - async createSlug(entity: Employee) { + async createSlug(entity: Employee): Promise { try { - if (!entity || !entity.user) { + if (!entity?.user) { console.error('Entity or User object is not defined.'); return; } const { firstName, lastName, username, email } = entity.user; - if (firstName || lastName) { - // Use first &/or last name to create slug - entity.profile_link = sluggable(`${firstName || ''} ${lastName || ''}`.trim()); - } else if (username) { - // Use username to create slug if first & last name not found - entity.profile_link = sluggable(username); - } else { - // Use email to create slug if nothing found - entity.profile_link = sluggable(retrieveNameFromEmail(email)); - } + // Determine the slug based on the available fields in order of preference + const slugSource = + firstName?.trim() || lastName?.trim() + ? `${(firstName || '').trim()} ${(lastName || '').trim()}`.trim() + : username || retrieveNameFromEmail(email); + + entity.profile_link = sluggable(slugSource); } catch (error) { console.error(`EmployeeSubscriber: Error creating slug for entity with id ${entity.id}: `, error); } @@ -155,25 +152,29 @@ export class EmployeeSubscriber extends BaseEntityEventSubscriber { * Calculates and updates the total number of employees for an organization. * Handles both TypeORM and MikroORM environments. * - * @param entity The employee entity with organizationId and tenantId. - * @param em The entity manager, either TypeORM's or MikroORM's. + * @param {Employee} entity - The employee entity containing organizationId and tenantId. + * @param {MultiOrmEntityManager} em - The entity manager, either TypeORM's or MikroORM's. + * @returns {Promise} - Returns a promise indicating the completion of the total employee calculation. */ async calculateTotalEmployees(entity: Employee, em: MultiOrmEntityManager): Promise { try { const { organizationId, tenantId } = entity; + if (!organizationId) return; // Early return if organizationId is missing + + // Determine the total number of employees based on the ORM type + const totalEmployees = + em instanceof TypeOrmEntityManager + ? await em.countBy(Employee, { organizationId, tenantId }) + : await em.count(Employee, { organizationId, tenantId }); - // Check if organizationId is present in the entity - if (Object.prototype.hasOwnProperty.call(entity, 'organizationId')) { - // Handle TypeORM specific logic - if (em instanceof TypeOrmEntityManager) { - const totalEmployees = await em.countBy(Employee, { organizationId, tenantId }); - await em.update(Organization, { id: organizationId, tenantId }, { totalEmployees }); - } - // Handle MikroORM specific logic - else if (em instanceof MikroOrmEntityManager) { - const totalEmployees = await em.count(Employee, { organizationId, tenantId }); - await em.nativeUpdate(Organization, { id: organizationId, tenantId }, { totalEmployees }); - } + // Update the organization with the calculated total employees + const criteria = { id: organizationId, tenantId }; + const partialEntity = { totalEmployees }; + + if (em instanceof TypeOrmEntityManager) { + await em.update(Organization, criteria, partialEntity); + } else { + await em.nativeUpdate(Organization, criteria, partialEntity); } } catch (error) { console.error('EmployeeSubscriber: Error while updating total employee count of the organization:', error); @@ -181,26 +182,33 @@ export class EmployeeSubscriber extends BaseEntityEventSubscriber { } /** - * Updates the employee's status based on the start and end work dates. + * Updates the employee's status and user's status based on the start and end work dates. * - * @param entity - The employee entity to be updated. + * @param {Employee} entity - The employee entity to be updated. + * @param {MultiOrmEntityManager} em - The entity manager used to interact with the database. */ - private updateEmployeeStatus(entity: Employee): void { - if (entity.startedWorkOn) { - this.setEmployeeStatus(entity, true, false); - entity.endWork = null; // Ensure end work date is cleared - } - if (entity.endWork) { - this.setEmployeeStatus(entity, false, true); + private updateEmployeeStatus(entity: Employee, em: MultiOrmEntityManager): void { + // Check if the employee has started or ended work + const hasStartedWork = !!entity.startedWorkOn; + const hasEndedWork = !!entity.endWork; + + // Update the employee's status based on the work dates + if (hasStartedWork || hasEndedWork) { + this.setEmployeeStatus(entity, hasStartedWork, hasEndedWork); + this.setUserOrganizationStatus(em, entity, hasStartedWork, hasEndedWork); + + if (hasStartedWork) { + entity.endWork = null; // Clear the end work date if the employee has started work + } } } /** - * Sets the employee's status flags. + * Sets the employee's status flags and user tracking permissions. * - * @param entity - The employee entity. - * @param isActive - The active status of the employee. - * @param isArchived - The archived status of the employee. + * @param {Employee} entity - The employee entity. + * @param {boolean} isActive - True if the employee is active; false otherwise. + * @param {boolean} isArchived - True if the employee is archived; false otherwise. */ private setEmployeeStatus(entity: Employee, isActive: boolean, isArchived: boolean): void { entity.isTrackingEnabled = isActive; @@ -210,11 +218,64 @@ export class EmployeeSubscriber extends BaseEntityEventSubscriber { } /** - * Simulate an asynchronous operation to set the full name. + * Sets the full name for the employee entity based on the associated user's name. * - * @param entity - The Employee entity. + * @param {Employee} entity - The Employee entity whose full name needs to be set. + * @returns {Promise} - Returns a promise indicating the completion of the operation. */ private async setFullName(entity: Employee): Promise { - entity.fullName = entity.user.name; + if (entity?.user?.name) { + entity.fullName = entity.user.name; + } + } + + /** + * Updates the status (active and archived) of a user organization entity based on the associated employee's details. + * Handles both TypeORM and MikroORM environments. + * + * @param {MultiOrmEntityManager} em - The entity manager, either TypeORM's or MikroORM's, used to interact with the database. + * @param {Employee} entity - The employee entity containing the user ID, organization ID, and tenant ID information. + * @param {boolean} isActive - The desired active status to set for the user organization. + * @param {boolean} isArchived - The desired archived status to set for the user organization. + * @returns {Promise} - Returns a promise indicating the completion of the user organization status update. + * + * @throws {Error} - Logs any error that occurs during the user organization status update process. + */ + async setUserOrganizationStatus( + em: MultiOrmEntityManager, + entity: Employee, + isActive: boolean, + isArchived: boolean + ): Promise { + try { + if (!entity.id) return; // Early return if entity.id is missing + + // Get the employee ID and tenant ID from the entity + const { id, tenantId, organizationId } = entity; + + // Fetch the employee entity based on the ORM being used + const employee = + em instanceof TypeOrmEntityManager + ? await em.findOne(Employee, { where: { id, organizationId, tenantId } }) + : await em.findOne(Employee, { id, organizationId, tenantId }); + + if (!employee) { + console.warn('Employee or associated user not found.'); + return; + } + + // Get the user ID from the employee + const userId = employee.userId; + + // Update the UserOrganization status based on the ORM being used + if (em instanceof TypeOrmEntityManager) { + await em.update(UserOrganization, { userId, organizationId }, { isActive, isArchived }); + } else if (em instanceof MikroOrmEntityManager) { + await em.nativeUpdate(UserOrganization, { userId, organizationId }, { isActive, isArchived }); + } + } catch (error) { + // Log the error if an exception occurs during the update process + console.error('EmployeeSubscriber: Error while updating user organization as active/inactive:', error); + } } } diff --git a/packages/core/src/user-organization/user-organization.entity.ts b/packages/core/src/user-organization/user-organization.entity.ts index 8419926de6e..84117ff3edb 100644 --- a/packages/core/src/user-organization/user-organization.entity.ts +++ b/packages/core/src/user-organization/user-organization.entity.ts @@ -1,6 +1,6 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { JoinColumn, RelationId } from 'typeorm'; -import { IsUUID } from 'class-validator'; +import { IsBoolean, IsOptional, IsUUID } from 'class-validator'; import { ID, IUser, IUserOrganization } from '@gauzy/contracts'; import { TenantOrganizationBaseEntity, User } from '../core/entities/internal'; import { ColumnIndex, MultiORMColumn, MultiORMEntity, MultiORMManyToOne } from './../core/decorators/entity'; @@ -8,7 +8,9 @@ import { MikroOrmUserOrganizationRepository } from './repository/mikro-orm-user- @MultiORMEntity('user_organization', { mikroOrmRepository: () => MikroOrmUserOrganizationRepository }) export class UserOrganization extends TenantOrganizationBaseEntity implements IUserOrganization { - @ApiProperty({ type: () => Boolean, default: true }) + @ApiPropertyOptional({ type: () => Boolean, default: true }) + @IsOptional() + @IsBoolean() @ColumnIndex() @MultiORMColumn({ default: true }) isDefault: boolean; diff --git a/packages/core/src/user-organization/user-organization.services.ts b/packages/core/src/user-organization/user-organization.services.ts index be33733d1b3..a0038678e40 100644 --- a/packages/core/src/user-organization/user-organization.services.ts +++ b/packages/core/src/user-organization/user-organization.services.ts @@ -1,8 +1,9 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { IOrganization, IPagination, ITenant, IUser, IUserOrganization, RolesEnum } from '@gauzy/contracts'; -import { PaginationParams, TenantAwareCrudService } from './../core/crud'; -import { Employee } from './../core/entities/internal'; +import { ID, IOrganization, IPagination, IUser, IUserOrganization, RolesEnum } from '@gauzy/contracts'; +import { RequestContext } from '../core/context'; +import { PaginationParams, TenantAwareCrudService } from '../core/crud'; +import { Employee } from '../core/entities/internal'; import { EmployeeService } from '../employee/employee.service'; import { TypeOrmOrganizationRepository } from '../organization/repository'; import { UserOrganization } from './user-organization.entity'; @@ -36,13 +37,16 @@ export class UserOrganizationService extends TenantAwareCrudService organization.user) // Filter out user organizations without a user object .map((organization: IUserOrganization) => organization.user.id); // Fetch all employee details in bulk for the extracted user IDs - const employees = await this.employeeService.findEmployeesByUserIds(userIds); + const employees = await this.employeeService.findEmployeesByUserIds(userIds, tenantId); // Map employee details to a dictionary for easier lookup const employeeMap = new Map(); @@ -74,10 +78,7 @@ export class UserOrganizationService extends TenantAwareCrudService { + async addUserToOrganization(user: IUser, organizationId: ID): Promise { /** If role is SUPER_ADMIN, add user to all organizations in the tenant */ if (user.role.name === RolesEnum.SUPER_ADMIN) { return await this._addUserToAllOrganizations(user.id, user.tenantId); @@ -97,10 +98,7 @@ export class UserOrganizationService extends TenantAwareCrudService { + private async _addUserToAllOrganizations(userId: ID, tenantId: ID): Promise { /** Add user to all organizations in the tenant */ const organizations = await this.typeOrmOrganizationRepository.find({ where: { tenantId } From 545f9e05fd2c5e8ce5a83307080349f51cebbf64 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Sat, 28 Sep 2024 17:25:58 +0530 Subject: [PATCH 06/14] fix: role permissions API filter for specific role --- .../roles-permissions.component.ts | 148 +++++++++--------- .../contracts/src/role-permission.model.ts | 6 + .../role-permission.controller.ts | 30 ++-- .../role-permission/role-permission.seed.ts | 54 +++++-- .../services/role/role-permissions.service.ts | 14 +- 5 files changed, 133 insertions(+), 119 deletions(-) diff --git a/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.ts b/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.ts index e795c42d08e..f42643330d8 100644 --- a/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.ts +++ b/apps/gauzy/src/app/pages/settings/roles-permissions/roles-permissions.component.ts @@ -1,14 +1,14 @@ import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { FormControl } from '@angular/forms'; import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; -import { PermissionGroups, IRolePermission, RolesEnum, IUser, IRole, PermissionsEnum } from '@gauzy/contracts'; import { TranslateService } from '@ngx-translate/core'; -import { debounceTime, filter, tap, map } from 'rxjs/operators'; import { Observable, Subject, of as observableOf, startWith, catchError } from 'rxjs'; +import { debounceTime, filter, tap, map } from 'rxjs/operators'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { TranslationBaseComponent } from '@gauzy/ui-core/i18n'; import { environment } from '@gauzy/ui-config'; +import { PermissionGroups, IRolePermission, RolesEnum, IUser, IRole, PermissionsEnum } from '@gauzy/contracts'; import { RolePermissionsService, RoleService, Store, ToastrService } from '@gauzy/ui-core/core'; +import { TranslationBaseComponent } from '@gauzy/ui-core/i18n'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -22,17 +22,13 @@ export class RolesPermissionsComponent extends TranslationBaseComponent implemen isWantToCreate: boolean = false; loading: boolean; enabledPermissions: any = {}; - user: IUser; role: IRole; roles: IRole[] = []; permissions: IRolePermission[] = []; - roles$: Observable = observableOf([]); permissions$: Subject = new Subject(); - roleSubject$: Subject = new Subject(); - formControl: FormControl = new FormControl(); @ViewChild('input') input: ElementRef; @@ -127,36 +123,42 @@ export class RolesPermissionsComponent extends TranslationBaseComponent implemen } } - async loadPermissions() { + /** + * Loads and sets the enabled permissions for the current role. + */ + async loadPermissions(): Promise { this.enabledPermissions = {}; - if (!this.role) { - return; - } + if (!this.role) return; - const { tenantId } = this.user; - const { id: roleId } = this.role; + try { + const { id: roleId, tenantId } = this.role; - this.permissions = ( - await this.rolePermissionsService - .getRolePermissions({ - roleId, - tenantId - }) - .finally(() => (this.loading = false)) - ).items; + // Fetch role permissions and update the enabledPermissions map + const { items: permissions } = await this.rolePermissionsService.getRolePermissions({ + roleId, + tenantId + }); - this.permissions.forEach((p) => { - if (!this.enabledPermissions.hasOwnProperty(p.permission)) { - this.enabledPermissions[p.permission] = p.enabled; - } - }); + this.permissions = permissions; + this.permissions.forEach(({ permission, enabled }) => { + this.enabledPermissions[permission] = enabled; + }); + } finally { + this.loading = false; + } } - async permissionChanged(permission: string, enabled: boolean, allowChange: boolean) { - /** - * If anyone trying to update another users permissions without enough permission - */ + /** + * Handles the change in permission status and updates it accordingly. + * + * @param {string} permission - The name of the permission to be changed. + * @param {boolean} enabled - Indicates whether the permission should be enabled or disabled. + * @param {boolean} allowChange - Flag indicating whether the current user has the right to change the permission. + * @returns {Promise} + */ + async permissionChanged(permission: string, enabled: boolean, allowChange: boolean): Promise { + // Check if the user has permission to make changes if (!allowChange) { this.toastrService.danger( this.getTranslation('TOASTR.MESSAGE.PERMISSION_UPDATE_ERROR'), @@ -164,25 +166,22 @@ export class RolesPermissionsComponent extends TranslationBaseComponent implemen ); return; } + try { const { id: roleId } = this.role; const { tenantId } = this.user; const permissionToEdit = this.permissions.find((p) => p.permission === permission); + const payload = { enabled, roleId, tenantId, permission }; - const payload = { - enabled, - roleId, - tenantId, - permission - }; - permissionToEdit && permissionToEdit.id - ? await this.rolePermissionsService.update(permissionToEdit.id, { - ...payload - }) - : await this.rolePermissionsService.create({ - ...payload - }); + // Update or create the permission based on its existence + if (permissionToEdit?.id) { + await this.rolePermissionsService.update(permissionToEdit.id, payload); + } else { + await this.rolePermissionsService.create(payload); + } + + // Display success message this.toastrService.success( this.getTranslation('TOASTR.MESSAGE.PERMISSION_UPDATED', { permissionName: this.getTranslation('ORGANIZATIONS_PAGE.PERMISSIONS.' + permission), @@ -191,43 +190,49 @@ export class RolesPermissionsComponent extends TranslationBaseComponent implemen this.getTranslation('TOASTR.TITLE.SUCCESS') ); } catch (error) { + // Display error message this.toastrService.danger( this.getTranslation('TOASTR.MESSAGE.PERMISSION_UPDATE_ERROR'), this.getTranslation('TOASTR.TITLE.ERROR') ); } finally { + // Notify about permission update this.permissions$.next(true); } } /** - * CHANGE current selected role + * Handles the change of the currently selected role. */ - onSelectedRole() { + onSelectedRole(): void { this.role = this.getRoleByName(this.formControl.value); this.isWantToCreate = !this.role; this.permissions$.next(true); } /** - * GET role by name + * Retrieves a role by its name. * - * @param name - * @returns + * @param {string} name - The name of the role to retrieve. + * @returns {IRole | undefined} - The found role, or undefined if not found. */ - getRoleByName(name: IRole['name']) { - return this.roles.find((role: IRole) => name === role.name); + getRoleByName(name: IRole['name']): IRole | undefined { + return this.roles.find((role) => role.name === name); } - /*** - * GET Administration permissions & removed some permission in DEMO + /** + * Retrieves administration permissions, removing certain permissions in DEMO mode. + * + * @returns {PermissionsEnum[]} - The filtered list of administration permissions. */ getAdministrationPermissions(): PermissionsEnum[] { - // removed permissions for all users in DEMO mode - const deniedPermissions = [PermissionsEnum.ACCESS_DELETE_ACCOUNT, PermissionsEnum.ACCESS_DELETE_ALL_DATA]; + const deniedPermissions = new Set([ + PermissionsEnum.ACCESS_DELETE_ACCOUNT, + PermissionsEnum.ACCESS_DELETE_ALL_DATA + ]); - return this.permissionGroups.ADMINISTRATION.filter((permission) => - environment.DEMO ? !deniedPermissions.includes(permission) : true + return this.permissionGroups.ADMINISTRATION.filter( + (permission) => !environment.DEMO || !deniedPermissions.has(permission) ); } @@ -313,30 +318,21 @@ export class RolesPermissionsComponent extends TranslationBaseComponent implemen } /** - * Disabled General Group Permissions + * Checks whether the General Group Permissions should be disabled. * - * @returns + * @returns {boolean} - Returns true if the general permissions are disabled; otherwise, false. */ isDisabledGeneralPermissions(): boolean { - if (!this.role) { - return true; - } + if (!this.role) return true; - /** - * Disabled all permissions for "SUPER_ADMIN" - */ - const excludes = [RolesEnum.SUPER_ADMIN, RolesEnum.ADMIN]; - if (excludes.includes(this.user.role.name as RolesEnum)) { - if (this.role.name === RolesEnum.SUPER_ADMIN) { - return true; - } - } - if (this.user.role.name === RolesEnum.ADMIN) { - if (this.role.name === RolesEnum.ADMIN) { - return true; - } - } - return false; + // Disable permissions for "SUPER_ADMIN" role and when the current user's role is "ADMIN" + const userRole = this.user.role.name as RolesEnum; + const roleName = this.role.name; + + return ( + (userRole === RolesEnum.SUPER_ADMIN && roleName === RolesEnum.SUPER_ADMIN) || + (userRole === RolesEnum.ADMIN && roleName === RolesEnum.ADMIN) + ); } /** diff --git a/packages/contracts/src/role-permission.model.ts b/packages/contracts/src/role-permission.model.ts index 276f2cb6ed2..42ace6cb4db 100644 --- a/packages/contracts/src/role-permission.model.ts +++ b/packages/contracts/src/role-permission.model.ts @@ -26,6 +26,12 @@ export interface IRolePermissionCreateInput extends IBasePerTenantEntityModel { enabled: boolean; } +export interface IRolePermissionFindInput extends IBasePerTenantEntityModel { + roleId?: ID; + permission?: string; + enabled?: boolean; +} + export interface IRolePermissionUpdateInput extends IRolePermissionCreateInput { enabled: boolean; } diff --git a/packages/core/src/role-permission/role-permission.controller.ts b/packages/core/src/role-permission/role-permission.controller.ts index 5b9253b35b7..03aa3d965eb 100644 --- a/packages/core/src/role-permission/role-permission.controller.ts +++ b/packages/core/src/role-permission/role-permission.controller.ts @@ -17,7 +17,7 @@ import { ID, IPagination, IRolePermission, IRolePermissions, PermissionsEnum } f import { CrudController, PaginationParams } from './../core/crud'; import { Permissions } from './../shared/decorators'; import { PermissionGuard, TenantPermissionGuard } from './../shared/guards'; -import { ParseJsonPipe, UUIDValidationPipe, UseValidationPipe } from './../shared/pipes'; +import { UUIDValidationPipe, UseValidationPipe } from './../shared/pipes'; import { CreateRolePermissionDTO, UpdateRolePermissionDTO } from './dto'; import { RolePermission } from './role-permission.entity'; import { RolePermissionService } from './role-permission.service'; @@ -74,23 +74,12 @@ export class RolePermissionController extends CrudController { } /** - * GET role-permissions for specific tenant + * GET role permissions for a specific tenant with pagination. * - * @param options - * @returns - */ - @Get('/pagination') - async pagination(@Query() options: PaginationParams): Promise> { - return await this._rolePermissionService.findAllRolePermissions(options); - } - - /** - * GET all role permissions for specific tenant - * - * @param data - * @returns + * @param {PaginationParams} query - The query parameters for pagination and filtering. + * @returns {Promise>} - Returns a promise that resolves to a paginated list of role permissions. */ - @ApiOperation({ summary: 'Find role permissions.' }) + @ApiOperation({ summary: 'Retrieve role permissions for a specific tenant.' }) @ApiResponse({ status: HttpStatus.OK, description: 'Found role permissions.', @@ -101,10 +90,11 @@ export class RolePermissionController extends CrudController { description: 'Record not found' }) @HttpCode(HttpStatus.OK) - @Get('/') - async findAll(@Query('data', ParseJsonPipe) data: any): Promise> { - const { findInput } = data; - return this._rolePermissionService.findAllRolePermissions({ where: findInput }); + @Get(['/pagination', '/']) + async findAllRolePermissions( + @Query() query: PaginationParams + ): Promise> { + return this._rolePermissionService.findAllRolePermissions(query); } /** diff --git a/packages/core/src/role-permission/role-permission.seed.ts b/packages/core/src/role-permission/role-permission.seed.ts index 80dd8ad59a8..0825ea9ff04 100644 --- a/packages/core/src/role-permission/role-permission.seed.ts +++ b/packages/core/src/role-permission/role-permission.seed.ts @@ -8,28 +8,50 @@ import { environment } from '@gauzy/config'; import { DEFAULT_ROLE_PERMISSIONS } from './default-role-permissions'; import { RolePermission } from './role-permission.entity'; -export const createRolePermissions = async (dataSource: DataSource, roles: IRole[], tenants: ITenant[]) => { - // removed permissions for all users in DEMO mode - const deniedPermissions = [PermissionsEnum.ACCESS_DELETE_ACCOUNT, PermissionsEnum.ACCESS_DELETE_ALL_DATA]; +/** + * Creates role permissions for each tenant and role. + * + * @param {DataSource} dataSource - The data source to interact with the database. + * @param {IRole[]} roles - The list of roles to create permissions for. + * @param {ITenant[]} tenants - The list of tenants for whom to create role permissions. + */ +export const createRolePermissions = async ( + dataSource: DataSource, + roles: IRole[], + tenants: ITenant[] +): Promise => { + // Permissions that should be denied in DEMO mode + const deniedPermissions = new Set([PermissionsEnum.ACCESS_DELETE_ACCOUNT, PermissionsEnum.ACCESS_DELETE_ALL_DATA]); for (const tenant of tenants) { - const rolePermissions: IRolePermission[] = []; - DEFAULT_ROLE_PERMISSIONS.forEach(({ role: roleEnum, defaultEnabledPermissions }) => { + const rolePermissions = []; + + // Loop through each default role permission configuration + for (const { role: roleEnum, defaultEnabledPermissions } of DEFAULT_ROLE_PERMISSIONS) { + // Find the corresponding role for the current tenant const role = roles.find((dbRole: IRole) => dbRole.name === roleEnum && dbRole.tenant.name === tenant.name); + if (role) { - const permissions = Object.values(PermissionsEnum).filter((permission: PermissionsEnum) => - environment.demo ? !deniedPermissions.includes(permission) : true + // Filter permissions, excluding denied permissions in DEMO mode + const permissions = environment.demo + ? Object.values(PermissionsEnum).filter((permission) => !deniedPermissions.has(permission)) + : Object.values(PermissionsEnum); + + // Create RolePermission objects and add them to the array + rolePermissions.push( + ...permissions.map((permission) => { + const rolePermission = new RolePermission(); + rolePermission.role = role; + rolePermission.permission = permission; + rolePermission.enabled = defaultEnabledPermissions.includes(permission); + rolePermission.tenant = tenant; + return rolePermission; + }) ); - for (const permission of permissions) { - const rolePermission = new RolePermission(); - rolePermission.role = role; - rolePermission.permission = permission; - rolePermission.enabled = defaultEnabledPermissions.includes(permission); - rolePermission.tenant = tenant; - rolePermissions.push(rolePermission); - } } - }); + } + + // Save all role permissions in one batch for the current tenant await dataSource.manager.save(rolePermissions); } }; diff --git a/packages/ui-core/core/src/lib/services/role/role-permissions.service.ts b/packages/ui-core/core/src/lib/services/role/role-permissions.service.ts index 28abf7dad7b..bcfe9960aa7 100644 --- a/packages/ui-core/core/src/lib/services/role/role-permissions.service.ts +++ b/packages/ui-core/core/src/lib/services/role/role-permissions.service.ts @@ -6,25 +6,25 @@ import { IPagination, IRolePermission, IRolePermissionCreateInput, + IRolePermissionFindInput, IRolePermissionUpdateInput } from '@gauzy/contracts'; -import { API_PREFIX } from '@gauzy/ui-core/common'; +import { API_PREFIX, toParams } from '@gauzy/ui-core/common'; @Injectable() export class RolePermissionsService { constructor(private readonly http: HttpClient) {} /** - * Retrieves a list of role permissions based on the provided filter criteria. + * Retrieves role permissions based on the specified filter criteria. * - * @param findInput - Optional filter criteria for retrieving role permissions. - * @returns A promise that resolves to an object containing the list of role permissions (`items`) and the total count (`total`). + * @param {IRolePermissionFindInput} [where] - An optional filter object used to specify the criteria for retrieving role permissions. + * @returns {Promise>} - Returns a promise that resolves to a pagination object containing the role permissions. */ - getRolePermissions(findInput?: any): Promise> { - const data = JSON.stringify(findInput); + getRolePermissions(where?: IRolePermissionFindInput): Promise> { return firstValueFrom( this.http.get>(`${API_PREFIX}/role-permissions`, { - params: { data } + params: toParams({ where }) }) ); } From 417133098923aed07b4f867c5187b6ac51cc1247 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Sat, 28 Sep 2024 17:31:01 +0530 Subject: [PATCH 07/14] fix(deepscan): missing variable types --- packages/core/src/role-permission/role-permission.seed.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/role-permission/role-permission.seed.ts b/packages/core/src/role-permission/role-permission.seed.ts index 0825ea9ff04..7c3c386cb46 100644 --- a/packages/core/src/role-permission/role-permission.seed.ts +++ b/packages/core/src/role-permission/role-permission.seed.ts @@ -24,7 +24,7 @@ export const createRolePermissions = async ( const deniedPermissions = new Set([PermissionsEnum.ACCESS_DELETE_ACCOUNT, PermissionsEnum.ACCESS_DELETE_ALL_DATA]); for (const tenant of tenants) { - const rolePermissions = []; + const rolePermissions: IRolePermission[] = []; // Loop through each default role permission configuration for (const { role: roleEnum, defaultEnabledPermissions } of DEFAULT_ROLE_PERMISSIONS) { @@ -39,7 +39,7 @@ export const createRolePermissions = async ( // Create RolePermission objects and add them to the array rolePermissions.push( - ...permissions.map((permission) => { + ...permissions.map((permission: string) => { const rolePermission = new RolePermission(); rolePermission.role = role; rolePermission.permission = permission; From cae9228941777498aec65653cd6c373df8ce61c3 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Sat, 28 Sep 2024 17:37:23 +0530 Subject: [PATCH 08/14] fix: packages build --- packages/core/src/role-permission/role-permission.seed.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/role-permission/role-permission.seed.ts b/packages/core/src/role-permission/role-permission.seed.ts index 7c3c386cb46..cad8c652d48 100644 --- a/packages/core/src/role-permission/role-permission.seed.ts +++ b/packages/core/src/role-permission/role-permission.seed.ts @@ -39,7 +39,7 @@ export const createRolePermissions = async ( // Create RolePermission objects and add them to the array rolePermissions.push( - ...permissions.map((permission: string) => { + ...permissions.map((permission) => { const rolePermission = new RolePermission(); rolePermission.role = role; rolePermission.permission = permission; From f099c8e2a250fb8f6e108d8975a48d4c738f2287 Mon Sep 17 00:00:00 2001 From: samuelmbabhazi Date: Sun, 29 Sep 2024 10:30:26 +0200 Subject: [PATCH 09/14] [FEATURE] Add Online/Offline Status Indicator to User Avatar --- .../components/avatar/avatar.component.html | 5 + .../components/avatar/avatar.component.scss | 359 +++++++++--------- .../lib/components/avatar/avatar.component.ts | 16 +- 3 files changed, 207 insertions(+), 173 deletions(-) diff --git a/packages/ui-core/shared/src/lib/components/avatar/avatar.component.html b/packages/ui-core/shared/src/lib/components/avatar/avatar.component.html index b446dab08a0..808e0d6fba7 100644 --- a/packages/ui-core/shared/src/lib/components/avatar/avatar.component.html +++ b/packages/ui-core/shared/src/lib/components/avatar/avatar.component.html @@ -3,6 +3,11 @@
+
diff --git a/packages/ui-core/shared/src/lib/components/avatar/avatar.component.scss b/packages/ui-core/shared/src/lib/components/avatar/avatar.component.scss index 8cd3522114a..7dfbb13eab4 100644 --- a/packages/ui-core/shared/src/lib/components/avatar/avatar.component.scss +++ b/packages/ui-core/shared/src/lib/components/avatar/avatar.component.scss @@ -1,187 +1,204 @@ @import 'themes'; @mixin common { - .inner-wrapper { - border-radius: var(--button-rectangle-border-radius); - align-items: center; - overflow: hidden; - display: flex; - gap: 8px; - } - - .avatar-wrapper { - width: 100%; - border-radius: var(--border-radius); - } - - .names-wrapper { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - .link-text { - cursor: pointer; - text-decoration: none; - font-style: normal; - line-height: 15px; - letter-spacing: 0; - white-space: nowrap; - width: 100%; - text-overflow: ellipsis; - } - - .link-text:hover { - text-decoration: underline; - } - - .image-container { - width: 20px; - cursor: pointer; - border-radius: var(--border-radius); - display: flex; - - img { - width: 20px; - height: 20px; - object-fit: cover; - border-radius: var(--border-radius) !important; - } - } + .inner-wrapper { + border-radius: var(--button-rectangle-border-radius); + align-items: center; + overflow: hidden; + display: flex; + gap: 8px; + } + + .avatar-wrapper { + width: 100%; + border-radius: var(--border-radius); + } + + .names-wrapper { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .link-text { + cursor: pointer; + text-decoration: none; + font-style: normal; + line-height: 15px; + letter-spacing: 0; + white-space: nowrap; + width: 100%; + text-overflow: ellipsis; + } + + .link-text:hover { + text-decoration: underline; + } + + .image-container { + width: 20px; + cursor: pointer; + border-radius: var(--border-radius); + display: flex; + position: relative; + + img { + width: 20px; + height: 20px; + object-fit: cover; + border-radius: var(--border-radius) !important; + } + .status-indicator { + position: absolute; + width: 10px; + height: 10px; + border-radius: 8px; + border: 2px solid #ebebeb; + right: 0; + top: 0; + + &.online { + background-color: #4caf50; /* Green for online */ + } + &.offline { + background-color: #f44336; /* Red for offline */ + } + } + } } :host { - display: block; - - .link-text { - font-size: 14px; - font-weight: 600; - line-height: 16px; - cursor: pointer; - text-decoration: none; - letter-spacing: 0em; - color: nb-theme(gauzy-text-contact); - } - - .caption { - font-size: 11px; - font-weight: 400; - line-height: 11px; - letter-spacing: 0em; - color: var(--gauzy-text-color-2); - } - - .link-text:hover { - text-decoration: underline; - } - - .image-container { - width: 48px; - cursor: pointer; - - img { - width: 48px; - height: 48px; - object-fit: cover; - } - - &.lg { - width: 64px; - - img { - width: 64px; - height: 64px; - } - } - - &.sm { - width: 32px; - - img { - width: 32px; - height: 32px; - } - } - } + display: block; + + .link-text { + font-size: 14px; + font-weight: 600; + line-height: 16px; + cursor: pointer; + text-decoration: none; + letter-spacing: 0em; + color: nb-theme(gauzy-text-contact); + } + + .caption { + font-size: 11px; + font-weight: 400; + line-height: 11px; + letter-spacing: 0em; + color: var(--gauzy-text-color-2); + } + + .link-text:hover { + text-decoration: underline; + } + + .image-container { + width: 48px; + cursor: pointer; + + img { + width: 48px; + height: 48px; + object-fit: cover; + } + + &.lg { + width: 64px; + + img { + width: 64px; + height: 64px; + } + } + + &.sm { + width: 32px; + + img { + width: 32px; + height: 32px; + } + } + } } :host-context(.report-table) { - width: 100%; - - .inner-wrapper { - width: fit-content; - @include nb-ltr(padding, 3px 9px 3px 3px); - @include nb-rtl(padding, 3px 3px 3px 9px); - background-color: var(--color-primary-transparent-100); - } - - .link-text { - font-size: 12px; - font-weight: 400; - text-overflow: ellipsis; - color: var(--text-primary-color) !important; - } - - @include common; + width: 100%; + + .inner-wrapper { + width: fit-content; + @include nb-ltr(padding, 3px 9px 3px 3px); + @include nb-rtl(padding, 3px 3px 3px 9px); + background-color: var(--color-primary-transparent-100); + } + + .link-text { + font-size: 12px; + font-weight: 400; + text-overflow: ellipsis; + color: var(--text-primary-color) !important; + } + + @include common; } :host-context(.avatar-dashboard) { - width: 100%; - - .inner-wrapper { - width: 100%; - } - - .names-wrapper { - width: 100%; - } - - .link-text { - font-size: 14px; - font-weight: 600; - color: var(--gauzy-text-color-1) !important; - } - - &.activity { - .image-container { - width: 28px; - border-radius: var(--button-rectangle-border-radius) !important; - - img { - border-radius: var(--button-rectangle-border-radius) !important; - width: 28px; - height: 28px; - } - } - } - - @include common; + width: 100%; + + .inner-wrapper { + width: 100%; + } + + .names-wrapper { + width: 100%; + } + + .link-text { + font-size: 14px; + font-weight: 600; + color: var(--gauzy-text-color-1) !important; + } + + &.activity { + .image-container { + width: 28px; + border-radius: var(--button-rectangle-border-radius) !important; + + img { + border-radius: var(--button-rectangle-border-radius) !important; + width: 28px; + height: 28px; + } + } + } + + @include common; } :host-context(.workspace) { - $radius: var(--border-radius); - width: 100%; - - .inner-wrapper { - width: fit-content; - padding: calc($radius / 4); - background-color: var(--color-primary-transparent-100); - border-radius: calc($radius / 2) !important; - } - - @include common; - - .image-container { - &, - img { - border-radius: calc($radius / 4) !important; - } - } - - .names-wrapper { - a.link-text { - text-decoration: none; - cursor: none; - } - } + $radius: var(--border-radius); + width: 100%; + + .inner-wrapper { + width: fit-content; + padding: calc($radius / 4); + background-color: var(--color-primary-transparent-100); + border-radius: calc($radius / 2) !important; + } + + @include common; + + .image-container { + &, + img { + border-radius: calc($radius / 4) !important; + } + } + + .names-wrapper { + a.link-text { + text-decoration: none; + cursor: none; + } + } } diff --git a/packages/ui-core/shared/src/lib/components/avatar/avatar.component.ts b/packages/ui-core/shared/src/lib/components/avatar/avatar.component.ts index d5a23e16e80..5f64dbe0dea 100644 --- a/packages/ui-core/shared/src/lib/components/avatar/avatar.component.ts +++ b/packages/ui-core/shared/src/lib/components/avatar/avatar.component.ts @@ -1,6 +1,9 @@ import { Component, Input, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { ID } from '@gauzy/contracts'; +import { EmployeesService } from '@gauzy/ui-core/core'; +import { Observable } from 'rxjs'; +import { tap, map } from 'rxjs/operators'; @Component({ selector: 'ngx-avatar', @@ -15,6 +18,8 @@ export class AvatarComponent implements OnInit { @Input() id: string; @Input() isOption: boolean; + online$: Observable; + // Added for set component value when used for angular2-smart-table renderer. @Input() set value(object) { for (const key in object) { @@ -45,9 +50,16 @@ export class AvatarComponent implements OnInit { this._name = value; } - constructor(private readonly router: Router) {} + constructor(private readonly router: Router, private readonly _employeeService: EmployeesService) {} - ngOnInit() {} + ngOnInit() { + if (this.id) { + this.online$ = this._employeeService.getEmployeeById(this.id).pipe( + tap((employee) => console.log('Employee data:', employee)), // Log the employee object + map((employee) => employee?.isOnline && !employee?.isAway) // Continue with the online status check + ); + } + } /** * Navigates to the employee edit page based on the provided employee ID. From ee4bc8eb7f1d75476ef6efdbe38646c6cca289f4 Mon Sep 17 00:00:00 2001 From: samuelmbabhazi Date: Sun, 29 Sep 2024 10:31:28 +0200 Subject: [PATCH 10/14] [FEATURE] Add Online/Offline Status Indicator to User Avatar --- .../shared/src/lib/components/avatar/avatar.component.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/ui-core/shared/src/lib/components/avatar/avatar.component.ts b/packages/ui-core/shared/src/lib/components/avatar/avatar.component.ts index 5f64dbe0dea..e772a00297f 100644 --- a/packages/ui-core/shared/src/lib/components/avatar/avatar.component.ts +++ b/packages/ui-core/shared/src/lib/components/avatar/avatar.component.ts @@ -3,7 +3,7 @@ import { Router } from '@angular/router'; import { ID } from '@gauzy/contracts'; import { EmployeesService } from '@gauzy/ui-core/core'; import { Observable } from 'rxjs'; -import { tap, map } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; @Component({ selector: 'ngx-avatar', @@ -54,10 +54,9 @@ export class AvatarComponent implements OnInit { ngOnInit() { if (this.id) { - this.online$ = this._employeeService.getEmployeeById(this.id).pipe( - tap((employee) => console.log('Employee data:', employee)), // Log the employee object - map((employee) => employee?.isOnline && !employee?.isAway) // Continue with the online status check - ); + this.online$ = this._employeeService + .getEmployeeById(this.id) + .pipe(map((employee) => employee?.isOnline && !employee?.isAway)); } } From 06b28d1336772a5955ffc579413b94d3f0c0bd4c Mon Sep 17 00:00:00 2001 From: samuelmbabhazi Date: Sun, 29 Sep 2024 11:18:36 +0200 Subject: [PATCH 11/14] Limit TODO && Add Tooltip --- .../employees/timesheet/daily/daily/daily.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/gauzy/src/app/pages/employees/timesheet/daily/daily/daily.component.html b/apps/gauzy/src/app/pages/employees/timesheet/daily/daily/daily.component.html index 656322b19cb..79ed6cc6e46 100644 --- a/apps/gauzy/src/app/pages/employees/timesheet/daily/daily/daily.component.html +++ b/apps/gauzy/src/app/pages/employees/timesheet/daily/daily/daily.component.html @@ -92,8 +92,8 @@
- - {{ 'TIMESHEET.TODO' | translate }} : + + {{ 'TIMESHEET.TODO' | translate }}: {{ log?.task?.title | truncate : 50 }} From 5d0809a11c26f076b75ab295ffa2d49c963e5e50 Mon Sep 17 00:00:00 2001 From: "Rahul R." Date: Sun, 29 Sep 2024 17:23:00 +0530 Subject: [PATCH 12/14] fix: don't be use API to call employee --- .../time-tracking.component.html | 3 +- .../statistic/statistic.service.ts | 4 ++ .../components/avatar/avatar.component.html | 16 +++---- .../lib/components/avatar/avatar.component.ts | 48 +++++++++---------- 4 files changed, 37 insertions(+), 34 deletions(-) diff --git a/apps/gauzy/src/app/pages/dashboard/time-tracking/time-tracking.component.html b/apps/gauzy/src/app/pages/dashboard/time-tracking/time-tracking.component.html index 84f10143872..e51a1e6bca5 100644 --- a/apps/gauzy/src/app/pages/dashboard/time-tracking/time-tracking.component.html +++ b/apps/gauzy/src/app/pages/dashboard/time-tracking/time-tracking.component.html @@ -224,13 +224,14 @@

diff --git a/packages/core/src/time-tracking/statistic/statistic.service.ts b/packages/core/src/time-tracking/statistic/statistic.service.ts index 1790d025289..aaea394cbe1 100644 --- a/packages/core/src/time-tracking/statistic/statistic.service.ts +++ b/packages/core/src/time-tracking/statistic/statistic.service.ts @@ -1731,6 +1731,8 @@ export class StatisticService { query.innerJoin(`${query.alias}.timeSlots`, 'time_slot'); query.innerJoin(`employee.user`, 'user'); query.select(p(`"${query.alias}"."employeeId"`), 'id'); + query.addSelect(p(`"employee"."isOnline"`), 'isOnline'); + query.addSelect(p(`"employee"."isAway"`), 'isAway'); query.addSelect(p(`MAX("${query.alias}"."startedAt")`), 'startedAt'); query.addSelect(p(`"user"."imageUrl"`), 'user_image_url'); // Builds a SELECT statement for the "user_name" column based on the database type. @@ -1764,6 +1766,8 @@ export class StatisticService { ); query.groupBy(p(`"${query.alias}"."employeeId"`)); query.addGroupBy(p(`"user"."id"`)); + query.addGroupBy(p(`"employee"."isOnline"`)); + query.addGroupBy(p(`"employee"."isAway"`)); query.addOrderBy(p(`"startedAt"`), 'DESC'); query.limit(3); diff --git a/packages/ui-core/shared/src/lib/components/avatar/avatar.component.html b/packages/ui-core/shared/src/lib/components/avatar/avatar.component.html index 808e0d6fba7..baa8f3269e6 100644 --- a/packages/ui-core/shared/src/lib/components/avatar/avatar.component.html +++ b/packages/ui-core/shared/src/lib/components/avatar/avatar.component.html @@ -1,24 +1,22 @@
-
+
- - {{ name }} + + {{ name }} +
{{ name }}
-
{{ name }}
- - {{ appendCaption }} - + {{ appendCaption }} {{ caption }}
diff --git a/packages/ui-core/shared/src/lib/components/avatar/avatar.component.ts b/packages/ui-core/shared/src/lib/components/avatar/avatar.component.ts index e772a00297f..1e5de62a44b 100644 --- a/packages/ui-core/shared/src/lib/components/avatar/avatar.component.ts +++ b/packages/ui-core/shared/src/lib/components/avatar/avatar.component.ts @@ -1,9 +1,8 @@ import { Component, Input, OnInit } from '@angular/core'; import { Router } from '@angular/router'; -import { ID } from '@gauzy/contracts'; -import { EmployeesService } from '@gauzy/ui-core/core'; -import { Observable } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; +import { ID, IEmployee } from '@gauzy/contracts'; @Component({ selector: 'ngx-avatar', @@ -11,17 +10,28 @@ import { map } from 'rxjs/operators'; styleUrls: ['./avatar.component.scss'] }) export class AvatarComponent implements OnInit { + public online$: Observable; + @Input() size: 'lg' | 'sm' | 'md' = 'md'; @Input() src: string; @Input() appendCaption: string; @Input() caption: string; - @Input() id: string; + @Input() id: ID; @Input() isOption: boolean; - online$: Observable; + /** + * A class member and getter/setter for managing an employee object. + */ + private _employee = new BehaviorSubject(null); + @Input() set employee(value: IEmployee) { + this._employee.next(value); + } + get employee(): IEmployee { + return this._employee.getValue(); + } // Added for set component value when used for angular2-smart-table renderer. - @Input() set value(object) { + @Input() set value(object: any) { for (const key in object) { if (Object.prototype.hasOwnProperty.call(object, key)) { this[key] = object[key]; @@ -30,32 +40,22 @@ export class AvatarComponent implements OnInit { } /** - * A class member and getter/setter for managing a string name. - */ - _name: string; - - /** - * Getter method for the 'name' property, providing access to the private _name variable. - * @returns The current value of the 'name' property. - */ - get name(): string { - return this._name; - } - - /** - * Setter method for the 'name' property, annotated with @Input(). - * @param value - The new value to set for the 'name' property. + * A class member and getter/setter for managing an employee name. */ + private _name: string; @Input() set name(value: string) { this._name = value; } + get name(): string { + return this._name; + } - constructor(private readonly router: Router, private readonly _employeeService: EmployeesService) {} + constructor(private readonly router: Router) {} ngOnInit() { if (this.id) { - this.online$ = this._employeeService - .getEmployeeById(this.id) + this.online$ = this._employee + .asObservable() .pipe(map((employee) => employee?.isOnline && !employee?.isAway)); } } From 3968519ea5b854c95d5360f8e76e408c151b27e4 Mon Sep 17 00:00:00 2001 From: Kifungo A <45813955+adkif@users.noreply.github.com> Date: Sun, 29 Sep 2024 21:37:41 +0500 Subject: [PATCH 13/14] [Fix] Issue with Changing Task on Running Timer (#8281) * feat: add ITimeTrackerFormState to time-tracker.store * feat: pass data to ignition restart and store last selector state * feat: add `data` parameter to concatMap operator and called update selector states with it --- .../src/lib/time-tracker/+state/time-tracker.store.ts | 11 ++++++++++- .../src/lib/time-tracker/time-tracker.component.ts | 3 ++- .../timer-tracker-change-dialog.component.ts | 11 ++++++++--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/+state/time-tracker.store.ts b/packages/desktop-ui-lib/src/lib/time-tracker/+state/time-tracker.store.ts index 970199ded38..978fb149862 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/+state/time-tracker.store.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/+state/time-tracker.store.ts @@ -1,5 +1,6 @@ import { Injectable } from '@angular/core'; import { Store, StoreConfig } from '@datorama/akita'; +import { ITimeTrackerFormState } from '../../shared/features/time-tracker-form/time-tracker-form.service'; export enum TimerStartMode { MANUAL = 'manual', @@ -19,6 +20,7 @@ export enum IgnitionState { export interface ITimerIgnition { mode?: TimerStartMode; state?: IgnitionState; + data?: ITimeTrackerFormState; } export interface ITimeTrackerState { @@ -35,7 +37,14 @@ export function createInitialState(): ITimeTrackerState { isEditing: false, ignition: { mode: TimerStartMode.STOP, - state: IgnitionState.STOPPED + state: IgnitionState.STOPPED, + data: { + clientId: null, + teamId: null, + projectId: null, + taskId: null, + note: null + } } }; } diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts index be14d9958ec..ab5f9636591 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/time-tracker.component.ts @@ -681,8 +681,9 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit { this.timeTrackerQuery.ignition$ .pipe( filter(({ state }) => state === IgnitionState.RESTARTING), - concatMap(async () => { + concatMap(async ({ data }) => { this.isProcessingEnabled = true; + this.timeTrackerFormService.setState(data); const session: moment.Moment = this._session?.clone(); const sessionLog = await this.silentRestart(); return { sessionLog, session }; diff --git a/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.ts b/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.ts index 90281ecd468..cf821b61850 100644 --- a/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.ts +++ b/packages/desktop-ui-lib/src/lib/time-tracker/timer-tracker-change-dialog/timer-tracker-change-dialog.component.ts @@ -10,7 +10,10 @@ import { ClientSelectorService } from '../../shared/features/client-selector/+st import { ProjectSelectorService } from '../../shared/features/project-selector/+state/project-selector.service'; import { TaskSelectorService } from '../../shared/features/task-selector/+state/task-selector.service'; import { TeamSelectorService } from '../../shared/features/team-selector/+state/team-selector.service'; -import { TimeTrackerFormService } from '../../shared/features/time-tracker-form/time-tracker-form.service'; +import { + ITimeTrackerFormState, + TimeTrackerFormService +} from '../../shared/features/time-tracker-form/time-tracker-form.service'; @UntilDestroy({ checkProperties: true }) @Component({ @@ -20,6 +23,7 @@ import { TimeTrackerFormService } from '../../shared/features/time-tracker-form/ changeDetection: ChangeDetectionStrategy.OnPush }) export class TimerTrackerChangeDialogComponent implements OnInit { + private lastSelectorState: ITimeTrackerFormState; public form: FormGroup = new FormGroup({ clientId: new FormControl(null), projectId: new FormControl(null), @@ -50,7 +54,7 @@ export class TimerTrackerChangeDialogComponent implements OnInit { .pipe( filter(({ state }) => state === IgnitionState.RESTARTED), tap(() => this.timeTrackerStore.update({ ignition: { state: IgnitionState.STARTED } })), - tap(() => this.dismiss(this.form.value)), + tap(() => this.dismiss(this.lastSelectorState)), untilDestroyed(this) ) .subscribe(); @@ -90,7 +94,8 @@ export class TimerTrackerChangeDialogComponent implements OnInit { } public applyChanges() { - this.timeTrackerStore.update({ ignition: { state: IgnitionState.RESTARTING } }); + this.lastSelectorState = this.form.value; + this.timeTrackerStore.ignition({ state: IgnitionState.RESTARTING, data: this.form.value }); } public get isRestarting$(): Observable { From b6c9f5500194c6ba0d61c5a3665a06b4b3a9ccd1 Mon Sep 17 00:00:00 2001 From: Ruslan Konviser Date: Sun, 29 Sep 2024 18:43:55 +0200 Subject: [PATCH 14/14] Update server-api-stage.yml --- .github/workflows/server-api-stage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/server-api-stage.yml b/.github/workflows/server-api-stage.yml index 74060608320..5e3acab350b 100644 --- a/.github/workflows/server-api-stage.yml +++ b/.github/workflows/server-api-stage.yml @@ -313,7 +313,7 @@ jobs: PROCESSOR_REVISION: '' PSModuleAnalysisCachePath: '' PSModulePath: '' - Path: 'C:\hostedtoolcache\windows\node\20.11.1\x64;C:\Program Files\Git\bin;C:\npm\prefix;C:\hostedtoolcache\windows\Python\3.9.13\x64\Scripts;C:\hostedtoolcache\windows\Python\3.9.13\x64;C:\Program Files\OpenSSL\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\OpenSSH\;C:\Program Files\dotnet\;C:\Program Files\PowerShell\7\;C:\Program Files\CMake\bin;C:\Program Files\nodejs\;C:\Program Files\Git\cmd;C:\Program Files\Git\mingw64\bin;C:\Program Files\Git\usr\bin;C:\Program Files\GitHub CLI\;C:\Program Files\Amazon\AWSCLIV2\;C:\Users\runneradmin\.dotnet\tools' + Path: 'C:\hostedtoolcache\windows\node\20.11.1\x64;C:\Program Files\Git\bin;C:\npm\prefix;C:\hostedtoolcache\windows\Python\3.9.13\x64\Scripts;C:\hostedtoolcache\windows\Python\3.9.13\x64;C:\Program Files\OpenSSL\bin;C:\Windows\system32;C:\Windows;C:\Windows\System32\OpenSSH\;C:\Program Files\dotnet\;C:\Program Files\PowerShell\7\;C:\Program Files\CMake\bin;C:\Program Files\nodejs\;C:\Program Files\Git\cmd;C:\Program Files\Git\usr\bin;C:\Program Files\Amazon\AWSCLIV2\' DOTNET_MULTILEVEL_LOOKUP: '' DOTNET_NOLOGO: '' DOTNET_SKIP_FIRST_TIME_EXPERIENCE: ''