Merge branch 'develop' of https://github.com/ever-co/gauzy into feature#1363-sales-deals-list

This commit is contained in:
Salathiel Genese
2020-07-09 04:08:56 +04:00
76 changed files with 1137 additions and 330 deletions
+1
View File
@@ -40,3 +40,4 @@ Thumbs.db
/apps/api/src/app/integrations/upwork/csv
.env
/.deploy/nginx/log
+11 -5
View File
@@ -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,
+3 -1
View File
@@ -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
+21 -2
View File
@@ -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);
+2 -5
View File
@@ -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
]
},
{
+82
View File
@@ -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);
}
};
+2 -2
View File
@@ -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;
}
}
+6 -2
View File
@@ -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 {}
+92 -6
View File
@@ -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:
+3 -2
View File
@@ -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);
}
@@ -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"
@@ -53,3 +53,15 @@
.button {
height: 40px;
}
.nameValidParent {
position: relative;
.nameValid {
position: absolute;
top: 66px;
left: 0px;
font-size: 11px;
color: #ff3d71;
}
}
@@ -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
@@ -45,7 +45,9 @@
</div>
<ga-candidate-interview-form
[interviewList]="interviewList"
#candidateInterviewForm
(titleExist)="titleExist()"
[isCalendar]="isCalendar"
[interviewers]="editData?.interviewers"
></ga-candidate-interview-form>
@@ -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
@@ -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'];
}
@@ -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)'
@@ -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;
}
@@ -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
}
}
);
@@ -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"
@@ -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;
}
}
@@ -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')
);
}
}
@@ -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"
@@ -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;
}
}
@@ -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(
@@ -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
+2 -2
View File
@@ -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]
}
},
{
@@ -48,7 +48,6 @@
permissionChanged(permission, $event)
"
labelPosition="start"
[disabled]="selectedRole === superAdminRole"
status="basic"
>{{
'ORGANIZATIONS_PAGE.PERMISSIONS.' +
@@ -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;
}
@@ -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'))
}
}
+9 -4
View File
@@ -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",
+30 -25
View File
@@ -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"
}
}
+9 -4
View File
@@ -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",
+9 -4
View File
@@ -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[];
}
+4 -2
View File
@@ -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
+1
View File
@@ -50,6 +50,7 @@ export interface TimeOff extends IBaseEntityModel {
export interface TimeOffFindInput extends IBaseEntityModel {
employees?: Employee[];
employeeId?: string;
organizationId?: string;
description?: string;
policy?: TimeOffPolicy;
+1 -1
View File
@@ -230,7 +230,7 @@ export interface ICreateScreenshotInput {
employeeId?: string;
fullUrl: string;
thumbUrl?: string;
recordedAt: Date;
recordedAt: Date | string;
}
export interface Screenshot extends IBaseEntityModel {