mirror of
https://github.com/ever-co/ever-gauzy.git
synced 2026-10-02 01:54:50 +08:00
Merge branch 'develop' of https://github.com/ever-co/gauzy into feature#1363-sales-deals-list
This commit is contained in:
@@ -40,3 +40,4 @@ Thumbs.db
|
||||
/apps/api/src/app/integrations/upwork/csv
|
||||
|
||||
.env
|
||||
/.deploy/nginx/log
|
||||
|
||||
@@ -101,6 +101,7 @@ import { EstimateEmailModule } from './estimate-email/estimate-email.module';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { TimeOffRequestModule } from './time-off-request/time-off-request.module';
|
||||
import { DealModule } from './deal/deal.module';
|
||||
import { HelpCenterAuthorModule } from './help-center-author/help-center-author.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -171,6 +172,10 @@ import { DealModule } from './deal/deal.module';
|
||||
path: '/help-center-article',
|
||||
module: HelpCenterArticleModule
|
||||
},
|
||||
{
|
||||
path: '/help-center-author',
|
||||
module: HelpCenterAuthorModule
|
||||
},
|
||||
{ path: '/equipment', module: EquipmentModule },
|
||||
{ path: '/employee-level', module: EmployeeLevelModule },
|
||||
|
||||
@@ -378,10 +383,10 @@ import { DealModule } from './deal/deal.module';
|
||||
path: '/pipelines',
|
||||
module: PipelineModule
|
||||
},
|
||||
{
|
||||
path: '/deals',
|
||||
module: DealModule,
|
||||
},
|
||||
{
|
||||
path: '/deals',
|
||||
module: DealModule
|
||||
},
|
||||
{
|
||||
path: '/payments',
|
||||
module: PaymentModule
|
||||
@@ -473,7 +478,7 @@ import { DealModule } from './deal/deal.module';
|
||||
AvailabilitySlotsModule,
|
||||
PipelineModule,
|
||||
StageModule,
|
||||
DealModule,
|
||||
DealModule,
|
||||
...(environment.sentry
|
||||
? [
|
||||
SentryModule.forRoot({
|
||||
@@ -488,6 +493,7 @@ import { DealModule } from './deal/deal.module';
|
||||
]
|
||||
: []),
|
||||
HelpCenterModule,
|
||||
HelpCenterAuthorModule,
|
||||
EquipmentModule,
|
||||
EquipmentSharingModule,
|
||||
TaskModule,
|
||||
|
||||
@@ -84,6 +84,7 @@ import { EventType } from '../event-types/event-type.entity';
|
||||
import { AvailabilitySlots } from '../availability-slots/availability-slots.entity';
|
||||
import { ProductTypeTranslation } from '../product-type/product-type-translation.entity';
|
||||
import { HelpCenter } from '../help-center/help-center.entity';
|
||||
import { HelpCenterAuthor } from './../help-center-author/help-center-author.entity';
|
||||
import { ProductCategoryTranslation } from '../product-category/product-category-translation.entity';
|
||||
import { Pipeline } from '../pipeline/pipeline.entity';
|
||||
import { Payment } from '../payment/payment.entity';
|
||||
@@ -174,6 +175,7 @@ const entities = [
|
||||
CandidateCriterionsRating,
|
||||
HelpCenter,
|
||||
HelpCenterArticle,
|
||||
HelpCenterAuthor,
|
||||
IntegrationSetting,
|
||||
Integration,
|
||||
IntegrationType,
|
||||
@@ -195,7 +197,7 @@ const entities = [
|
||||
Pipeline,
|
||||
Payment,
|
||||
Stage,
|
||||
Deal,
|
||||
Deal,
|
||||
Payment,
|
||||
EstimateEmail,
|
||||
Contact
|
||||
|
||||
@@ -149,7 +149,10 @@ import { Contact } from '../../contact/contact.entity';
|
||||
|
||||
import { createRandomTimesheet } from '../../timesheet/timesheet/timesheet.seed';
|
||||
import { createRandomTask } from '../../tasks/task.seed';
|
||||
import { createRandomOrganizationProjects } from '../../organization-projects/organization-projects.seed';
|
||||
import {
|
||||
createDefaultOrganizationProjects,
|
||||
createRandomOrganizationProjects
|
||||
} from '../../organization-projects/organization-projects.seed';
|
||||
|
||||
import { RequestApprovalTeam } from '../../request-approval-team/request-approval-team.entity';
|
||||
import { RequestApproval } from '../../request-approval/request-approval.entity';
|
||||
@@ -181,6 +184,7 @@ import {
|
||||
import { createDefaultKeyResultUpdates } from '../../keyresult-update/keyresult-update.seed';
|
||||
import { seedRandomOrganizationDepartments } from '../../organization-department/organization-department.seed';
|
||||
import { seedRandomOrganizationPosition } from '../../organization-positions/organization-position.seed';
|
||||
import { createRandomOrganizationTags, createTags } from '../../tags/tag.seed';
|
||||
|
||||
const allEntities = [
|
||||
TimeOffPolicy,
|
||||
@@ -384,6 +388,11 @@ export class SeedDataService {
|
||||
|
||||
await createDefaultProductTypes(this.connection, defaultOrganizations);
|
||||
|
||||
await createDefaultOrganizationProjects(
|
||||
this.connection,
|
||||
defaultOrganizations
|
||||
);
|
||||
|
||||
await createDefaultProducts(this.connection, tenant);
|
||||
|
||||
await createDefaultTimeFrames(
|
||||
@@ -624,10 +633,20 @@ export class SeedDataService {
|
||||
|
||||
await createSkills(this.connection);
|
||||
await createLanguages(this.connection);
|
||||
await createTags(this.connection);
|
||||
|
||||
const tags = await createRandomOrganizationTags(
|
||||
this.connection,
|
||||
tenants,
|
||||
tenantOrganizationsMap
|
||||
);
|
||||
|
||||
await createRandomOrganizationProjects(
|
||||
this.connection,
|
||||
this.organizations
|
||||
tenants,
|
||||
tenantOrganizationsMap,
|
||||
tags,
|
||||
env.randomSeedConfig.projectsPerOrganization || 10
|
||||
);
|
||||
await createRandomTask(this.connection);
|
||||
await createRandomTimesheet(this.connection);
|
||||
|
||||
@@ -51,13 +51,10 @@ export const updateDefaultGoalProgress = async (
|
||||
});
|
||||
goals.forEach(async (goal) => {
|
||||
const progressTotal = goal.keyResults.reduce(
|
||||
(a, b) => a + b.progress * parseInt(b.weight, 10),
|
||||
0
|
||||
);
|
||||
const weightTotal = goal.keyResults.reduce(
|
||||
(a, b) => a + parseInt(b.weight, 10),
|
||||
(a, b) => a + b.progress * +b.weight,
|
||||
0
|
||||
);
|
||||
const weightTotal = goal.keyResults.reduce((a, b) => a + +b.weight, 0);
|
||||
const goalProgress = Math.round(progressTotal / weightTotal);
|
||||
await connection.manager.update(
|
||||
Goal,
|
||||
|
||||
@@ -82,7 +82,6 @@ export class HelpCenterArticleController extends CrudController<
|
||||
description: 'Record not found'
|
||||
})
|
||||
@UseGuards(PermissionGuard)
|
||||
// @Permissions(PermissionsEnum.ORG_CANDIDATES_INTERVIEWERS_EDIT)
|
||||
@Delete('deleteBulkByCategoryId')
|
||||
async deleteBulkByCategoryId(
|
||||
@Query('data', ParseJsonPipe) data: any
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { Entity, Column, ManyToMany, JoinTable } from 'typeorm';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Base } from '../core/entities/base';
|
||||
import { IHelpCenterArticle } from '@gauzy/models';
|
||||
import { IHelpCenterArticle, IHelpCenterAuthor } from '@gauzy/models';
|
||||
import { HelpCenterAuthor } from '../help-center-author/help-center-author.entity';
|
||||
|
||||
@Entity('knowledge_base_article')
|
||||
export class HelpCenterArticle extends Base implements IHelpCenterArticle {
|
||||
@@ -32,4 +33,13 @@ export class HelpCenterArticle extends Base implements IHelpCenterArticle {
|
||||
@ApiProperty({ type: Number })
|
||||
@Column()
|
||||
index: number;
|
||||
|
||||
@ManyToMany(
|
||||
(type) => HelpCenterAuthor,
|
||||
(helpCenterAuthor) => helpCenterAuthor.articles
|
||||
)
|
||||
@JoinTable({
|
||||
name: 'help_center_author'
|
||||
})
|
||||
authors?: IHelpCenterAuthor[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Controller, UseGuards } from '@nestjs/common';
|
||||
import { CrudController } from '../core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { HelpCenterAuthor } from './help-center-author.entity';
|
||||
import { HelpCenterAuthorService } from './help-center-author.service';
|
||||
|
||||
@ApiTags('knowledge_base_author')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@Controller()
|
||||
export class HelpCenterAuthorController extends CrudController<
|
||||
HelpCenterAuthor
|
||||
> {
|
||||
constructor(
|
||||
private readonly helpCenterAuthorService: HelpCenterAuthorService
|
||||
) {
|
||||
super(helpCenterAuthorService);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Entity, Column, ManyToMany, JoinTable } from 'typeorm';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Base } from '../core/entities/base';
|
||||
import { IHelpCenterAuthor, IHelpCenterArticle } from '@gauzy/models';
|
||||
import { HelpCenterArticle } from '../help-center-article/help-center-article.entity';
|
||||
|
||||
@Entity('knowledge_base_author')
|
||||
export class HelpCenterAuthor extends Base implements IHelpCenterAuthor {
|
||||
@ApiProperty({ type: String })
|
||||
@Column()
|
||||
employeeId: string;
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
@Column()
|
||||
articleId: string;
|
||||
|
||||
@ManyToMany(
|
||||
(type) => HelpCenterArticle,
|
||||
(helpCenterArticle) => helpCenterArticle.authors
|
||||
)
|
||||
@JoinTable({
|
||||
name: 'help_center_author'
|
||||
})
|
||||
articles: IHelpCenterArticle[];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { User } from '../user/user.entity';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HelpCenterAuthorService } from './help-center-author.service';
|
||||
import { HelpCenterAuthorController } from './help-center-author.controller';
|
||||
import { HelpCenterAuthor } from './help-center-author.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([HelpCenterAuthor, User])],
|
||||
providers: [HelpCenterAuthorService, UserService],
|
||||
controllers: [HelpCenterAuthorController],
|
||||
exports: [HelpCenterAuthorService]
|
||||
})
|
||||
export class HelpCenterAuthorModule {}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CrudService } from '../core/crud/crud.service';
|
||||
import { HelpCenterAuthor } from './help-center-author.entity';
|
||||
|
||||
@Injectable()
|
||||
export class HelpCenterAuthorService extends CrudService<HelpCenterAuthor> {
|
||||
constructor(
|
||||
@InjectRepository(HelpCenterAuthor)
|
||||
private readonly HelpCenterAuthorRepository: Repository<
|
||||
HelpCenterAuthor
|
||||
>
|
||||
) {
|
||||
super(HelpCenterAuthorRepository);
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ export const seedRandomOrganizationDepartments = async (
|
||||
const employmentDepartment = new OrganizationDepartment();
|
||||
employmentDepartment.name = name;
|
||||
employmentDepartment.organizationId = organizationId;
|
||||
employmentDepartment.tenant = tenant;
|
||||
return employmentDepartment;
|
||||
});
|
||||
departments = [...departments, ...organizationDepartments];
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as faker from 'faker';
|
||||
import { Tag } from '../tags/tag.entity';
|
||||
import { OrganizationProjects } from './organization-projects.entity';
|
||||
import { Organization } from '@gauzy/models';
|
||||
import { Tenant } from '../tenant/tenant.entity';
|
||||
|
||||
const defaultProjects = [
|
||||
'Gauzy Platform (Open-Source)',
|
||||
@@ -11,7 +12,7 @@ const defaultProjects = [
|
||||
'Gauzy Platform DevOps'
|
||||
];
|
||||
|
||||
export const createRandomOrganizationProjects = async (
|
||||
export const createDefaultOrganizationProjects = async (
|
||||
connection: Connection,
|
||||
defaultOrganizations: Organization[]
|
||||
) => {
|
||||
@@ -32,3 +33,33 @@ export const createRandomOrganizationProjects = async (
|
||||
});
|
||||
await connection.manager.save(projects);
|
||||
};
|
||||
|
||||
export const createRandomOrganizationProjects = async (
|
||||
connection: Connection,
|
||||
tenants: Tenant[],
|
||||
tenantOrganizationsMap: Map<Tenant, Organization[]>,
|
||||
tags: Tag[],
|
||||
maxProjectsPerOrganization
|
||||
) => {
|
||||
const projects: OrganizationProjects[] = [];
|
||||
for (const tenant of tenants) {
|
||||
const projectsPerOrganization =
|
||||
Math.floor(Math.random() * (maxProjectsPerOrganization - 5)) + 5;
|
||||
const orgs = tenantOrganizationsMap.get(tenant);
|
||||
orgs.forEach((org) => {
|
||||
let orgTags: Tag[] = [];
|
||||
orgTags = tags.filter((x) => (x.organization = org));
|
||||
for (let i = 0; i < projectsPerOrganization; i++) {
|
||||
const project = new OrganizationProjects();
|
||||
project.tags = [tags[Math.floor(Math.random() * tags.length)]];
|
||||
project.name = faker.company.companyName();
|
||||
project.organizationId = org.id;
|
||||
project.tenant = tenant;
|
||||
project.startDate = faker.date.past(5);
|
||||
project.endDate = faker.date.past(2);
|
||||
projects.push(project);
|
||||
}
|
||||
});
|
||||
}
|
||||
await connection.manager.save(projects);
|
||||
};
|
||||
|
||||
@@ -128,7 +128,8 @@ export const defaultRolePermissions = [
|
||||
PermissionsEnum.APPROVAL_POLICY_VIEW,
|
||||
PermissionsEnum.REQUEST_APPROVAL_EDIT,
|
||||
PermissionsEnum.REQUEST_APPROVAL_VIEW,
|
||||
PermissionsEnum.ORG_CANDIDATES_TASK_EDIT
|
||||
PermissionsEnum.ORG_CANDIDATES_TASK_EDIT,
|
||||
PermissionsEnum.VIEW_MY_TASK
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Connection } from 'typeorm';
|
||||
import * as faker from 'faker';
|
||||
import { Tag } from './tag.entity';
|
||||
import { Tenant } from '../tenant/tenant.entity';
|
||||
import { Organization } from '../organization/organization.entity';
|
||||
|
||||
const tagGlobalNames = [
|
||||
'VIP',
|
||||
'Urgent',
|
||||
'Crazy',
|
||||
'Broken',
|
||||
'TODO',
|
||||
'In Process',
|
||||
'Verified',
|
||||
'Third Party API'
|
||||
];
|
||||
|
||||
const tagOrganizationsNames = [
|
||||
'Program',
|
||||
'Web',
|
||||
'Mobile',
|
||||
'Frontend',
|
||||
'Backend',
|
||||
'Database',
|
||||
'Authentication',
|
||||
'Security',
|
||||
'Dashboard',
|
||||
'API',
|
||||
'Design',
|
||||
'Testing',
|
||||
'Local',
|
||||
'QC',
|
||||
'Production',
|
||||
'Demo'
|
||||
];
|
||||
|
||||
export const createTags = async (connection: Connection): Promise<Tag[]> => {
|
||||
const tags: Tag[] = [];
|
||||
for (const name of tagGlobalNames) {
|
||||
const tag = new Tag();
|
||||
tag.name = name;
|
||||
tag.description = '';
|
||||
tag.color = faker.commerce.color();
|
||||
tags.push(tag);
|
||||
}
|
||||
|
||||
await connection
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(Tag)
|
||||
.values(tags)
|
||||
.execute();
|
||||
|
||||
return tags;
|
||||
};
|
||||
|
||||
export const createRandomOrganizationTags = async (
|
||||
connection: Connection,
|
||||
tenants: Tenant[],
|
||||
tenantOrganizationsMap: Map<Tenant, Organization[]>
|
||||
): Promise<Tag[]> => {
|
||||
let tags: Tag[] = [];
|
||||
|
||||
for (const tenant of tenants) {
|
||||
const organizations = tenantOrganizationsMap.get(tenant);
|
||||
organizations.forEach((org) => {
|
||||
const organizationTags: Tag[] = Object.values(
|
||||
tagOrganizationsNames
|
||||
).map((name) => {
|
||||
const orgTags = new Tag();
|
||||
orgTags.name = name + '-' + org.name;
|
||||
orgTags.description = '';
|
||||
orgTags.color = faker.commerce.color();
|
||||
orgTags.organization = org;
|
||||
orgTags.tenant = tenant;
|
||||
return orgTags;
|
||||
});
|
||||
tags = [...tags, ...organizationTags];
|
||||
});
|
||||
return await connection.manager.save(tags);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ export class TaskService extends CrudService<Task> {
|
||||
.leftJoinAndSelect('task.members', 'members')
|
||||
.leftJoinAndSelect('members.user', 'users')
|
||||
.leftJoinAndSelect('task.teams', 'teams')
|
||||
.leftJoinAndSelect('task.creator', 'users')
|
||||
.leftJoinAndSelect('task.creator', 'creator')
|
||||
.where('"task_members"."employeeId" = :employeeId', { employeeId })
|
||||
.getCount();
|
||||
const items = await this.taskRepository
|
||||
@@ -44,7 +44,7 @@ export class TaskService extends CrudService<Task> {
|
||||
.leftJoinAndSelect('task.members', 'members')
|
||||
.leftJoinAndSelect('members.user', 'users')
|
||||
.leftJoinAndSelect('task.teams', 'teams')
|
||||
.leftJoinAndSelect('task.creator', 'users')
|
||||
.leftJoinAndSelect('task.creator', 'creator')
|
||||
.where((qb) => {
|
||||
const subQuery = qb
|
||||
.subQuery()
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Controller, UseGuards, HttpStatus, Post, Body } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
UseGuards,
|
||||
HttpStatus,
|
||||
Post,
|
||||
Body,
|
||||
Get,
|
||||
Query
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { CrudController } from '../core/crud/crud.controller';
|
||||
import { TimeOffRequest } from './time-off-request.entity';
|
||||
@@ -6,6 +14,8 @@ import { AuthGuard } from '@nestjs/passport';
|
||||
import { TimeOffRequestService } from './time-off-request.service';
|
||||
import { PermissionGuard } from '../shared/guards/auth/permission.guard';
|
||||
import { TimeOffCreateInput as ITimeOffCreateInput } from '@gauzy/models';
|
||||
import { TimeOff as ITimeOff } from '@gauzy/models';
|
||||
import { IPagination } from '../core';
|
||||
|
||||
@ApiTags('TimeOffRequest')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
@@ -15,6 +25,21 @@ export class TimeOffRequestControler extends CrudController<TimeOffRequest> {
|
||||
super(requestService);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Find all time off requests.' })
|
||||
@UseGuards(PermissionGuard)
|
||||
@Get()
|
||||
async findAllProposals(
|
||||
@Query('data') data: string
|
||||
): Promise<IPagination<ITimeOff>> {
|
||||
const { relations, findInput, filterDate } = JSON.parse(data);
|
||||
|
||||
return this.requestService.getAllTimeOffRequests(
|
||||
relations,
|
||||
findInput,
|
||||
filterDate
|
||||
);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Create new time off request / holiday record' })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.CREATED,
|
||||
|
||||
@@ -20,4 +20,40 @@ export class TimeOffRequestService extends CrudService<TimeOffRequest> {
|
||||
|
||||
return await this.timeOfRequestRepository.save(request);
|
||||
}
|
||||
|
||||
async getAllTimeOffRequests(relations, findInput?, filterDate?) {
|
||||
const allRequests = await this.timeOfRequestRepository.find({
|
||||
where: findInput['organziationId'],
|
||||
relations
|
||||
});
|
||||
let items = [];
|
||||
const total = await this.timeOfRequestRepository.count();
|
||||
|
||||
if (findInput['employeeId']) {
|
||||
allRequests.forEach((request) => {
|
||||
request.employees.forEach((e) => {
|
||||
if (e.id === findInput['employeeId']) {
|
||||
items.push(request);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
items = allRequests;
|
||||
}
|
||||
|
||||
if (filterDate) {
|
||||
const dateObject = new Date(filterDate);
|
||||
|
||||
const month = dateObject.getMonth() + 1;
|
||||
const year = dateObject.getFullYear();
|
||||
|
||||
items = [...items].filter((i) => {
|
||||
const currentItemMonth = i.start.getMonth() + 1;
|
||||
const currentItemYear = i.start.getFullYear();
|
||||
return currentItemMonth === month && currentItemYear === year;
|
||||
});
|
||||
}
|
||||
|
||||
return { items, total };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
RelationId,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
AfterLoad
|
||||
} from 'typeorm';
|
||||
import { Entity, Column, RelationId, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { Base } from '../core/entities/base';
|
||||
import { Screenshot as IScreenshot } from '@gauzy/models';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsNumber, IsDateString } from 'class-validator';
|
||||
import { TimeSlot } from './time-slot.entity';
|
||||
import { environment } from '@env-api/environment';
|
||||
|
||||
@Entity('screenshot')
|
||||
export class Screenshot extends Base implements IScreenshot {
|
||||
@@ -46,10 +38,4 @@ export class Screenshot extends Base implements IScreenshot {
|
||||
@IsDateString()
|
||||
@Column({ nullable: true, default: null })
|
||||
deletedAt?: Date;
|
||||
|
||||
@AfterLoad()
|
||||
afterLoad?() {
|
||||
this.fullUrl = environment.baseUrl + '/' + this.fullUrl;
|
||||
this.thumbUrl = environment.baseUrl + '/' + this.thumbUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import { EmployeeService } from '../employee/employee.service';
|
||||
import { OrganizationContactService } from '../organization-contact/organization-contact.service';
|
||||
import { UpworkController } from './upwork.controller';
|
||||
import { UpworkService } from './upwork.service';
|
||||
import { IntegrationMap } from '../integration-map/integration-map.entity';
|
||||
import { IntegrationMapService } from '../integration-map/integration-map.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -22,7 +24,8 @@ import { UpworkService } from './upwork.service';
|
||||
Employee,
|
||||
OrganizationVendor,
|
||||
OrganizationContact,
|
||||
ExpenseCategory
|
||||
ExpenseCategory,
|
||||
IntegrationMap
|
||||
]),
|
||||
CqrsModule
|
||||
],
|
||||
@@ -34,7 +37,8 @@ import { UpworkService } from './upwork.service';
|
||||
EmployeeService,
|
||||
OrganizationVendorsService,
|
||||
OrganizationContactService,
|
||||
ExpenseCategoriesService
|
||||
ExpenseCategoriesService,
|
||||
IntegrationMapService
|
||||
]
|
||||
})
|
||||
export class UpworkModule {}
|
||||
|
||||
@@ -29,21 +29,27 @@ import {
|
||||
import { arrayToObject } from '../core';
|
||||
import { Engagements } from 'upwork-api/lib/routers/hr/engagements.js';
|
||||
import { Workdiary } from 'upwork-api/lib/routers/workdiary.js';
|
||||
import { Snapshot } from 'upwork-api/lib/routers/snapshot.js';
|
||||
import { IntegrationMapSyncEntityCommand } from '../integration-map/commands';
|
||||
import {
|
||||
TimesheetGetCommand,
|
||||
TimesheetCreateCommand,
|
||||
TimeLogCreateCommand,
|
||||
TimeSlotCreateCommand
|
||||
TimeSlotCreateCommand,
|
||||
ScreenshotCreateCommand
|
||||
} from '../timesheet/commands';
|
||||
import * as moment from 'moment';
|
||||
import { OrganizationProjectCreateCommand } from '../organization-projects/commands/organization-project.create.command';
|
||||
import { IntegrationMapService } from '../integration-map/integration-map.service';
|
||||
|
||||
@Injectable()
|
||||
export class UpworkService {
|
||||
private _upworkApi: UpworkApi;
|
||||
|
||||
constructor(private commandBus: CommandBus) {}
|
||||
constructor(
|
||||
private _integrationMapService: IntegrationMapService,
|
||||
private commandBus: CommandBus
|
||||
) {}
|
||||
|
||||
private async _consumerHasAccessToken(consumerKey: string) {
|
||||
const integrationSetting = await this.commandBus.execute(
|
||||
@@ -373,13 +379,14 @@ export class UpworkService {
|
||||
forDate
|
||||
) {
|
||||
const workDiaries = await Promise.all(
|
||||
syncedContracts.map(async (c) => {
|
||||
syncedContracts.map(async (contract) => {
|
||||
const wd = await this.getWorkDiary({
|
||||
contractId: c.sourceId,
|
||||
contractId: contract.sourceId,
|
||||
config,
|
||||
forDate
|
||||
});
|
||||
|
||||
console.log(wd, 'wd');
|
||||
const cells = wd.data.cells;
|
||||
const sourceId = wd.data.contract.record_id;
|
||||
|
||||
@@ -392,7 +399,7 @@ export class UpworkService {
|
||||
employeeId,
|
||||
integrationId,
|
||||
organizationId,
|
||||
projectId: c.gauzyId,
|
||||
projectId: contract.gauzyId,
|
||||
startDate: moment
|
||||
.unix(cells[0].cell_time)
|
||||
.format('YYYY-MM-DD HH:mm:ss'),
|
||||
@@ -412,7 +419,15 @@ export class UpworkService {
|
||||
timeSlotsDto
|
||||
);
|
||||
|
||||
return { integratedTimeLog, integratedTimeSlots };
|
||||
const integratedScreenshots = await this.syncSnapshots(
|
||||
timeSlotsDto
|
||||
);
|
||||
|
||||
return {
|
||||
integratedTimeLog,
|
||||
integratedTimeSlots,
|
||||
integratedScreenshots
|
||||
};
|
||||
})
|
||||
);
|
||||
return workDiaries;
|
||||
@@ -468,4 +483,75 @@ export class UpworkService {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get snapshots for given contractId and Unix time
|
||||
*/
|
||||
async getSnapshotByContractId(
|
||||
getWorkDiaryDto: IGetWorkDiaryDto
|
||||
): Promise<any> {
|
||||
const api = new UpworkApi(getWorkDiaryDto.config);
|
||||
const snapshots = new Snapshot(api);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
api.setAccessToken(
|
||||
getWorkDiaryDto.config.accessToken,
|
||||
getWorkDiaryDto.config.accessSecret,
|
||||
() => {
|
||||
snapshots.getByContract(
|
||||
getWorkDiaryDto.contractId,
|
||||
moment(getWorkDiaryDto.forDate).unix(),
|
||||
(err, data) => (err ? reject(err) : resolve(data))
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Sync Snapshots By Contract
|
||||
*/
|
||||
async syncSnapshots(timeSlotsData) {
|
||||
const {
|
||||
timeSlots = [],
|
||||
employeeId,
|
||||
integrationId,
|
||||
sourceId
|
||||
} = timeSlotsData;
|
||||
const integrationMaps = await timeSlots.map(
|
||||
async ({
|
||||
id,
|
||||
cell_time,
|
||||
screenshot_img,
|
||||
screenshot_img_thmb,
|
||||
snapshot_time
|
||||
}) => {
|
||||
let recordedAt = moment
|
||||
.unix(snapshot_time)
|
||||
.format('YYYY-MM-DD HH:mm:ss');
|
||||
let activityTimestamp = moment
|
||||
.unix(cell_time)
|
||||
.format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
const gauzyScreenshot = await this.commandBus.execute(
|
||||
new ScreenshotCreateCommand({
|
||||
fullUrl: screenshot_img,
|
||||
thumbUrl: screenshot_img_thmb,
|
||||
recordedAt,
|
||||
activityTimestamp,
|
||||
employeeId
|
||||
})
|
||||
);
|
||||
|
||||
return await this._integrationMapService.create({
|
||||
gauzyId: gauzyScreenshot.id,
|
||||
integrationId,
|
||||
sourceId,
|
||||
entity: IntegrationEntity.SCREENSHOT
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return await Promise.all(integrationMaps);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +286,8 @@ export const environment: IEnvironment = {
|
||||
tenants: 5,
|
||||
organizationsPerTenant: 10,
|
||||
employeesPerOrganization: 10,
|
||||
candidatesPerOrganization: 2
|
||||
candidatesPerOrganization: 2,
|
||||
projectsPerOrganization: 30
|
||||
},
|
||||
|
||||
defaultHubstaffUserPass:
|
||||
|
||||
@@ -101,7 +101,7 @@ export const environment: IEnvironment = {
|
||||
|
||||
twitterConfig: {
|
||||
clientId: process.env.TwitterClientId,
|
||||
clientSecret: process.env.TwitterClientSecret
|
||||
clientSecret: process.env.TwitterClientSecret
|
||||
},
|
||||
|
||||
fiverrConfig: {
|
||||
@@ -493,7 +493,8 @@ export const environment: IEnvironment = {
|
||||
tenants: 5,
|
||||
organizationsPerTenant: 2,
|
||||
employeesPerOrganization: 5,
|
||||
candidatesPerOrganization: 2
|
||||
candidatesPerOrganization: 2,
|
||||
projectsPerOrganization: 30
|
||||
},
|
||||
|
||||
defaultHubstaffUserPass:
|
||||
|
||||
@@ -81,6 +81,7 @@ export interface IEnvironment {
|
||||
organizationsPerTenant: number; //No of random organizations seeded will be (organizationsPerTenant * tenants)
|
||||
employeesPerOrganization: number; //No of random employees seeded will be (employeesPerOrganization * organizationsPerTenant * tenants)
|
||||
candidatesPerOrganization: number; //No of random employees seeded will be (candidatesPerOrganization * organizationsPerTenant * tenants)
|
||||
projectsPerOrganization: number; // No of random projects seeded will be (projectsPerOrganization * organizationsPerTenant * tenants)
|
||||
};
|
||||
|
||||
sentry?: {
|
||||
|
||||
@@ -1,23 +1,45 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import {TimeOffPolicy, TimeOffPolicyCreateInput, TimeOffPolicyFindInput, TimeOffPolicyUpdateInput, TimeOffCreateInput, TimeOff} from '@gauzy/models';
|
||||
import {
|
||||
TimeOffPolicy,
|
||||
TimeOffPolicyCreateInput,
|
||||
TimeOffPolicyFindInput,
|
||||
TimeOffPolicyUpdateInput,
|
||||
TimeOffCreateInput,
|
||||
TimeOff,
|
||||
TimeOffFindInput
|
||||
} from '@gauzy/models';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable({ providedIn: 'root'})
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TimeOffService {
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
createPolicy(createInput: TimeOffPolicyCreateInput): Observable<TimeOffPolicy> {
|
||||
return this.http.post<TimeOffPolicy>('/api/time-off-policy/create', createInput);
|
||||
createPolicy(
|
||||
createInput: TimeOffPolicyCreateInput
|
||||
): Observable<TimeOffPolicy> {
|
||||
return this.http.post<TimeOffPolicy>(
|
||||
'/api/time-off-policy/create',
|
||||
createInput
|
||||
);
|
||||
}
|
||||
|
||||
getAllPolicies(relations?: string[], findInput?: TimeOffPolicyFindInput): Observable<{ items: TimeOffPolicy[], total: number }> {
|
||||
getAllPolicies(
|
||||
relations?: string[],
|
||||
findInput?: TimeOffPolicyFindInput
|
||||
): Observable<{ items: TimeOffPolicy[]; total: number }> {
|
||||
const data = JSON.stringify({ relations, findInput });
|
||||
|
||||
return this.http.get<{ items: TimeOffPolicy[], total: number }> (`/api/time-off-policy`, {params: { data } });
|
||||
return this.http.get<{ items: TimeOffPolicy[]; total: number }>(
|
||||
`/api/time-off-policy`,
|
||||
{ params: { data } }
|
||||
);
|
||||
}
|
||||
|
||||
updatePolicy(id: string, updateInput: TimeOffPolicyUpdateInput): Observable<TimeOffPolicy> {
|
||||
updatePolicy(
|
||||
id: string,
|
||||
updateInput: TimeOffPolicyUpdateInput
|
||||
): Observable<TimeOffPolicy> {
|
||||
return this.http.put(`/api/time-off-policy/${id}`, updateInput);
|
||||
}
|
||||
|
||||
@@ -25,6 +47,19 @@ export class TimeOffService {
|
||||
return this.http.delete(`/api/time-off-policy/${id}`);
|
||||
}
|
||||
|
||||
getAllTimeOffRecords(
|
||||
relations?: string[],
|
||||
findInput?: TimeOffFindInput,
|
||||
filterDate?: Date
|
||||
): Observable<{ items: TimeOff[]; total: number }> {
|
||||
const data = JSON.stringify({ relations, findInput, filterDate });
|
||||
|
||||
return this.http.get<{ items: TimeOff[]; total: number }>(
|
||||
'/api/time-off-request',
|
||||
{ params: { data } }
|
||||
);
|
||||
}
|
||||
|
||||
createRequest(timeOffRequest: TimeOffCreateInput): Observable<TimeOff> {
|
||||
return this.http.post('/api/time-off-request', timeOffRequest);
|
||||
}
|
||||
|
||||
+7
@@ -5,6 +5,7 @@
|
||||
'title-width': isCalendar,
|
||||
'title-width-small': !isCalendar
|
||||
}"
|
||||
class="nameValidParent"
|
||||
>
|
||||
<label for="inputTitle" class="label">{{
|
||||
'FORM.LABELS.TITLE' | translate
|
||||
@@ -23,6 +24,12 @@
|
||||
}}"
|
||||
formControlName="title"
|
||||
/>
|
||||
<div *ngIf="isTitleExisted" class="nameValid">
|
||||
{{
|
||||
'CANDIDATES_PAGE.EDIT_CANDIDATE.INTERVIEW.INTERVIEW_TITLE_EXIST'
|
||||
| translate
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
*ngIf="!isCalendar"
|
||||
|
||||
+12
@@ -53,3 +53,15 @@
|
||||
.button {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.nameValidParent {
|
||||
position: relative;
|
||||
|
||||
.nameValid {
|
||||
position: absolute;
|
||||
top: 66px;
|
||||
left: 0px;
|
||||
font-size: 11px;
|
||||
color: #ff3d71;
|
||||
}
|
||||
}
|
||||
|
||||
+27
-2
@@ -3,12 +3,19 @@ import {
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
ChangeDetectorRef,
|
||||
Input
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter
|
||||
} from '@angular/core';
|
||||
import { FormBuilder, Validators } from '@angular/forms';
|
||||
import { takeUntil, first } from 'rxjs/operators';
|
||||
import { Subject } from 'rxjs';
|
||||
import { Employee, IDateRange, ICandidateInterviewers } from '@gauzy/models';
|
||||
import {
|
||||
Employee,
|
||||
IDateRange,
|
||||
ICandidateInterviewers,
|
||||
ICandidateInterview
|
||||
} from '@gauzy/models';
|
||||
import { EmployeesService } from '../../../../@core/services';
|
||||
import { NbDialogService } from '@nebular/theme';
|
||||
import { CandidateCalendarInfoComponent } from '../../candidate-calendar-info/candidate-calendar-info.component';
|
||||
@@ -21,12 +28,17 @@ import { CandidateCalendarInfoComponent } from '../../candidate-calendar-info/ca
|
||||
export class CandidateInterviewFormComponent implements OnInit, OnDestroy {
|
||||
@Input() interviewers: ICandidateInterviewers[];
|
||||
@Input() isCalendar: boolean;
|
||||
@Input() interviewList: ICandidateInterview[];
|
||||
@Output() titleExist = new EventEmitter<any>();
|
||||
|
||||
yesterday = new Date();
|
||||
form: any;
|
||||
employees: Employee[];
|
||||
employeeIds: string[];
|
||||
isMeeting: boolean;
|
||||
interviewNames: string[];
|
||||
selectedEmployeeIds = null;
|
||||
isTitleExisted: boolean;
|
||||
selectedRange: IDateRange = { start: null, end: null };
|
||||
private _ngDestroy$ = new Subject<void>();
|
||||
|
||||
@@ -40,12 +52,25 @@ export class CandidateInterviewFormComponent implements OnInit, OnDestroy {
|
||||
ngOnInit() {
|
||||
this.yesterday.setDate(this.yesterday.getDate() - 1);
|
||||
this.loadFormData();
|
||||
this.form.valueChanges.subscribe((item) => {
|
||||
this.interviewNames.forEach((el) => {
|
||||
this.isTitleExisted =
|
||||
el === item.title.toLocaleLowerCase() ? true : false;
|
||||
if (this.isTitleExisted) {
|
||||
this.titleExist.emit();
|
||||
}
|
||||
});
|
||||
});
|
||||
this.employeeService
|
||||
.getAll(['user'])
|
||||
.pipe(takeUntil(this._ngDestroy$))
|
||||
.subscribe((employees) => {
|
||||
this.employees = employees.items;
|
||||
});
|
||||
this.interviewNames = [];
|
||||
this.interviewList.forEach((tech) => {
|
||||
this.interviewNames.push(tech.title.toLocaleLowerCase());
|
||||
});
|
||||
|
||||
//if editing
|
||||
this.employeeIds = this.interviewers
|
||||
|
||||
+2
@@ -45,7 +45,9 @@
|
||||
</div>
|
||||
|
||||
<ga-candidate-interview-form
|
||||
[interviewList]="interviewList"
|
||||
#candidateInterviewForm
|
||||
(titleExist)="titleExist()"
|
||||
[isCalendar]="isCalendar"
|
||||
[interviewers]="editData?.interviewers"
|
||||
></ga-candidate-interview-form>
|
||||
|
||||
+7
@@ -44,6 +44,7 @@ export class CandidateInterviewMutationComponent
|
||||
@Input() isCalendar: boolean;
|
||||
@Input() selectedRangeCalendar: IDateRange;
|
||||
@Input() header: string;
|
||||
@Input() interviewList: ICandidateInterview[];
|
||||
@ViewChild('stepper')
|
||||
stepper: NbStepperComponent;
|
||||
@ViewChild('candidateCriterionsForm')
|
||||
@@ -61,6 +62,7 @@ export class CandidateInterviewMutationComponent
|
||||
candidates: Candidate[] = [];
|
||||
selectedInterviewers: string[];
|
||||
criterionsId = null;
|
||||
isTitleExist = false;
|
||||
emptyInterview = {
|
||||
title: '',
|
||||
interviewers: null,
|
||||
@@ -93,6 +95,11 @@ export class CandidateInterviewMutationComponent
|
||||
this.candidates = items;
|
||||
}
|
||||
|
||||
titleExist() {
|
||||
//TO DO
|
||||
//this.isTitleExist = true;
|
||||
}
|
||||
|
||||
async ngAfterViewInit() {
|
||||
this.form = this.candidateInterviewForm.form;
|
||||
//if editing
|
||||
|
||||
+3
-3
@@ -110,8 +110,8 @@
|
||||
<div class="row" *ngIf="invitationType === 'EMPLOYEE'">
|
||||
<div class="col-sm-12">
|
||||
<div class="form-group">
|
||||
<label for="clientsSelect" class="label label-with-select"
|
||||
>{{ 'FORM.LABELS.CLIENTS_OPTIONAL' | translate }}
|
||||
<label for="contactsSelect" class="label label-with-select"
|
||||
>{{ 'FORM.LABELS.CONTACTS_OPTIONAL' | translate }}
|
||||
<button
|
||||
nbButton
|
||||
ghost
|
||||
@@ -122,7 +122,7 @@
|
||||
</button>
|
||||
</label>
|
||||
<ng-select
|
||||
id="clientsSelect"
|
||||
id="contactsSelect"
|
||||
class="adjust-height"
|
||||
[hideSelected]="true"
|
||||
multiple="true"
|
||||
|
||||
@@ -137,7 +137,13 @@ export class InvitesComponent extends TranslationBaseComponent
|
||||
|
||||
try {
|
||||
const { items } = await this.inviteService.getAll(
|
||||
['projects', 'invitedBy', 'role', 'clients', 'departments'],
|
||||
[
|
||||
'projects',
|
||||
'invitedBy',
|
||||
'role',
|
||||
'organizationContact',
|
||||
'departments'
|
||||
],
|
||||
{
|
||||
organizationId: this.selectedOrganizationId
|
||||
}
|
||||
@@ -175,8 +181,8 @@ export class InvitesComponent extends TranslationBaseComponent
|
||||
projectNames: (invite.projects || []).map(
|
||||
(project) => project.name
|
||||
),
|
||||
clientNames: (invite.clients || []).map(
|
||||
(client) => client.name
|
||||
clientNames: (invite.organizationContact || []).map(
|
||||
(organizationContact) => organizationContact.name
|
||||
),
|
||||
departmentNames: (invite.departments || []).map(
|
||||
(department) => department.name
|
||||
@@ -215,8 +221,8 @@ export class InvitesComponent extends TranslationBaseComponent
|
||||
renderComponent: ProjectNamesComponent,
|
||||
filter: false
|
||||
},
|
||||
clients: {
|
||||
title: this.getTranslation('SM_TABLE.CLIENTS'),
|
||||
contact: {
|
||||
title: this.getTranslation('SM_TABLE.CONTACTS'),
|
||||
type: 'custom',
|
||||
renderComponent: ClientNamesComponent,
|
||||
filter: false
|
||||
@@ -252,12 +258,12 @@ export class InvitesComponent extends TranslationBaseComponent
|
||||
|
||||
if (this.invitationType === InvitationTypeEnum.USER) {
|
||||
delete settingsSmartTable['columns']['projects'];
|
||||
delete settingsSmartTable['columns']['clients'];
|
||||
delete settingsSmartTable['columns']['contact'];
|
||||
delete settingsSmartTable['columns']['departments'];
|
||||
}
|
||||
if (this.invitationType === InvitationTypeEnum.CANDIDATE) {
|
||||
delete settingsSmartTable['columns']['projects'];
|
||||
delete settingsSmartTable['columns']['clients'];
|
||||
delete settingsSmartTable['columns']['contact'];
|
||||
delete settingsSmartTable['columns']['roleName'];
|
||||
}
|
||||
|
||||
|
||||
+55
-82
@@ -5,7 +5,9 @@ import { takeUntil } from 'rxjs/operators';
|
||||
import {
|
||||
Candidate,
|
||||
ICandidateInterview,
|
||||
ICandidateFeedback
|
||||
ICandidateFeedback,
|
||||
ICandidateTechnologies,
|
||||
ICandidatePersonalQualities
|
||||
} from '@gauzy/models';
|
||||
import { CandidateFeedbacksService } from 'apps/gauzy/src/app/@core/services/candidate-feedbacks.service';
|
||||
|
||||
@@ -23,8 +25,6 @@ export class CandidateCriterionsRatingChartComponent implements OnDestroy {
|
||||
data: any;
|
||||
options: any;
|
||||
backgroundColor: string[] = [];
|
||||
currLabels = [];
|
||||
currRating = [];
|
||||
private _ngDestroy$ = new Subject<void>();
|
||||
constructor(
|
||||
private themeService: NbThemeService,
|
||||
@@ -44,95 +44,68 @@ export class CandidateCriterionsRatingChartComponent implements OnDestroy {
|
||||
const criterionsRating = [];
|
||||
this.feedbacks.forEach((feedback) => {
|
||||
this.interviewList.forEach((interview) => {
|
||||
if (interview.id === feedback.interviewId) {
|
||||
if (!currInterviews.includes(interview)) {
|
||||
currInterviews.push(interview);
|
||||
}
|
||||
if (
|
||||
interview.id === feedback.interviewId &&
|
||||
!currInterviews.includes(interview)
|
||||
) {
|
||||
currInterviews.push(interview);
|
||||
}
|
||||
});
|
||||
feedback.criterionsRating.forEach((techEl) => {
|
||||
criterionsRating.push(techEl);
|
||||
feedback.criterionsRating.forEach((criterionEl) => {
|
||||
currInterviews.forEach((interview) => {
|
||||
interview.personalQualities.forEach(
|
||||
(pq: ICandidatePersonalQualities) => {
|
||||
if (pq.id === criterionEl.personalQualityId) {
|
||||
criterionsRating.push({
|
||||
name: pq.name,
|
||||
rating: criterionEl.rating
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
interview.technologies.forEach(
|
||||
(t: ICandidateTechnologies) => {
|
||||
if (t.id === criterionEl.technologyId) {
|
||||
criterionsRating.push({
|
||||
name: t.name,
|
||||
rating: criterionEl.rating
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
this.currRating = [];
|
||||
const arr1 = criterionsRating
|
||||
.reduce((prev, curr) => {
|
||||
const existing = prev.find(
|
||||
(data) => data.technologyId === curr.technologyId
|
||||
);
|
||||
if (!existing) {
|
||||
return [...prev, { ...curr, rating: [curr.rating] }];
|
||||
}
|
||||
existing.rating.push(curr.rating);
|
||||
return [...prev];
|
||||
}, [])
|
||||
.map((data) => {
|
||||
const rating =
|
||||
data.rating.reduce((prev, curr) => prev + curr) /
|
||||
data.rating.length;
|
||||
return { ...data, rating };
|
||||
})
|
||||
.filter((item) => item.technologyId);
|
||||
|
||||
const arr2 = criterionsRating
|
||||
.reduce((prev, curr) => {
|
||||
const existing = prev.find(
|
||||
(data) =>
|
||||
data.personalQualityId === curr.personalQualityId
|
||||
);
|
||||
if (!existing) {
|
||||
return [...prev, { ...curr, rating: [curr.rating] }];
|
||||
}
|
||||
existing.rating.push(curr.rating);
|
||||
return [...prev];
|
||||
}, [])
|
||||
.map((data) => {
|
||||
const rating =
|
||||
data.rating.reduce((prev, curr) => prev + curr) /
|
||||
data.rating.length;
|
||||
return { ...data, rating };
|
||||
})
|
||||
.filter((item) => item.personalQualityId);
|
||||
|
||||
this.currRating = [...arr1, ...arr2];
|
||||
this.currLabels = this.getCriterionsName(currInterviews);
|
||||
this.labels = [];
|
||||
this.rating = [];
|
||||
this.currLabels[1].forEach((labelId, index) => {
|
||||
this.loadColor(this.currLabels[0]);
|
||||
this.currRating.forEach((itemRating) => {
|
||||
if (
|
||||
itemRating.technologyId === labelId ||
|
||||
itemRating.personalQualityId === labelId
|
||||
) {
|
||||
this.rating.push(itemRating.rating);
|
||||
this.labels.push(this.currLabels[0][index]);
|
||||
}
|
||||
});
|
||||
});
|
||||
this.rating = this.getCriterionsRating(criterionsRating).map(
|
||||
(x) => x.rating
|
||||
);
|
||||
this.labels = this.getCriterionsRating(criterionsRating).map(
|
||||
(x) => x.name
|
||||
);
|
||||
this.loadChart();
|
||||
}
|
||||
}
|
||||
getCriterionsName(currInterviews: ICandidateInterview[]) {
|
||||
const resName = [];
|
||||
const resId = [];
|
||||
currInterviews.forEach((item) => {
|
||||
item.technologies.forEach((tech) => {
|
||||
if (!resName.includes(tech.name)) {
|
||||
resName.push(tech.name);
|
||||
resId.push(tech.id);
|
||||
getCriterionsRating(criterionsRating) {
|
||||
return criterionsRating
|
||||
.reduce((prev, curr) => {
|
||||
const existing = prev.find((data) => data.name === curr.name);
|
||||
if (!existing) {
|
||||
return [...prev, { ...curr, rating: [curr.rating] }];
|
||||
}
|
||||
existing.rating.push(curr.rating);
|
||||
return [...prev];
|
||||
}, [])
|
||||
.map((data) => {
|
||||
this.loadColor(data.rating);
|
||||
const rating = (
|
||||
data.rating.reduce((prev, curr) => prev + curr) /
|
||||
data.rating.length
|
||||
).toFixed(2);
|
||||
return { ...data, rating };
|
||||
});
|
||||
item.personalQualities.forEach((qual) => {
|
||||
if (!resName.includes(qual.name)) {
|
||||
resName.push(qual.name);
|
||||
resId.push(qual.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
return [resName, resId];
|
||||
}
|
||||
|
||||
private loadChart() {
|
||||
this.themeService
|
||||
.getJsTheme()
|
||||
@@ -170,8 +143,8 @@ export class CandidateCriterionsRatingChartComponent implements OnDestroy {
|
||||
};
|
||||
});
|
||||
}
|
||||
loadColor(labels: string[]) {
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
loadColor(data: string[]) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const color =
|
||||
i % 2 === 0
|
||||
? 'rgba(75, 192, 192, 0.2)'
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ export class CriterionsRatingChartComponent implements OnDestroy {
|
||||
x.forEach((item, index) => (sumArr[index] += item))
|
||||
);
|
||||
const result = [];
|
||||
sumArr.forEach((x) => result.push(x / ratings.length));
|
||||
sumArr.forEach((x) => result.push((x / ratings.length).toFixed(2)));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -67,7 +67,8 @@ export class EditCandidateInterviewComponent extends TranslationBaseComponent
|
||||
header: this.getTranslation(
|
||||
'CANDIDATES_PAGE.EDIT_CANDIDATE.INTERVIEW.SCHEDULE_INTERVIEW'
|
||||
),
|
||||
selectedCandidate: this.selectedCandidate
|
||||
selectedCandidate: this.selectedCandidate,
|
||||
interviewList: this.interviewList
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
+12
@@ -50,6 +50,18 @@
|
||||
>
|
||||
</div>
|
||||
</nb-card-header>
|
||||
|
||||
<nb-card-body class="existedNames" *ngIf="existedQualNames?.length > 0">
|
||||
<span>{{
|
||||
'CANDIDATES_PAGE.CRITERIONS.ALREADY_EXISTED' | translate
|
||||
}}</span>
|
||||
<div class="existedNames-card">
|
||||
<div class="existedName" *ngFor="let name of existedQualNames">
|
||||
{{ name }}
|
||||
</div>
|
||||
</div>
|
||||
</nb-card-body>
|
||||
|
||||
<nb-card-body class="qualities">
|
||||
<div
|
||||
class="qualities-card"
|
||||
|
||||
+30
@@ -46,3 +46,33 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.existedNames {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&-card {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.existedName {
|
||||
border: 1px #ff3d70 solid;
|
||||
margin: 0.2rem 0;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 48%;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
+56
-17
@@ -19,6 +19,8 @@ export class CandidatePersonalQualitiesComponent
|
||||
personalQualitiesList: ICandidatePersonalQualities[];
|
||||
form: FormGroup;
|
||||
editId = null;
|
||||
existedQualNames: string[];
|
||||
qualityNames: string[] = [];
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private readonly toastrService: NbToastrService,
|
||||
@@ -47,6 +49,18 @@ export class CandidatePersonalQualitiesComponent
|
||||
name: ['', Validators.required]
|
||||
})
|
||||
);
|
||||
this.form.valueChanges.subscribe((item) => {
|
||||
this.existedQualNames = [];
|
||||
const enteredName = item.qualities[0].name;
|
||||
this.personalQualitiesList.forEach((el) => {
|
||||
if (
|
||||
enteredName !== '' &&
|
||||
el.name.toLocaleLowerCase().includes(enteredName)
|
||||
) {
|
||||
this.existedQualNames.push(el.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async loadQualities() {
|
||||
@@ -55,6 +69,10 @@ export class CandidatePersonalQualitiesComponent
|
||||
this.personalQualitiesList = res.items.filter(
|
||||
(item) => !item.interviewId
|
||||
);
|
||||
this.qualityNames = [];
|
||||
this.personalQualitiesList.forEach((tech) => {
|
||||
this.qualityNames.push(tech.name.toLocaleLowerCase());
|
||||
});
|
||||
}
|
||||
}
|
||||
async save() {
|
||||
@@ -70,27 +88,48 @@ export class CandidatePersonalQualitiesComponent
|
||||
}
|
||||
|
||||
async update(formValue: ICandidatePersonalQualities) {
|
||||
try {
|
||||
await this.candidatePersonalQualitiesService.update(this.editId, {
|
||||
...formValue
|
||||
});
|
||||
this.editId = null;
|
||||
this.toastrSuccess('UPDATED');
|
||||
this.loadQualities();
|
||||
} catch (error) {
|
||||
this.toastrError(error);
|
||||
if (!this.qualityNames.includes(formValue.name.toLocaleLowerCase())) {
|
||||
try {
|
||||
await this.candidatePersonalQualitiesService.update(
|
||||
this.editId,
|
||||
{
|
||||
...formValue
|
||||
}
|
||||
);
|
||||
this.editId = null;
|
||||
this.toastrSuccess('UPDATED');
|
||||
this.loadQualities();
|
||||
} catch (error) {
|
||||
this.toastrError(error);
|
||||
}
|
||||
} else {
|
||||
this.toastrService.danger(
|
||||
this.getTranslation(
|
||||
'CANDIDATES_PAGE.CRITERIONS.TOASTR_ALREADY_EXIST'
|
||||
),
|
||||
this.getTranslation('TOASTR.TITLE.ERROR')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async create(formValue: ICandidatePersonalQualities) {
|
||||
try {
|
||||
await this.candidatePersonalQualitiesService.create({
|
||||
...formValue
|
||||
});
|
||||
this.toastrSuccess('CREATED');
|
||||
this.loadQualities();
|
||||
} catch (error) {
|
||||
this.toastrError(error);
|
||||
if (!this.qualityNames.includes(formValue.name.toLocaleLowerCase())) {
|
||||
try {
|
||||
await this.candidatePersonalQualitiesService.create({
|
||||
...formValue
|
||||
});
|
||||
this.toastrSuccess('CREATED');
|
||||
this.loadQualities();
|
||||
} catch (error) {
|
||||
this.toastrError(error);
|
||||
}
|
||||
} else {
|
||||
this.toastrService.danger(
|
||||
this.getTranslation(
|
||||
'CANDIDATES_PAGE.CRITERIONS.TOASTR_ALREADY_EXIST'
|
||||
),
|
||||
this.getTranslation('TOASTR.TITLE.ERROR')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -51,6 +51,16 @@
|
||||
>
|
||||
</div></nb-card-header
|
||||
>
|
||||
<nb-card-body class="existedNames" *ngIf="existedTechNames?.length > 0">
|
||||
<span>{{
|
||||
'CANDIDATES_PAGE.CRITERIONS.ALREADY_EXISTED' | translate
|
||||
}}</span>
|
||||
<div class="existedNames-card">
|
||||
<div class="existedName" *ngFor="let name of existedTechNames">
|
||||
{{ name }}
|
||||
</div>
|
||||
</div>
|
||||
</nb-card-body>
|
||||
<nb-card-body class="technologies">
|
||||
<div
|
||||
class="technologies-card"
|
||||
|
||||
+30
@@ -46,3 +46,33 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.existedNames {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&-card {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.existedName {
|
||||
border: 1px #ff3d70 solid;
|
||||
margin: 0.2rem 0;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 48%;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
+57
-17
@@ -16,6 +16,8 @@ export class CandidateTechnologiesComponent extends TranslationBaseComponent
|
||||
implements OnInit, OnDestroy {
|
||||
private _ngDestroy$ = new Subject<void>();
|
||||
technologiesList: ICandidateTechnologies[];
|
||||
existedTechNames: string[];
|
||||
technologyNames: string[] = [];
|
||||
form: FormGroup;
|
||||
editId = null;
|
||||
constructor(
|
||||
@@ -45,6 +47,18 @@ export class CandidateTechnologiesComponent extends TranslationBaseComponent
|
||||
name: ['', Validators.required]
|
||||
})
|
||||
);
|
||||
this.form.valueChanges.subscribe((item) => {
|
||||
this.existedTechNames = [];
|
||||
const enteredName = item.technologies[0].name;
|
||||
this.technologiesList.forEach((el) => {
|
||||
if (
|
||||
enteredName !== '' &&
|
||||
el.name.toLocaleLowerCase().includes(enteredName)
|
||||
) {
|
||||
this.existedTechNames.push(el.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async loadTechnologies() {
|
||||
@@ -53,6 +67,10 @@ export class CandidateTechnologiesComponent extends TranslationBaseComponent
|
||||
this.technologiesList = res.items.filter(
|
||||
(item) => !item.interviewId
|
||||
);
|
||||
this.technologyNames = [];
|
||||
this.technologiesList.forEach((tech) => {
|
||||
this.technologyNames.push(tech.name.toLocaleLowerCase());
|
||||
});
|
||||
}
|
||||
}
|
||||
async save() {
|
||||
@@ -68,27 +86,49 @@ export class CandidateTechnologiesComponent extends TranslationBaseComponent
|
||||
}
|
||||
|
||||
async update(formValue: ICandidateTechnologies) {
|
||||
try {
|
||||
await this.candidateTechnologiesService.update(this.editId, {
|
||||
...formValue
|
||||
});
|
||||
this.editId = null;
|
||||
this.toastrSuccess('UPDATED');
|
||||
this.loadTechnologies();
|
||||
} catch (error) {
|
||||
this.toastrError(error);
|
||||
if (
|
||||
!this.technologyNames.includes(formValue.name.toLocaleLowerCase())
|
||||
) {
|
||||
try {
|
||||
await this.candidateTechnologiesService.update(this.editId, {
|
||||
...formValue
|
||||
});
|
||||
this.editId = null;
|
||||
this.toastrSuccess('UPDATED');
|
||||
this.loadTechnologies();
|
||||
} catch (error) {
|
||||
this.toastrError(error);
|
||||
}
|
||||
} else {
|
||||
this.toastrService.danger(
|
||||
this.getTranslation(
|
||||
'CANDIDATES_PAGE.CRITERIONS.TOASTR_ALREADY_EXIST'
|
||||
),
|
||||
this.getTranslation('TOASTR.TITLE.ERROR')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async create(formValue: ICandidateTechnologies) {
|
||||
try {
|
||||
await this.candidateTechnologiesService.create({
|
||||
...formValue
|
||||
});
|
||||
this.toastrSuccess('CREATED');
|
||||
this.loadTechnologies();
|
||||
} catch (error) {
|
||||
this.toastrError(error);
|
||||
if (
|
||||
!this.technologyNames.includes(formValue.name.toLocaleLowerCase())
|
||||
) {
|
||||
try {
|
||||
await this.candidateTechnologiesService.create({
|
||||
...formValue
|
||||
});
|
||||
this.toastrSuccess('CREATED');
|
||||
this.loadTechnologies();
|
||||
} catch (error) {
|
||||
this.toastrError(error);
|
||||
}
|
||||
} else {
|
||||
this.toastrService.danger(
|
||||
this.getTranslation(
|
||||
'CANDIDATES_PAGE.CRITERIONS.TOASTR_ALREADY_EXIST'
|
||||
),
|
||||
this.getTranslation('TOASTR.TITLE.ERROR')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<nb-card size="giant" class="min-width-80vw">
|
||||
<nb-card size="giant" class="min-width-60vw">
|
||||
<nb-card-header>
|
||||
<div class="main-header">
|
||||
<h4>{{ goal.name }}</h4>
|
||||
@@ -58,7 +58,9 @@
|
||||
<nb-card-body>
|
||||
<nb-tabset fullWidth>
|
||||
<nb-tab tabTitle="{{ 'GOALS_PAGE.KEY_RESULTS' | translate }}">
|
||||
<nb-card *ngFor="let keyResult of goal.keyResults">
|
||||
<nb-card
|
||||
*ngFor="let keyResult of goal.keyResults; let i = index"
|
||||
>
|
||||
<nb-card-header>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
@@ -84,7 +86,7 @@
|
||||
<div class="col-md-2">
|
||||
<nb-actions>
|
||||
<nb-action
|
||||
(click)="keyResultUpdate(keyResult)"
|
||||
(click)="keyResultDetails(i, keyResult)"
|
||||
icon="eye"
|
||||
></nb-action>
|
||||
</nb-actions>
|
||||
@@ -96,12 +98,6 @@
|
||||
<nb-tab tabTitle="{{ 'GOALS_PAGE.UPDATES' | translate }}">
|
||||
<nb-card>
|
||||
<nb-card-header>
|
||||
<div class="main-header">
|
||||
<h6>{{ 'GOALS_PAGE.UPDATES' | translate }}</h6>
|
||||
<button nbButton status="primary">
|
||||
{{ 'BUTTONS.ADD_NEW' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="section mt-2 overflow-scroll"
|
||||
*ngIf="!!this.updates"
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.min-width-80vw {
|
||||
min-width: 80vw !important;
|
||||
.min-width-60vw {
|
||||
min-width: 60vw !important;
|
||||
}
|
||||
|
||||
@@ -7,13 +7,18 @@ import { first } from 'rxjs/operators';
|
||||
import { KeyResultService } from '../../../@core/services/keyresult.service';
|
||||
import { GoalService } from '../../../@core/services/goal.service';
|
||||
import { AlertModalComponent } from '../../../@shared/alert-modal/alert-modal.component';
|
||||
import { KeyResultDetailsComponent } from '../keyresult-details/keyresult-details.component';
|
||||
import { ToastrService } from '../../../@core/services/toastr.service';
|
||||
import { TranslationBaseComponent } from '../../../@shared/language-base/translation-base.component';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ga-goal-details',
|
||||
templateUrl: './goal-details.component.html',
|
||||
styleUrls: ['./goal-details.component.scss']
|
||||
})
|
||||
export class GoalDetailsComponent implements OnInit {
|
||||
export class GoalDetailsComponent extends TranslationBaseComponent
|
||||
implements OnInit {
|
||||
goal: Goal;
|
||||
src: string;
|
||||
ownerName: string;
|
||||
@@ -23,8 +28,12 @@ export class GoalDetailsComponent implements OnInit {
|
||||
private employeeService: EmployeesService,
|
||||
private dialogService: NbDialogService,
|
||||
private keyResultService: KeyResultService,
|
||||
private goalService: GoalService
|
||||
) {}
|
||||
private goalService: GoalService,
|
||||
private toastrService: ToastrService,
|
||||
readonly translateService: TranslateService
|
||||
) {
|
||||
super(translateService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
const employee = await this.employeeService.getEmployeeById(
|
||||
@@ -59,6 +68,30 @@ export class GoalDetailsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
async keyResultDetails(index, selectedKeyResult) {
|
||||
const dialog = this.dialogService.open(KeyResultDetailsComponent, {
|
||||
hasScroll: true,
|
||||
context: {
|
||||
keyResult: selectedKeyResult
|
||||
}
|
||||
});
|
||||
const response = await dialog.onClose.pipe(first()).toPromise();
|
||||
if (!!response) {
|
||||
if (response === 'deleted') {
|
||||
this.goal.keyResults.splice(index, 1);
|
||||
this.toastrService.danger(
|
||||
this.getTranslation('TOASTR.MESSAGE.KEY_RESULT_DELETED'),
|
||||
this.getTranslation('TOASTR.TITLE.SUCCESS')
|
||||
);
|
||||
} else {
|
||||
this.goal.keyResults[index] = response;
|
||||
this.goal.progress = this.calculateGoalProgress(
|
||||
this.goal.keyResults
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async keyResultUpdate(selectedKeyResult) {
|
||||
const dialog = this.dialogService.open(KeyResultUpdateComponent, {
|
||||
hasScroll: true,
|
||||
@@ -75,20 +108,19 @@ export class GoalDetailsComponent implements OnInit {
|
||||
selectedKeyResult.id,
|
||||
keyResultData
|
||||
);
|
||||
const keyResNumber = this.goal.keyResults.length;
|
||||
this.goal.progress = this.calculateGoalProgress(
|
||||
keyResNumber,
|
||||
this.goal.keyResults
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
calculateGoalProgress(totalCount, keyResults) {
|
||||
calculateGoalProgress(keyResults) {
|
||||
const progressTotal = keyResults.reduce(
|
||||
(a: number, b: KeyResult) => a + b.progress,
|
||||
(a: number, b: KeyResult) => a + b.progress * +b.weight,
|
||||
0
|
||||
);
|
||||
return Math.round(progressTotal / totalCount);
|
||||
const weightTotal = keyResults.reduce((a, b) => a + +b.weight, 0);
|
||||
return Math.round(progressTotal / weightTotal);
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<nb-card [nbSpinner]="loading" nbSpinnerStatus="primary" nbSpinnerSize="large">
|
||||
<nb-card-header>
|
||||
<nb-card-header *ngIf="!loading">
|
||||
<div class="main-header">
|
||||
<h4>
|
||||
{{ 'GOALS_PAGE.HEADER' | translate }} {{ organizationName }}
|
||||
|
||||
@@ -138,7 +138,6 @@ export class GoalsComponent extends TranslationBaseComponent
|
||||
const response = await dialog.onClose.pipe(first()).toPromise();
|
||||
if (!!response) {
|
||||
this.goals[index].progress = this.calculateGoalProgress(
|
||||
null,
|
||||
this.goals[index].keyResults
|
||||
);
|
||||
const goalData = this.goals[index];
|
||||
@@ -216,7 +215,7 @@ export class GoalsComponent extends TranslationBaseComponent
|
||||
}
|
||||
}
|
||||
|
||||
calculateGoalProgress(totalCount, keyResults) {
|
||||
calculateGoalProgress(keyResults) {
|
||||
const progressTotal = keyResults.reduce(
|
||||
(a, b) => a + b.progress * parseInt(b.weight, 10),
|
||||
0
|
||||
@@ -345,8 +344,7 @@ export class GoalsComponent extends TranslationBaseComponent
|
||||
const dialog = this.dialogService.open(KeyResultDetailsComponent, {
|
||||
hasScroll: true,
|
||||
context: {
|
||||
keyResult: selectedkeyResult,
|
||||
owner: this.goals[index].owner.id
|
||||
keyResult: selectedkeyResult
|
||||
}
|
||||
});
|
||||
const response = await dialog.onClose.pipe(first()).toPromise();
|
||||
@@ -358,9 +356,7 @@ export class GoalsComponent extends TranslationBaseComponent
|
||||
);
|
||||
this.loadPage();
|
||||
} else {
|
||||
const keyResNumber = this.goals[index].keyResults.length;
|
||||
this.goals[index].progress = this.calculateGoalProgress(
|
||||
keyResNumber,
|
||||
this.goals[index].keyResults
|
||||
);
|
||||
const goalData = this.goals[index];
|
||||
@@ -396,9 +392,7 @@ export class GoalsComponent extends TranslationBaseComponent
|
||||
selectedKeyResult.id,
|
||||
keyResultData
|
||||
);
|
||||
const keyResNumber = this.goals[index].keyResults.length;
|
||||
this.goals[index].progress = this.calculateGoalProgress(
|
||||
keyResNumber,
|
||||
this.goals[index].keyResults
|
||||
);
|
||||
await this.goalService.update(
|
||||
|
||||
+4
-7
@@ -50,11 +50,11 @@ export class KeyResultParametersComponent implements OnInit, OnDestroy {
|
||||
JSON.stringify(this.data.allKeyResults)
|
||||
);
|
||||
let weightSum = this.data.allKeyResults.reduce(
|
||||
(a, b) => a + parseInt(b.weight, 10),
|
||||
(a, b) => a + +b.weight,
|
||||
0
|
||||
);
|
||||
this.keyResultWeight = Math.round(
|
||||
parseInt(this.weightForm.value.weight, 10) * (100 / weightSum)
|
||||
+this.weightForm.value.weight * (100 / weightSum)
|
||||
);
|
||||
this.weightForm.controls['weight'].valueChanges
|
||||
.pipe(takeUntil(this._ngDestroy$))
|
||||
@@ -63,12 +63,9 @@ export class KeyResultParametersComponent implements OnInit, OnDestroy {
|
||||
(element) =>
|
||||
element.id === this.data.selectedKeyResult.id
|
||||
).weight = weight;
|
||||
weightSum = allKeyResult.reduce(
|
||||
(a, b) => a + parseInt(b.weight, 10),
|
||||
0
|
||||
);
|
||||
weightSum = allKeyResult.reduce((a, b) => a + +b.weight, 0);
|
||||
this.keyResultWeight = Math.round(
|
||||
parseInt(weight, 10) * (100 / weightSum)
|
||||
+weight * (100 / weightSum)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<ngx-avatar class="ml-3" size="sm" [src]="src" [name]="ownerName">
|
||||
</ngx-avatar>
|
||||
<p class="mt-3 ml-3">
|
||||
Deadline:
|
||||
{{ 'KEY_RESULT_PAGE.FORM.LABELS.DEADLINE' | translate }}:
|
||||
<span class="font-weight-bold">{{ endDate | date }}</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -131,16 +131,16 @@
|
||||
</div>
|
||||
</nb-card-body>
|
||||
<nb-card-footer>
|
||||
<button nbButton status="success" (click)="closeDialog()">
|
||||
{{ 'BUTTONS.SAVE' | translate }}
|
||||
<button status="danger" nbButton (click)="deleteKeyResult()">
|
||||
{{ 'BUTTONS.DELETE' | translate }}
|
||||
</button>
|
||||
<button
|
||||
class="float-right"
|
||||
status="danger"
|
||||
nbButton
|
||||
(click)="deleteKeyResult()"
|
||||
status="success"
|
||||
(click)="closeDialog()"
|
||||
>
|
||||
{{ 'BUTTONS.DELETE' | translate }}
|
||||
{{ 'BUTTONS.SAVE' | translate }}
|
||||
</button>
|
||||
</nb-card-footer>
|
||||
</nb-card>
|
||||
|
||||
@@ -4,16 +4,18 @@ import { EmployeesService } from '../../../@core/services';
|
||||
import {
|
||||
KeyResult,
|
||||
KeyResultUpdates,
|
||||
KeyResultDeadlineEnum
|
||||
KeyResultDeadlineEnum,
|
||||
RolesEnum
|
||||
} from '@gauzy/models';
|
||||
import { KeyResultUpdateComponent } from '../keyresult-update/keyresult-update.component';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { first, takeUntil } from 'rxjs/operators';
|
||||
import { KeyResultService } from '../../../@core/services/keyresult.service';
|
||||
import { Subject } from 'rxjs';
|
||||
import { AlertModalComponent } from '../../../@shared/alert-modal/alert-modal.component';
|
||||
import { KeyResultProgressChartComponent } from '../keyresult-progress-chart/keyresult-progress-chart.component';
|
||||
import { GoalSettingsService } from '../../../@core/services/goal-settings.service';
|
||||
import { isFuture, isToday, compareDesc, isPast } from 'date-fns';
|
||||
import { Store } from '../../../@core/services/store.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ga-keyresult-details',
|
||||
@@ -21,7 +23,6 @@ import { isFuture, isToday, compareDesc, isPast } from 'date-fns';
|
||||
styleUrls: ['./keyresult-details.component.scss']
|
||||
})
|
||||
export class KeyResultDetailsComponent implements OnInit, OnDestroy {
|
||||
owner: string;
|
||||
src: string;
|
||||
keyResult: KeyResult;
|
||||
updates: KeyResultUpdates[];
|
||||
@@ -39,12 +40,13 @@ export class KeyResultDetailsComponent implements OnInit, OnDestroy {
|
||||
private employeeService: EmployeesService,
|
||||
private dialogService: NbDialogService,
|
||||
private keyResultService: KeyResultService,
|
||||
private goalSettingsService: GoalSettingsService
|
||||
private goalSettingsService: GoalSettingsService,
|
||||
private store: Store
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const employee = await this.employeeService.getEmployeeById(
|
||||
this.owner,
|
||||
this.keyResult.owner.id,
|
||||
['user']
|
||||
);
|
||||
this.src = employee.user.imageUrl;
|
||||
@@ -55,7 +57,7 @@ export class KeyResultDetailsComponent implements OnInit, OnDestroy {
|
||||
// prevent keyresult updates after deadline
|
||||
this.goalSettingsService
|
||||
.getTimeFrameByName(this.keyResult.goal.deadline)
|
||||
.then((res) => {
|
||||
.then(async (res) => {
|
||||
const timeFrame = res.items[0];
|
||||
this.startDate = new Date(timeFrame.startDate);
|
||||
if (
|
||||
@@ -72,6 +74,20 @@ export class KeyResultDetailsComponent implements OnInit, OnDestroy {
|
||||
(isFuture(this.endDate) || isToday(this.endDate)) &&
|
||||
isPast(this.startDate);
|
||||
}
|
||||
await this.store.user$
|
||||
.pipe(takeUntil(this._ngDestroy$))
|
||||
.subscribe((user) => {
|
||||
if (
|
||||
user.role.name !== RolesEnum.SUPER_ADMIN &&
|
||||
user.role.name !== RolesEnum.ADMIN &&
|
||||
user.employee.id !== this.keyResult.owner.id &&
|
||||
!!this.keyResult.lead.id
|
||||
? user.employee.id !== this.keyResult.lead.id
|
||||
: false
|
||||
) {
|
||||
this.isUpdatable = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<nb-card size="large" class="max-width-500">
|
||||
<nb-card size="large" class="max-width-30vw">
|
||||
<nb-card-header>
|
||||
<div class="main-header">
|
||||
<h4>{{ 'KEY_RESULT_PAGE.UPDATE_KEY_RESULT' | translate }}</h4>
|
||||
@@ -11,7 +11,12 @@
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<h6>{{ keyResult.name }}</h6>
|
||||
<div *ngIf="keyResult.type == 'Number'">
|
||||
<div
|
||||
*ngIf="
|
||||
keyResult.type == keyResultTypeEnum.NUMBER ||
|
||||
keyResult.type == keyResultTypeEnum.CURRENCY
|
||||
"
|
||||
>
|
||||
<div class="row p-2">
|
||||
<div class="col-md-6">
|
||||
<p>
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
.max-width-500 {
|
||||
max-width: 600px;
|
||||
min-width: 500px;
|
||||
.max-width-30vw {
|
||||
max-width: 30vw;
|
||||
min-width: 30vw;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,8 @@ export class KeyResultUpdateComponent extends TranslationBaseComponent
|
||||
} else if (this.keyResult.type === KeyResultTypeEnum.TRUE_OR_FALSE) {
|
||||
this.hideStatus = true;
|
||||
this.keyResultUpdateForm.patchValue({
|
||||
newValueBoolean: this.keyResult.update === 1 ? true : false
|
||||
newValueBoolean: this.keyResult.update === 1 ? true : false,
|
||||
newStatus: KeyResultUpdateStatusEnum.ON_TRACK
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="multi-select">
|
||||
<ga-employee-multi-select
|
||||
[allEmployees]="employees"
|
||||
(selectedChange)="onMembersSelected($event)"
|
||||
[selectedEmployeeIds]="employeeIds"
|
||||
>
|
||||
</ga-employee-multi-select>
|
||||
</div>
|
||||
<div class="edit-node-field">
|
||||
<p class="label">{{ 'HELP_PAGE.ARTICLE_TEXT' | translate }}:</p>
|
||||
<div class="ck-editor">
|
||||
|
||||
@@ -57,3 +57,7 @@
|
||||
margin-left: -15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.multi-select {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IHelpCenterArticle } from '@gauzy/models';
|
||||
import { IHelpCenterArticle, Employee } from '@gauzy/models';
|
||||
import { Component, OnDestroy, OnInit, Input } from '@angular/core';
|
||||
import { NbDialogRef } from '@nebular/theme';
|
||||
import { Subject } from 'rxjs';
|
||||
@@ -6,6 +6,8 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { TranslationBaseComponent } from '../../../@shared/language-base/translation-base.component';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { HelpCenterArticleService } from '../../../@core/services/help-center-article.service';
|
||||
import { EmployeesService } from '../../../@core/services';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ga-add-article',
|
||||
@@ -23,6 +25,7 @@ export class AddArticleComponent extends TranslationBaseComponent
|
||||
protected dialogRef: NbDialogRef<AddArticleComponent>,
|
||||
readonly translateService: TranslateService,
|
||||
private readonly fb: FormBuilder,
|
||||
private employeeService: EmployeesService,
|
||||
private helpCenterArticleService: HelpCenterArticleService
|
||||
) {
|
||||
super(translateService);
|
||||
@@ -31,24 +34,46 @@ export class AddArticleComponent extends TranslationBaseComponent
|
||||
public data = {
|
||||
name: '',
|
||||
description: '',
|
||||
data: ''
|
||||
data: '',
|
||||
valid: null
|
||||
};
|
||||
public selectedPrivacy = false;
|
||||
public selectedStatus = false;
|
||||
employees: Employee[];
|
||||
selectedEmployeeIds = null;
|
||||
employeeIds: string[] = [];
|
||||
|
||||
ngOnInit() {
|
||||
this.form = this.fb.group({
|
||||
name: ['', Validators.required],
|
||||
desc: ['', Validators.required],
|
||||
data: ['', Validators.required]
|
||||
data: ['', Validators.required],
|
||||
valid: [null, Validators.required]
|
||||
});
|
||||
this.employeeService
|
||||
.getAll(['user'])
|
||||
.pipe(takeUntil(this._ngDestroy$))
|
||||
.subscribe((employees) => {
|
||||
this.employees = employees.items;
|
||||
});
|
||||
|
||||
if (this.editType === 'add') this.loadFormData(this.data);
|
||||
if (this.editType === 'edit') {
|
||||
this.loadFormData(this.article);
|
||||
this.selectedPrivacy = this.article.privacy;
|
||||
this.selectedStatus = this.article.draft;
|
||||
// this.article.authors.forEach((author) => this.employeeIds.push(author));
|
||||
}
|
||||
}
|
||||
|
||||
onMembersSelected(event: string[]) {
|
||||
this.selectedEmployeeIds = event;
|
||||
const value = this.selectedEmployeeIds[0] ? true : null;
|
||||
this.form.patchValue({
|
||||
valid: value
|
||||
});
|
||||
}
|
||||
|
||||
toggleStatus(event: boolean) {
|
||||
this.selectedStatus = event;
|
||||
}
|
||||
@@ -61,7 +86,8 @@ export class AddArticleComponent extends TranslationBaseComponent
|
||||
this.form.patchValue({
|
||||
name: data.name,
|
||||
desc: data.description,
|
||||
data: data.data
|
||||
data: data.data,
|
||||
valid: null
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EmployeeMultiSelectModule } from './../../../@shared/employee/employee-multi-select/employee-multi-select.module';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ThemeModule, HttpLoaderFactory } from '../../../@theme/theme.module';
|
||||
@@ -27,6 +28,7 @@ import { HelpCenterArticleService } from '../../../@core/services/help-center-ar
|
||||
NbToggleModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
EmployeeMultiSelectModule,
|
||||
TranslateModule.forChild({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
|
||||
@@ -30,9 +30,11 @@
|
||||
class="cards"
|
||||
*ngFor="let article of this.nodes; let i = index"
|
||||
>
|
||||
<div>
|
||||
<div class="main-article" (click)="openArticle(i)">
|
||||
<div class="row-name">
|
||||
<p class="name">{{ article.name }}</p>
|
||||
<p class="name">
|
||||
{{ article.name }}
|
||||
</p>
|
||||
<p class="draft" *ngIf="article.draft === true">
|
||||
{{ 'HELP_PAGE.DRAFT' | translate }}
|
||||
</p>
|
||||
@@ -41,6 +43,11 @@
|
||||
</p>
|
||||
</div>
|
||||
<p>{{ article.description }}</p>
|
||||
<div
|
||||
class="data"
|
||||
*ngIf="showData[i]"
|
||||
[innerHtml]="dataArray[i]"
|
||||
></div>
|
||||
<div class="user-info">
|
||||
<p>
|
||||
{{ 'HELP_PAGE.WRITTEN_BY' | translate }}
|
||||
@@ -48,7 +55,7 @@
|
||||
<div class="create-at">
|
||||
<nb-icon class="icon" icon="clock-outline"></nb-icon>
|
||||
<p>
|
||||
{{ 'HELP_PAGE.CREATE_AT' | translate }}:
|
||||
{{ 'HELP_PAGE.CREATED_AT' | translate }}:
|
||||
{{ article.createdAt | date: 'short' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
@@ -66,7 +67,6 @@
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
margin-top: 27px;
|
||||
font-size: 24px;
|
||||
display: none;
|
||||
}
|
||||
@@ -78,7 +78,7 @@
|
||||
}
|
||||
|
||||
.draft {
|
||||
padding: 0 0.2rem;
|
||||
padding: 0 0.4rem;
|
||||
color: #fff;
|
||||
background-color: #aaaeb3;
|
||||
margin-left: 10px;
|
||||
@@ -89,7 +89,7 @@
|
||||
text-align: center;
|
||||
}
|
||||
.privacy {
|
||||
padding: 0 0.2rem;
|
||||
padding: 0 0.4rem;
|
||||
color: #fff;
|
||||
background-color: blueviolet;
|
||||
margin-left: 10px;
|
||||
@@ -105,3 +105,11 @@
|
||||
font-size: 18px;
|
||||
padding-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.data {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.main-article {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SafeHtml, DomSanitizer } from '@angular/platform-browser';
|
||||
import { IHelpCenterArticle, IHelpCenter } from '@gauzy/models';
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { TranslationBaseComponent } from '../../@shared/language-base/translation-base.component';
|
||||
@@ -21,10 +22,13 @@ export class HelpCenterComponent extends TranslationBaseComponent
|
||||
private dialogService: NbDialogService,
|
||||
readonly translateService: TranslateService,
|
||||
private helpCenterArticleService: HelpCenterArticleService,
|
||||
private readonly toastrService: NbToastrService
|
||||
private readonly toastrService: NbToastrService,
|
||||
private sanitizer: DomSanitizer
|
||||
) {
|
||||
super(translateService);
|
||||
}
|
||||
public showData: boolean[] = [];
|
||||
public dataArray: SafeHtml[] = [];
|
||||
public nodes: IHelpCenterArticle[] = [];
|
||||
public categoryName = '';
|
||||
public categoryId = '';
|
||||
@@ -36,6 +40,10 @@ export class HelpCenterComponent extends TranslationBaseComponent
|
||||
this.loadArticles(this.categoryId);
|
||||
}
|
||||
|
||||
openArticle(i) {
|
||||
this.showData[i] = !this.showData[i];
|
||||
}
|
||||
|
||||
deletedNode() {
|
||||
this.categoryId = '';
|
||||
this.categoryName = '';
|
||||
@@ -43,9 +51,17 @@ export class HelpCenterComponent extends TranslationBaseComponent
|
||||
}
|
||||
|
||||
async loadArticles(id) {
|
||||
this.showData = [];
|
||||
this.dataArray = [];
|
||||
const result = await this.helpCenterArticleService.findByCategoryId(id);
|
||||
if (result) {
|
||||
this.nodes = result;
|
||||
for (let i = 0; i < this.nodes.length; i++) {
|
||||
this.showData.push(false);
|
||||
this.dataArray.push(
|
||||
this.sanitizer.bypassSecurityTrustHtml(this.nodes[i].data)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ const routes: Routes = [
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'clients',
|
||||
path: 'contacts',
|
||||
loadChildren: () =>
|
||||
import('./work-in-progress/work-in-progress.module').then(
|
||||
(m) => m.WorkInProgressModule
|
||||
|
||||
@@ -225,10 +225,10 @@ export class PagesComponent implements OnInit, OnDestroy {
|
||||
title: 'My Tasks',
|
||||
icon: 'person-outline',
|
||||
link: '/pages/tasks/me',
|
||||
hidden: !this.isEmployee,
|
||||
data: {
|
||||
translated: false,
|
||||
translationKey: 'MENU.MY_TASKS'
|
||||
translationKey: 'MENU.MY_TASKS',
|
||||
permissionKeys: [PermissionsEnum.VIEW_MY_TASK]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
-1
@@ -48,7 +48,6 @@
|
||||
permissionChanged(permission, $event)
|
||||
"
|
||||
labelPosition="start"
|
||||
[disabled]="selectedRole === superAdminRole"
|
||||
status="basic"
|
||||
>{{
|
||||
'ORGANIZATIONS_PAGE.PERMISSIONS.' +
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ import {
|
||||
} from '@gauzy/models';
|
||||
import { NbToastrService } from '@nebular/theme';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { RolePermissionsService } from 'apps/gauzy/src/app/@core/services/role-permissions.service';
|
||||
import { RoleService } from 'apps/gauzy/src/app/@core/services/role.service';
|
||||
import { RolePermissionsService } from '../../../@core/services/role-permissions.service';
|
||||
import { RoleService } from '../../../@core/services/role.service';
|
||||
import { Subject } from 'rxjs';
|
||||
import { first, takeUntil } from 'rxjs/operators';
|
||||
import { TranslationBaseComponent } from '../../../@shared/language-base/translation-base.component';
|
||||
|
||||
@@ -74,12 +74,9 @@ export class TaskDialogComponent extends TranslationBaseComponent
|
||||
|
||||
private async loadProjects() {
|
||||
const organizationId = this._organizationsStore.selectedOrganization.id;
|
||||
const { items } = await this.organizationProjectsService.getAll(
|
||||
['client'],
|
||||
{
|
||||
organizationId
|
||||
}
|
||||
);
|
||||
const { items } = await this.organizationProjectsService.getAll([], {
|
||||
organizationId
|
||||
});
|
||||
|
||||
if (items) this.projects = items;
|
||||
}
|
||||
|
||||
+3
-6
@@ -69,12 +69,9 @@ export class TeamTaskDialogComponent extends TranslationBaseComponent
|
||||
|
||||
private async loadProjects() {
|
||||
const organizationId = this._organizationsStore.selectedOrganization.id;
|
||||
const { items } = await this.organizationProjectsService.getAll(
|
||||
['client'],
|
||||
{
|
||||
organizationId
|
||||
}
|
||||
);
|
||||
const { items } = await this.organizationProjectsService.getAll([], {
|
||||
organizationId
|
||||
});
|
||||
|
||||
if (items) this.projects = items;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<h4>{{ 'TIME_OFF_PAGE.HEADER' | translate }}</h4>
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<div class="row">
|
||||
<div class="row mb-3">
|
||||
<button
|
||||
nbButton
|
||||
status="primary"
|
||||
@@ -60,7 +60,7 @@
|
||||
[settings]="settingsSmartTable"
|
||||
[source]="sourceSmartTable"
|
||||
(userRowSelect)="selectRecord($event)"
|
||||
#employeesTable
|
||||
#timeOffTable
|
||||
>
|
||||
</ng2-smart-table>
|
||||
</nb-card-body>
|
||||
|
||||
@@ -8,6 +8,8 @@ import { TimeOffRequestMutationComponent } from '../../@shared/time-off/time-off
|
||||
import { TimeOffService } from '../../@core/services/time-off.service';
|
||||
import { LocalDataSource } from 'ng2-smart-table';
|
||||
import { untilDestroyed } from 'ngx-take-until-destroy';
|
||||
import { PictureNameTagsComponent } from '../../@shared/table-components/picture-name-tags/picture-name-tags.component';
|
||||
import { DatePipe } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'ngx-time-off',
|
||||
@@ -23,14 +25,15 @@ export class TimeOffComponent implements OnInit, OnDestroy {
|
||||
private store: Store
|
||||
) {}
|
||||
|
||||
settingsSmartTable: object;
|
||||
sourceSmartTable = new LocalDataSource();
|
||||
|
||||
private _selectedOrganizationId: string;
|
||||
timeOffRequest: TimeOff;
|
||||
selectedDate: Date;
|
||||
selectedEmployeeId: string;
|
||||
selectedStatus = 'All';
|
||||
timeOffStatuses = Object.keys(StatusTypesEnum);
|
||||
settingsSmartTable: object;
|
||||
sourceSmartTable = new LocalDataSource();
|
||||
loading = false;
|
||||
displayHolidays = true;
|
||||
hasEditPermission = false;
|
||||
@@ -53,7 +56,7 @@ export class TimeOffComponent implements OnInit, OnDestroy {
|
||||
this._loadTableData();
|
||||
} else {
|
||||
if (this._selectedOrganizationId) {
|
||||
this._loadTableData(null, this._selectedOrganizationId);
|
||||
this._loadTableData(this._selectedOrganizationId);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -67,7 +70,7 @@ export class TimeOffComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
if (this._selectedOrganizationId) {
|
||||
this.selectedEmployeeId = null;
|
||||
this._loadTableData(null, this._selectedOrganizationId);
|
||||
this._loadTableData(this._selectedOrganizationId);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -78,24 +81,84 @@ export class TimeOffComponent implements OnInit, OnDestroy {
|
||||
if (org) {
|
||||
this._selectedOrganizationId = org.id;
|
||||
if (this.loading) {
|
||||
this._loadTableData(
|
||||
this.store.selectedEmployee
|
||||
? this.store.selectedEmployee.id
|
||||
: null,
|
||||
this.store.selectedEmployee &&
|
||||
this.store.selectedEmployee.id
|
||||
? null
|
||||
: this._selectedOrganizationId
|
||||
);
|
||||
this._loadTableData(this._selectedOrganizationId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._loadSmartTableSettings();
|
||||
this._loadTableData();
|
||||
}
|
||||
|
||||
private _loadTableData(
|
||||
employeeId = this.selectedEmployeeId,
|
||||
orgId?: string
|
||||
) {}
|
||||
private _loadSmartTableSettings() {
|
||||
this.settingsSmartTable = {
|
||||
actions: false,
|
||||
columns: {
|
||||
fullName: {
|
||||
title: 'Employee',
|
||||
type: 'custom',
|
||||
renderComponent: PictureNameTagsComponent,
|
||||
class: 'align-row'
|
||||
},
|
||||
description: {
|
||||
title: 'Description',
|
||||
type: 'string',
|
||||
class: 'text-center'
|
||||
},
|
||||
policy: {
|
||||
title: 'Policy',
|
||||
type: 'string',
|
||||
class: 'text-center'
|
||||
},
|
||||
start: {
|
||||
title: 'Start',
|
||||
type: 'date',
|
||||
valuePrepareFunction: (date) =>
|
||||
new DatePipe('en-GB').transform(date, 'dd/MM/yyyy'),
|
||||
class: 'text-center'
|
||||
},
|
||||
end: {
|
||||
title: 'End',
|
||||
type: 'date',
|
||||
valuePrepareFunction: (date) =>
|
||||
new DatePipe('en-GB').transform(date, 'dd/MM/yyyy'),
|
||||
class: 'text-center'
|
||||
},
|
||||
requestDate: {
|
||||
title: 'Request Date',
|
||||
type: 'date',
|
||||
valuePrepareFunction: (date) =>
|
||||
new DatePipe('en-GB').transform(date, 'dd/MM/yyyy'),
|
||||
class: 'text-center'
|
||||
},
|
||||
status: {
|
||||
title: 'Status',
|
||||
type: 'string',
|
||||
class: 'text-center'
|
||||
}
|
||||
},
|
||||
pager: {
|
||||
display: true,
|
||||
perPage: 8
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _loadTableData(orgId?: string) {
|
||||
this.timeOffService
|
||||
.getAllTimeOffRecords(
|
||||
['employees', 'employees.user', 'policy'],
|
||||
{
|
||||
organizationId: orgId,
|
||||
employeeId: this.selectedEmployeeId || null
|
||||
},
|
||||
this.selectedDate || null
|
||||
)
|
||||
.pipe(first())
|
||||
.subscribe((res) => {
|
||||
this.sourceSmartTable.load(res.items);
|
||||
});
|
||||
}
|
||||
|
||||
openTimeOffSettings() {
|
||||
this.router.navigate(['/pages/employees/time-off/settings']);
|
||||
@@ -131,12 +194,21 @@ export class TimeOffComponent implements OnInit, OnDestroy {
|
||||
|
||||
private _createRecord() {
|
||||
if (this.timeOffRequest) {
|
||||
this.timeOffService.createRequest(this.timeOffRequest).pipe(first()).subscribe(() => {
|
||||
this.toastrService.success(
|
||||
`Time off record ${this.timeOffRequest.description} successfully created!`,
|
||||
'Success'
|
||||
this.timeOffService
|
||||
.createRequest(this.timeOffRequest)
|
||||
.pipe(first())
|
||||
.subscribe(
|
||||
() => {
|
||||
this.toastrService.success(
|
||||
`Time off record ${this.timeOffRequest.description} successfully created!`,
|
||||
'Success'
|
||||
);
|
||||
},
|
||||
() =>
|
||||
this.toastrService.danger(
|
||||
'Unable to create Time off record'
|
||||
)
|
||||
);
|
||||
}, () => this.toastrService.danger('Unable to create Time off record'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
"INVITED_BY": "Поканен от",
|
||||
"EXPIRE_DATE": "Изтича",
|
||||
"CLIENTS": "Клиенти",
|
||||
"CONTACTS": "Contacts",
|
||||
"DEPARTMENTS": "Отдели",
|
||||
"DESCRIPTION": "Описание",
|
||||
"APPLIED": "Applied",
|
||||
@@ -157,7 +158,7 @@
|
||||
"ROLE": "Роля",
|
||||
"EMAILS": "Електронни пощи",
|
||||
"PROJECTS_OPTIONAL": "Проекти (по избор)",
|
||||
"CLIENTS_OPTIONAL": "Клиенти (по избор)",
|
||||
"CONTACTS_OPTIONAL": "Contacts (Optional)",
|
||||
"DEPARTMENTS_OPTIONAL": "Отдели (по избор)",
|
||||
"PROJECTS": "Проекти",
|
||||
"ADD_NEW_DEPARTMENT": "Добави Нов Отдел",
|
||||
@@ -863,7 +864,8 @@
|
||||
"INTERVIEWERS": "interviewer(s)",
|
||||
"HIRE": "Hire",
|
||||
"POSTPONE": "Postpone",
|
||||
"REJECT": "Reject"
|
||||
"REJECT": "Reject",
|
||||
"INTERVIEW_TITLE_EXIST": "An interview with such name already exists"
|
||||
}
|
||||
},
|
||||
"INTERVIEW_INFO_MODAL": {
|
||||
@@ -916,6 +918,8 @@
|
||||
"CANDIDATE_CRITERIONS": "Candidates criterions",
|
||||
"RATE_CANDIDATE_BY_CRITERIONS": "Rate candidate by criterions",
|
||||
"TECHNOLOGY_STACK": "Technology Stack",
|
||||
"ALREADY_EXISTED": "Already existed",
|
||||
"TOASTR_ALREADY_EXIST": "A criterion with such name already exists",
|
||||
"PERSONAL_QUALITIES": "Personal Qualities",
|
||||
"CHOOSE_CRITERIONS": "Choose criterions for candidate",
|
||||
"TECHNOLOGY_PLACEHOLDER": "If you want to add criteria from the technology stack, you need to create them",
|
||||
@@ -1052,7 +1056,8 @@
|
||||
"ADMINISTRATION": "Администрация"
|
||||
},
|
||||
"ONLY_ADMIN": "These permissions are read-only and enabled only for admin",
|
||||
"INSUFFICIENT": "You do not have sufficient permissions. The following permissions are missing:"
|
||||
"INSUFFICIENT": "You do not have sufficient permissions. The following permissions are missing:",
|
||||
"VIEW_MY_TASK": "View My Tasks"
|
||||
}
|
||||
},
|
||||
"PUBLIC_PAGE": {
|
||||
@@ -1547,7 +1552,7 @@
|
||||
"HELP": "Помощ",
|
||||
"KNOWLEDGE_BASE": "Knowledge base",
|
||||
"CHOSE_ICON": "Chose icon to set",
|
||||
"CREATE_AT": "create at",
|
||||
"CREATED_AT": "created at",
|
||||
"WRITTEN_BY": "written by",
|
||||
"EMPLOYEES": "служители",
|
||||
"ONLY_FOR_EMPLOYEES": "Only for employees",
|
||||
|
||||
@@ -51,9 +51,9 @@
|
||||
"EMAIL": "Email"
|
||||
},
|
||||
"SM_TABLE": {
|
||||
"DATE": "Date",
|
||||
"TITLE": "Title",
|
||||
"STAGE": "Stage",
|
||||
"DATE": "Date",
|
||||
"TITLE": "Title",
|
||||
"STAGE": "Stage",
|
||||
"START_DATE": "Start Date",
|
||||
"END_DATE": "End Date",
|
||||
"CLIENT_NAME": "Client Name",
|
||||
@@ -106,6 +106,7 @@
|
||||
"INVITED_BY": "Invited By",
|
||||
"EXPIRE_DATE": "Expires",
|
||||
"CLIENTS": "Clients",
|
||||
"CONTACTS": "Contacts",
|
||||
"DEPARTMENTS": "Departments",
|
||||
"DESCRIPTION": "Description",
|
||||
"APPLIED": "Applied",
|
||||
@@ -160,7 +161,7 @@
|
||||
"ROLE": "Role",
|
||||
"EMAILS": "Email Addresses",
|
||||
"PROJECTS_OPTIONAL": "Projects (Optional)",
|
||||
"CLIENTS_OPTIONAL": "Clients (Optional)",
|
||||
"CONTACTS_OPTIONAL": "Contacts (Optional)",
|
||||
"DEPARTMENTS_OPTIONAL": "Departments (Optional)",
|
||||
"PROJECTS": "Projects",
|
||||
"ADD_NEW_DEPARTMENT": "Add New Department",
|
||||
@@ -866,7 +867,8 @@
|
||||
"INTERVIEWERS": "interviewer(s)",
|
||||
"HIRE": "Hire",
|
||||
"POSTPONE": "Postpone",
|
||||
"REJECT": "Reject"
|
||||
"REJECT": "Reject",
|
||||
"INTERVIEW_TITLE_EXIST": "An interview with such name already exists"
|
||||
}
|
||||
},
|
||||
"INTERVIEW_INFO_MODAL": {
|
||||
@@ -919,6 +921,8 @@
|
||||
"CANDIDATE_CRITERIONS": "Candidates criterions",
|
||||
"RATE_CANDIDATE_BY_CRITERIONS": "Rate candidate by criterions",
|
||||
"TECHNOLOGY_STACK": "Technology Stack",
|
||||
"ALREADY_EXISTED": "Already existed",
|
||||
"TOASTR_ALREADY_EXIST": "A criterion with such name already exists",
|
||||
"PERSONAL_QUALITIES": "Personal Qualities",
|
||||
"CHOOSE_CRITERIONS": "Choose criterions for candidate",
|
||||
"TECHNOLOGY_PLACEHOLDER": "If you want to add criteria from the technology stack, you need to create them",
|
||||
@@ -1055,7 +1059,8 @@
|
||||
"ADMINISTRATION": "Administration"
|
||||
},
|
||||
"ONLY_ADMIN": "These permissions are read-only and enabled only for admin",
|
||||
"INSUFFICIENT": "You do not have sufficient permissions. The following permissions are missing:"
|
||||
"INSUFFICIENT": "You do not have sufficient permissions. The following permissions are missing:",
|
||||
"VIEW_MY_TASK": "View My Tasks"
|
||||
}
|
||||
},
|
||||
"PUBLIC_PAGE": {
|
||||
@@ -1550,7 +1555,7 @@
|
||||
"HELP": "Help",
|
||||
"KNOWLEDGE_BASE": "Knowledge base",
|
||||
"CHOSE_ICON": "Chose icon to set",
|
||||
"CREATE_AT": "create at",
|
||||
"CREATED_AT": "created at",
|
||||
"WRITTEN_BY": "written by",
|
||||
"EMPLOYEES": "employees",
|
||||
"ONLY_FOR_EMPLOYEES": "Only for employees",
|
||||
@@ -1963,22 +1968,22 @@
|
||||
},
|
||||
"PIPELINES_PAGE": {
|
||||
"HEADER": "Pipelines",
|
||||
"HEADER_STAGES": "Stages",
|
||||
"VIEW_DEALS": "View Deals",
|
||||
"HEADER_FORM_EDIT": "Edit Pipeline",
|
||||
"HEADER_FORM_CREATE": "Create Pipeline",
|
||||
"RECORD_TYPE": "pipeline \"{{ name }}\""
|
||||
},
|
||||
"PIPELINE_DEALS_PAGE": {
|
||||
"HEADER": "Pipeline Deals",
|
||||
"FILTER_BY_STAGE": "Filter by Stage",
|
||||
"RECORD_TYPE": "stage \"{{ title }}\""
|
||||
},
|
||||
"PIPELINE_DEAL_EDIT_PAGE": {
|
||||
"HEADER": "Edit Deal | Pipeline: {{ name }}"
|
||||
},
|
||||
"PIPELINE_DEAL_CREATE_PAGE": {
|
||||
"HEADER": "Create Deal | Pipeline: {{ name }}",
|
||||
"SELECT_STAGE": "Select Stage"
|
||||
}
|
||||
"HEADER_STAGES": "Stages",
|
||||
"VIEW_DEALS": "View Deals",
|
||||
"HEADER_FORM_EDIT": "Edit Pipeline",
|
||||
"HEADER_FORM_CREATE": "Create Pipeline",
|
||||
"RECORD_TYPE": "pipeline \"{{ name }}\""
|
||||
},
|
||||
"PIPELINE_DEALS_PAGE": {
|
||||
"HEADER": "Pipeline Deals",
|
||||
"FILTER_BY_STAGE": "Filter by Stage",
|
||||
"RECORD_TYPE": "stage \"{{ title }}\""
|
||||
},
|
||||
"PIPELINE_DEAL_EDIT_PAGE": {
|
||||
"HEADER": "Edit Deal | Pipeline: {{ name }}"
|
||||
},
|
||||
"PIPELINE_DEAL_CREATE_PAGE": {
|
||||
"HEADER": "Create Deal | Pipeline: {{ name }}",
|
||||
"SELECT_STAGE": "Select Stage"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
"INVITED_BY": "Invited By",
|
||||
"EXPIRE_DATE": "Expires",
|
||||
"CLIENTS": "לקוחות",
|
||||
"CONTACTS": "Contacts",
|
||||
"DEPARTMENTS": "מחלקות",
|
||||
"DESCRIPTION": "Description",
|
||||
"APPLIED": "Applied",
|
||||
@@ -157,7 +158,7 @@
|
||||
"ROLE": "Role",
|
||||
"EMAILS": "Email Addresses",
|
||||
"PROJECTS_OPTIONAL": "Projects (Optional)",
|
||||
"CLIENTS_OPTIONAL": "Clients (Optional)",
|
||||
"CONTACTS_OPTIONAL": "Contacts (Optional)",
|
||||
"DEPARTMENTS_OPTIONAL": "Departments (Optional)",
|
||||
"PROJECTS": "פרויקטים",
|
||||
"ADD_NEW_DEPARTMENT": "Add New Department",
|
||||
@@ -863,7 +864,8 @@
|
||||
"INTERVIEWERS": "interviewer(s)",
|
||||
"HIRE": "Hire",
|
||||
"POSTPONE": "Postpone",
|
||||
"REJECT": "Reject"
|
||||
"REJECT": "Reject",
|
||||
"INTERVIEW_TITLE_EXIST": "An interview with such name already exists"
|
||||
}
|
||||
},
|
||||
"INTERVIEW_INFO_MODAL": {
|
||||
@@ -916,6 +918,8 @@
|
||||
"CANDIDATE_CRITERIONS": "Candidates criterions",
|
||||
"RATE_CANDIDATE_BY_CRITERIONS": "Rate candidate by criterions",
|
||||
"TECHNOLOGY_STACK": "Technology Stack",
|
||||
"ALREADY_EXISTED": "Already existed",
|
||||
"TOASTR_ALREADY_EXIST": "A criterion with such name already exists",
|
||||
"PERSONAL_QUALITIES": "Personal Qualities",
|
||||
"CHOOSE_CRITERIONS": "Choose criterions for candidate",
|
||||
"TECHNOLOGY_PLACEHOLDER": "If you want to add criteria from the technology stack, you need to create them",
|
||||
@@ -1052,7 +1056,8 @@
|
||||
"ADMINISTRATION": "Administration"
|
||||
},
|
||||
"ONLY_ADMIN": "These permissions are read-only and enabled only for admin",
|
||||
"INSUFFICIENT": "You do not have sufficient permissions. The following permissions are missing:"
|
||||
"INSUFFICIENT": "You do not have sufficient permissions. The following permissions are missing:",
|
||||
"VIEW_MY_TASK": "View My Tasks"
|
||||
}
|
||||
},
|
||||
"PUBLIC_PAGE": {
|
||||
@@ -1547,7 +1552,7 @@
|
||||
"HELP": "עזרה",
|
||||
"KNOWLEDGE_BASE": "Knowledge base",
|
||||
"CHOSE_ICON": "Chose icon to set",
|
||||
"CREATE_AT": "create at",
|
||||
"CREATED_AT": "created at",
|
||||
"WRITTEN_BY": "written by",
|
||||
"EMPLOYEES": "עובדים",
|
||||
"ONLY_FOR_EMPLOYEES": "Only for employees",
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
"INVITED_BY": "Приглашен",
|
||||
"EXPIRE_DATE": "Истекает",
|
||||
"CLIENTS": "Клиенты",
|
||||
"CONTACTS": "Контакты",
|
||||
"DEPARTMENTS": "Департаменты",
|
||||
"DESCRIPTION": "Описание",
|
||||
"APPLIED": "Применяется",
|
||||
@@ -157,7 +158,7 @@
|
||||
"ROLE": "Роль",
|
||||
"EMAILS": "Адреса E-mail",
|
||||
"PROJECTS_OPTIONAL": "Проекты (необязательно)",
|
||||
"CLIENTS_OPTIONAL": "Клиенты (необязательно)",
|
||||
"CONTACTS_OPTIONAL": "Контакты (необязательно)",
|
||||
"DEPARTMENTS_OPTIONAL": "Отделы (необязательно)",
|
||||
"PROJECTS": "Проекты",
|
||||
"ADD_NEW_DEPARTMENT": "Добавить новый отдел",
|
||||
@@ -863,7 +864,8 @@
|
||||
"INTERVIEWERS": "интервьюер(ы)",
|
||||
"HIRE": "Нанять",
|
||||
"POSTPONE": "Отложить",
|
||||
"REJECT": "Отклонить"
|
||||
"REJECT": "Отклонить",
|
||||
"INTERVIEW_TITLE_EXIST": "Интервью с таким именем уже существует"
|
||||
}
|
||||
},
|
||||
"INTERVIEW_INFO_MODAL": {
|
||||
@@ -916,6 +918,8 @@
|
||||
"CANDIDATE_CRITERIONS": "Кандидаты критерии",
|
||||
"RATE_CANDIDATE_BY_CRITERIONS": "Оценить кандидата по критериям",
|
||||
"TECHNOLOGY_STACK": "Стек технологий",
|
||||
"ALREADY_EXISTED": "Уже существует",
|
||||
"TOASTR_ALREADY_EXIST": "Критерий с таким именем уже существует",
|
||||
"PERSONAL_QUALITIES": "Личные качества",
|
||||
"CHOOSE_CRITERIONS": "Выберите критерии для кандидата",
|
||||
"TECHNOLOGY_PLACEHOLDER": "Если вы хотите добавить критерии из стека технологий, их нужно создать",
|
||||
@@ -1052,7 +1056,8 @@
|
||||
"ADMINISTRATION": "Администрация"
|
||||
},
|
||||
"ONLY_ADMIN": "Эти разрешения доступны только для чтения и включены только для администратора",
|
||||
"INSUFFICIENT": "У вас нет достаточных разрешений. Следующие разрешения отсутствуют:"
|
||||
"INSUFFICIENT": "У вас нет достаточных разрешений. Следующие разрешения отсутствуют:",
|
||||
"VIEW_MY_TASK": "Мои задачи"
|
||||
}
|
||||
},
|
||||
"PUBLIC_PAGE": {
|
||||
@@ -1547,7 +1552,7 @@
|
||||
"HELP": "Помогите",
|
||||
"KNOWLEDGE_BASE": "База знаний",
|
||||
"CHOSE_ICON": "Выберите значок для установки",
|
||||
"CREATE_AT": "создать в",
|
||||
"CREATED_AT": "создано в",
|
||||
"WRITTEN_BY": "написано",
|
||||
"EMPLOYEES": "сотрудники",
|
||||
"ONLY_FOR_EMPLOYEES": "Только для сотрудников",
|
||||
|
||||
@@ -8,4 +8,11 @@ export interface IHelpCenterArticle extends IBaseEntityModel {
|
||||
draft: boolean;
|
||||
privacy: boolean;
|
||||
categoryId: string;
|
||||
authors?: IHelpCenterAuthor[];
|
||||
}
|
||||
|
||||
export interface IHelpCenterAuthor extends IBaseEntityModel {
|
||||
articleId: string;
|
||||
employeeId: string;
|
||||
articles: IHelpCenterArticle[];
|
||||
}
|
||||
|
||||
@@ -67,7 +67,8 @@ export enum PermissionsEnum {
|
||||
VIEW_ALL_EMAILS = 'VIEW_ALL_EMAILS',
|
||||
ORG_HELP_CENTER_EDIT = 'ORG_HELP_CENTER_EDIT',
|
||||
EDIT_SALES_PIPELINES = 'EDIT_SALES_PIPELINES',
|
||||
CAN_APPROVE_TIMESHEET = 'CAN_APPROVE_TIMESHEET'
|
||||
CAN_APPROVE_TIMESHEET = 'CAN_APPROVE_TIMESHEET',
|
||||
VIEW_MY_TASK = 'VIEW_MY_TASK'
|
||||
}
|
||||
|
||||
export const PermissionGroups = {
|
||||
@@ -103,7 +104,8 @@ export const PermissionGroups = {
|
||||
PermissionsEnum.VIEW_ALL_EMAILS,
|
||||
PermissionsEnum.ORG_HELP_CENTER_EDIT,
|
||||
PermissionsEnum.EDIT_SALES_PIPELINES,
|
||||
PermissionsEnum.CAN_APPROVE_TIMESHEET
|
||||
PermissionsEnum.CAN_APPROVE_TIMESHEET,
|
||||
PermissionsEnum.VIEW_MY_TASK
|
||||
],
|
||||
|
||||
//Readonly permissions, are only enabled for admin role
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface TimeOff extends IBaseEntityModel {
|
||||
|
||||
export interface TimeOffFindInput extends IBaseEntityModel {
|
||||
employees?: Employee[];
|
||||
employeeId?: string;
|
||||
organizationId?: string;
|
||||
description?: string;
|
||||
policy?: TimeOffPolicy;
|
||||
|
||||
@@ -230,7 +230,7 @@ export interface ICreateScreenshotInput {
|
||||
employeeId?: string;
|
||||
fullUrl: string;
|
||||
thumbUrl?: string;
|
||||
recordedAt: Date;
|
||||
recordedAt: Date | string;
|
||||
}
|
||||
|
||||
export interface Screenshot extends IBaseEntityModel {
|
||||
|
||||
Reference in New Issue
Block a user