diff --git a/app/src/client/client.service.ts b/app/src/client/client.service.ts index fa72822..1373251 100644 --- a/app/src/client/client.service.ts +++ b/app/src/client/client.service.ts @@ -16,15 +16,19 @@ export class ClientService { async getClientList(userId: string, orderBy: string = 'created_at', sort: string = 'asc') { const currentTeamData: any = await this.teamService.getCurrentTeam(userId); const currentTeamId = currentTeamData.data.user_team[0].team.id; + + const { ROLE_ADMIN, ROLE_OWNER, ROLE_INVOICES_MANAGER } = this.roleCollaborationService.ROLES_IDS; + const isAdmin = - currentTeamData.data.user_team[0].role_collaboration_id === - this.roleCollaborationService.ROLES_IDS.ROLE_ADMIN; + currentTeamData.data.user_team[0].role_collaboration_id === ROLE_ADMIN; const isOwner = - currentTeamData.data.user_team[0].role_collaboration_id === - this.roleCollaborationService.ROLES_IDS.ROLE_OWNER; + currentTeamData.data.user_team[0].role_collaboration_id === ROLE_OWNER; - if (isAdmin || isOwner) { + const isInvoicesManager = + currentTeamData.data.user_team[0].role_collaboration_id === ROLE_INVOICES_MANAGER; + + if (isAdmin || isOwner || isInvoicesManager) { const query = `{ client( where: { diff --git a/app/src/client/interfaces/client.interface.ts b/app/src/client/interfaces/client.interface.ts index 40b285b..817a1dd 100644 --- a/app/src/client/interfaces/client.interface.ts +++ b/app/src/client/interfaces/client.interface.ts @@ -10,5 +10,5 @@ export interface Client { zip?: string; email?: string; avatar?: string; - companyName?: string; + company_name?: string; } diff --git a/app/src/core/core.module.ts b/app/src/core/core.module.ts index 463694e..ffe4857 100644 --- a/app/src/core/core.module.ts +++ b/app/src/core/core.module.ts @@ -20,25 +20,7 @@ import { CurrencyService } from './currency/currency.service'; }, }), ], - providers: [ - HttpRequestsService, - MailService, - EncryptionService, - JiraAuthService, - JiraService, - CurrencyService, - CrmService, - CrmAuthService, - ], - exports: [ - HttpRequestsService, - MailService, - EncryptionService, - JiraAuthService, - JiraService, - CurrencyService, - CrmService, - CrmAuthService, - ], + providers: [HttpRequestsService, MailService, EncryptionService, JiraAuthService, JiraService, CurrencyService, CrmService, CrmAuthService], + exports: [HttpRequestsService, MailService, EncryptionService, JiraAuthService, JiraService, CurrencyService, CrmService, CrmAuthService], }) export class CoreModule {} diff --git a/app/src/core/crm-auth/crm-auth.service.ts b/app/src/core/crm-auth/crm-auth.service.ts index b3b707e..f3ab342 100644 --- a/app/src/core/crm-auth/crm-auth.service.ts +++ b/app/src/core/crm-auth/crm-auth.service.ts @@ -11,8 +11,7 @@ export class CrmAuthService { async authenticate(): Promise { const URL: string = `${process.env.CRM_URL}/web/session/authenticate/`; - if ( - !Boolean(process.env.CRM_AUTH_LOGIN) || + if (!Boolean(process.env.CRM_AUTH_LOGIN) || !Boolean(process.env.CRM_AUTH_PASSWORD) || !Boolean(process.env.CRM_AUTH_DB) ) { @@ -44,7 +43,7 @@ export class CrmAuthService { return reject({ message: 'ERROR.AUTHENTICATE.CRM', }); - } + }, ); }); } diff --git a/app/src/core/crm-auth/interfaces/crm-auth.iterface.ts b/app/src/core/crm-auth/interfaces/crm-auth.iterface.ts index 8c52f26..e6beac0 100644 --- a/app/src/core/crm-auth/interfaces/crm-auth.iterface.ts +++ b/app/src/core/crm-auth/interfaces/crm-auth.iterface.ts @@ -6,3 +6,5 @@ export interface IAuthCrm { db: string; }; } + + diff --git a/app/src/core/http-requests/http-requests.service.ts b/app/src/core/http-requests/http-requests.service.ts index 9df6903..1e0bdc9 100644 --- a/app/src/core/http-requests/http-requests.service.ts +++ b/app/src/core/http-requests/http-requests.service.ts @@ -62,11 +62,12 @@ export class HttpRequestsService { .post(url, data, { headers: { 'Content-Type': 'application/json', - Cookie: cookies, + 'Cookie': cookies, }, }) .pipe(response => { return response; }); } + } diff --git a/app/src/core/sync/crm/crm.service.ts b/app/src/core/sync/crm/crm.service.ts index 53b42b6..a054597 100644 --- a/app/src/core/sync/crm/crm.service.ts +++ b/app/src/core/sync/crm/crm.service.ts @@ -10,7 +10,7 @@ import { ISyncMail } from './interfaces/sync-mail.iterface'; export class CrmService { constructor( private readonly httpRequestsService: HttpRequestsService, - private readonly crmAuthService: CrmAuthService + private readonly crmAuthService: CrmAuthService, ) {} async addUserEmailToCRM(email: string): Promise { @@ -29,9 +29,8 @@ export class CrmService { return Promise.reject(error); } - const sessionIdCookies: string = authResponseCRM.headers['set-cookie'].find(header => - header.startsWith('session_id=') - ); + const sessionIdCookies: string = authResponseCRM.headers['set-cookie'] + .find(header => header.startsWith('session_id=')); const emailData: ISyncMail = { params: { @@ -55,7 +54,7 @@ export class CrmService { return reject({ message: 'ERROR.ADD.EMAIL.CRM', }); - } + }, ); }); } diff --git a/app/src/core/sync/crm/interfaces/sync-mail.iterface.ts b/app/src/core/sync/crm/interfaces/sync-mail.iterface.ts index bf46720..fcac12a 100644 --- a/app/src/core/sync/crm/interfaces/sync-mail.iterface.ts +++ b/app/src/core/sync/crm/interfaces/sync-mail.iterface.ts @@ -5,3 +5,4 @@ export interface ISyncMail { }; }; } + diff --git a/app/src/invoice/interfaces/invoice.interface.ts b/app/src/invoice/interfaces/invoice.interface.ts index 876c786..d33c810 100644 --- a/app/src/invoice/interfaces/invoice.interface.ts +++ b/app/src/invoice/interfaces/invoice.interface.ts @@ -34,4 +34,5 @@ export interface Invoice { overdue?: boolean; to?: Client; projects?: InvoiceProject[]; + reference?: string; } diff --git a/app/src/invoice/invoice.controller.ts b/app/src/invoice/invoice.controller.ts index f0e53ff..86b916f 100644 --- a/app/src/invoice/invoice.controller.ts +++ b/app/src/invoice/invoice.controller.ts @@ -70,6 +70,7 @@ export class InvoiceController { originalLogo?: string; invoiceNumber?: string; discount?: number; + reference?: string; }, @UploadedFile() file ) { @@ -129,6 +130,7 @@ export class InvoiceController { timezoneOffset: body.timezoneOffset, logo: file && file.path ? file.path : newFileLogo ? newFileLogo : null, discount: body.discount, + reference: body.reference, }; const invoice = await this.invoiceService.createInvoice(invoiceRequest); @@ -248,6 +250,7 @@ export class InvoiceController { invoiceNumber: string; timezoneOffset: number; discount?: number; + reference?: string; }, @UploadedFile() file ) { @@ -299,6 +302,7 @@ export class InvoiceController { timezoneOffset: body.timezoneOffset, logo: file && file.path ? file.path : body.removeFile ? '' : null, discount: body.discount, + reference: body.reference, }; const invoiceData = { @@ -316,6 +320,7 @@ export class InvoiceController { timezoneOffset: invoice.timezone_offset, logo: invoice.logo, discount: invoice.discount, + reference: invoice.reference, }; Object.keys(invoiceData).forEach(prop => { diff --git a/app/src/invoice/invoice.service.ts b/app/src/invoice/invoice.service.ts index b0248d2..bc47754 100644 --- a/app/src/invoice/invoice.service.ts +++ b/app/src/invoice/invoice.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { AxiosResponse, AxiosError } from 'axios'; +import { AxiosError, AxiosResponse } from 'axios'; import { HttpRequestsService } from '../core/http-requests/http-requests.service'; import { TeamService } from '../team/team.service'; @@ -8,6 +8,7 @@ import PdfPrinter from 'pdfmake'; import { Invoice } from './interfaces/invoice.interface'; import moment, { Moment } from 'moment'; import { CurrencyService } from '../core/currency/currency.service'; +import {RoleCollaborationService} from "../role-collaboration/role-collaboration.service"; @Injectable() export class InvoiceService { @@ -15,21 +16,22 @@ export class InvoiceService { private readonly httpRequestsService: HttpRequestsService, private readonly teamService: TeamService, private readonly userService: UserService, - private readonly currencyService: CurrencyService + private readonly currencyService: CurrencyService, + private readonly roleCollaborationService: RoleCollaborationService, ) {} async createPdfDocument(invoice: Invoice) { const user: any = await this.userService.getUserById(invoice.user_id); const defaultLanguage = 'en'; - let language = user.language ? user.language : defaultLanguage; + const language = user.language ? user.language : defaultLanguage; const languageVariables = { en: { from: 'From', to: 'To', invoiceNumber: 'Invoice No: ', - date: 'Invoice Date:', - dueDate: 'Due:', - project: 'Project', + date: 'Issued:', + dueDate: 'Due Date:', + description: 'Description', hours: 'QTY', tax: 'Tax', rate: 'Rate', @@ -40,6 +42,7 @@ export class InvoiceService { summaryTax: 'Tax', summaryTotal: `Total (${invoice.currency})`, comments: 'Comments', + reference: 'Reference:', }, ru: { from: 'От', @@ -47,7 +50,7 @@ export class InvoiceService { invoiceNumber: 'Номер счета: ', date: 'Дата счета:', dueDate: 'Дата платежа:', - project: 'Проект', + description: 'Описание', hours: 'Кол-во', tax: 'Налог', rate: 'Ставка', @@ -58,6 +61,7 @@ export class InvoiceService { summaryTax: 'Налог', summaryTotal: `Всего (${invoice.currency})`, comments: 'Комментарии', + reference: 'Пометки:', }, uk: { from: 'Від', @@ -65,7 +69,7 @@ export class InvoiceService { invoiceNumber: 'Номер Рахунку: ', date: 'Дата рахунку:', dueDate: 'Дата платежу:', - project: 'Проект', + description: 'Опис', hours: 'К-сть', tax: 'Податок', rate: 'Ставка', @@ -76,6 +80,7 @@ export class InvoiceService { summaryTax: 'Податок', summaryTotal: `Всього (${invoice.currency})`, comments: 'Коментарі', + reference: 'Вiдмiтки:', }, it: { from: 'A partire dal', @@ -83,7 +88,7 @@ export class InvoiceService { invoiceNumber: 'Numero di fattura No: ', date: 'Data Fattura:', dueDate: 'Dovuto:', - project: 'Progetto', + description: 'Descrizione', hours: 'QTY', tax: 'Imposta', rate: 'Vota', @@ -94,6 +99,7 @@ export class InvoiceService { summaryTax: 'Imposta', summaryTotal: `Totale (${invoice.currency})`, comments: 'Commenti', + reference: 'Riferimento:', }, de: { from: 'Von', @@ -101,7 +107,7 @@ export class InvoiceService { invoiceNumber: 'Rechnung Nr: ', date: 'Rechnungsdatum:', dueDate: 'Fällig:', - project: 'Projekt', + description: 'Beschreibung', hours: 'QTY', tax: 'MwSt', rate: 'Bewertung', @@ -112,10 +118,11 @@ export class InvoiceService { summaryTax: 'MwSt', summaryTotal: `Gesamt(${invoice.currency})`, comments: 'Bemerkungen', + reference: 'Referenz:', }, }; - let docDefinitionLanguage = languageVariables[language] || languageVariables[defaultLanguage]; + const docDefinitionLanguage = languageVariables[language] || languageVariables[defaultLanguage]; const imageObj: any = { image: `${invoice.logo}`, @@ -135,34 +142,32 @@ export class InvoiceService { text: `${docDefinitionLanguage.to.toUpperCase()}`, }, ], - margin: [0, 10, 0, 8], + margin: [0, 10, 100, 8], }, - { style: 'text', columns: [ { - text: `${invoice.invoice_vendor.username ? invoice.invoice_vendor.username : ' '}`, + text: `${invoice.invoice_vendor.company_name ? invoice.invoice_vendor.company_name : ' '}`, }, { - text: `${invoice.to.name ? invoice.to.name : ' '}`, + text: `${invoice.to.company_name ? invoice.to.company_name : ' '}`, }, ], - margin: [0, 6, 0, 6], + margin: [0, 6, 100, 6], }, { style: 'text', columns: [ { - text: `${invoice.invoice_vendor.city ? invoice.invoice_vendor.city : ' '}`, + text: `${invoice.invoice_vendor.username ? invoice.invoice_vendor.username : ' '}`, }, { - text: `${invoice.to.city ? invoice.to.city : ' '}`, + text: `${invoice.to.name ? invoice.to.name : ' '}`, }, ], - margin: [0, 6, 0, 6], + margin: [0, 6, 100, 6], }, - { style: 'text', columns: [ @@ -173,7 +178,7 @@ export class InvoiceService { text: `${invoice.to.email ? invoice.to.email : ' '}`, }, ], - margin: [0, 6, 0, 0], + margin: [0, 6, 100, 0], }, { style: 'text', @@ -185,7 +190,33 @@ export class InvoiceService { text: `${invoice.to.phone ? invoice.to.phone : ' '}`, }, ], - margin: [0, 6, 0, 0], + margin: [0, 6, 100, 0], + }, + { + style: 'text', + columns: [ + { + text: `${[ + invoice.invoice_vendor.country, + invoice.invoice_vendor.state, + invoice.invoice_vendor.city, + invoice.invoice_vendor.zip, + ] + .filter(n => n) + .join(', ')} `, + }, + { + text: `${[ + invoice.to.country, + invoice.to.state, + invoice.to.city, + invoice.to.zip, + ] + .filter(n => n) + .join(', ')}`, + }, + ], + margin: [0, 6, 100, 6], }, { style: 'dateHeaders', @@ -194,10 +225,10 @@ export class InvoiceService { text: `${docDefinitionLanguage.invoiceNumber} ${invoice.invoice_number}`, }, { - text: `${docDefinitionLanguage.dueDate} ${moment(invoice.due_date).format('MMM Do, YYYY')}`, + text: `${docDefinitionLanguage.reference} ${invoice.reference || ` - `}`, }, ], - margin: [0, 25, 0, 0], + margin: [0, 25, 100, 0], }, { style: 'dateHeaders', @@ -207,8 +238,11 @@ export class InvoiceService { 'MMM Do, YYYY' )}`, }, + { + text: `${docDefinitionLanguage.dueDate} ${moment(invoice.due_date).format('MMM Do, YYYY')}`, + }, ], - margin: [0, 10, 0, 10], + margin: [0, 10, 100, 10], }, { style: 'tableMain', @@ -219,7 +253,7 @@ export class InvoiceService { body: [ [ ``, - `${docDefinitionLanguage.project}`, + `${docDefinitionLanguage.description}`, `${docDefinitionLanguage.hours}`, `${docDefinitionLanguage.rate}`, `${docDefinitionLanguage.tax}`, @@ -257,7 +291,12 @@ export class InvoiceService { text: `${docDefinitionLanguage.discount}`, }, { - text: `${invoice.discount} %`, + text: `${invoice.discount} % (${ + this.currencyService.getFormattedValue( + invoice.sub_total * invoice.discount / 100, + invoice.currency, + ) + })`, }, ], margin: [300, 10, 0, 0], @@ -349,7 +388,7 @@ export class InvoiceService { }, }; - let orderNumber: number = 8; + let orderNumber: number = 9; if (!invoice.comment) { const newContent = docDefinition.content.filter(item => item.label !== 'comment'); @@ -358,7 +397,7 @@ export class InvoiceService { if (invoice.logo) { docDefinition.content.unshift(imageObj); - orderNumber = 9; + orderNumber = 10; } for (let i = 0; i < invoice.projects.length; i++) { @@ -369,7 +408,7 @@ export class InvoiceService { tax = '-'; } - let projectOnIteration = { + const projectOnIteration = { style: 'tableProject', fillColor: '#ffffff', layout: 'noBorders', @@ -393,7 +432,7 @@ export class InvoiceService { docDefinition.content.splice(orderNumber + i, 0, projectOnIteration); } - let fonts = { + const fonts = { Roboto: { normal: 'fonts/Roboto-Regular.ttf', bold: 'fonts/Roboto-Medium.ttf', @@ -547,6 +586,7 @@ export class InvoiceService { invoiceNumber, timezoneOffset, discount, + reference, }): Promise { const { total, sumSubTotal, sumTaxTotal, projects } = this.getInvoiceProjectTotals(invoiceProjects, discount); const currentTeamData: any = await this.teamService.getCurrentTeam(userId); @@ -590,16 +630,18 @@ export class InvoiceService { }, }, invoiceComment: comment ? comment : '', + invoiceNumber: invoiceNumberValue ? invoiceNumberValue : '', + invoiceReference: reference ? reference : '', }; const query = `mutation - addVendorData($invoice_vendor: invoice_vendor_obj_rel_insert_input, $invoiceComment: String) { + addVendorData($invoice_vendor: invoice_vendor_obj_rel_insert_input, $invoiceComment: String, $invoiceNumber: String, $invoiceReference: String) { insert_invoice( objects: { status: "${invoiceStatus}" overdue : ${overdueStatus} timezone_offset: "${timezoneOffset}" - invoice_number: "${invoiceNumberValue}" + invoice_number: $invoiceNumber vendor_id: "${vendorId}" user_id: "${userId}" team_id: "${currentTeamId}" @@ -614,7 +656,9 @@ export class InvoiceService { discount: ${discount ? discount : 0} sub_total: ${sumSubTotal} tax_total: ${sumTaxTotal} - invoice_vendor: $invoice_vendor} + invoice_vendor: $invoice_vendor + reference: $invoiceReference + } ) { returning { id @@ -630,14 +674,7 @@ export class InvoiceService { } private async generateInvoiceNumber(teamId) { - const newInvoiceNumber = (invoiceCount: number, prevNumbers: string[] = []): string => { - const invoiceNumber: string = ('00' + (invoiceCount + 1)).slice(-3); - if (prevNumbers.indexOf(invoiceNumber) === -1) { - return invoiceNumber; - } else { - return newInvoiceNumber(invoiceCount + 1, prevNumbers); - } - }; + const newInvoiceNumber = (invoiceCount: number): string => ('00' + (invoiceCount + 1)).slice(-3); const variables = { where: { @@ -666,12 +703,7 @@ export class InvoiceService { nodes: teamInvoicePrevNumbers, } = res.data.invoice_aggregate; - return resolve( - newInvoiceNumber( - teamInvoiceCount, - teamInvoicePrevNumbers.map(invoice => invoice.invoice_number) - ) - ); + return resolve(newInvoiceNumber(teamInvoiceCount)); }, (error: AxiosError) => reject(error) ); @@ -691,17 +723,40 @@ export class InvoiceService { const currentTeamData: any = await this.teamService.getCurrentTeam(userId); const currentTeamId = currentTeamData.data.user_team[0].team.id; + const { ROLE_OWNER, ROLE_INVOICES_MANAGER } = this.roleCollaborationService.ROLES_IDS; + + const isOwner = + currentTeamData.data.user_team[0].role_collaboration_id === ROLE_OWNER; + + const isInvoicesManager = + currentTeamData.data.user_team[0].role_collaboration_id === ROLE_INVOICES_MANAGER; + + const invoiceAdmins = []; + if (isInvoicesManager || isOwner) { + const [{user_id: ownerId}] = ((await this.userService.getUserByRoleInTeam( + currentTeamId, + ROLE_OWNER, + )) as AxiosResponse).data.user_team; + + const invoiceManagers = ((await this.userService.getUserByRoleInTeam( + currentTeamId, + ROLE_INVOICES_MANAGER, + )) as AxiosResponse).data.user_team; + + invoiceAdmins.push(ownerId, ...invoiceManagers.map(({user_id}) => user_id)); + } + const variables = { where: { user_id: { - _eq: userId, + _in: invoiceAdmins, }, team_id: { _eq: currentTeamId, }, }, limit: pageSize, - offset: offset, + offset, }; if (search) { @@ -785,6 +840,7 @@ export class InvoiceService { sending_status status overdue + reference } invoicesAmount: invoice_aggregate( where: $where) { aggregate { @@ -821,10 +877,33 @@ export class InvoiceService { const currentTeamData: any = await this.teamService.getCurrentTeam(userId); const currentTeamId = currentTeamData.data.user_team[0].team.id; + const { ROLE_OWNER, ROLE_INVOICES_MANAGER } = this.roleCollaborationService.ROLES_IDS; + + const isInvoicesManager = + currentTeamData.data.user_team[0].role_collaboration_id === ROLE_INVOICES_MANAGER; + + const isOwner = + currentTeamData.data.user_team[0].role_collaboration_id === ROLE_OWNER; + + const invoiceAdmins = []; + if (isInvoicesManager || isOwner) { + const [{user_id: ownerId}] = ((await this.userService.getUserByRoleInTeam( + currentTeamId, + ROLE_OWNER, + )) as AxiosResponse).data.user_team; + + const invoiceManagers = ((await this.userService.getUserByRoleInTeam( + currentTeamId, + ROLE_INVOICES_MANAGER, + )) as AxiosResponse).data.user_team; + + invoiceAdmins.push(ownerId, ...invoiceManagers.map(({user_id}) => user_id)); + } + const variables = { where: { user_id: { - _eq: userId, + _in: invoiceAdmins, }, team_id: { _eq: currentTeamId, @@ -947,18 +1026,60 @@ export class InvoiceService { sending_status payment_status status + reference } }`; return new Promise((resolve, reject) => { this.httpRequestsService.graphql(query, variables).subscribe( - (res: AxiosResponse) => { + async (res: AxiosResponse) => { const resp = res.data.invoice; + if (!resp) { + return reject({ + message: 'ERROR.INVOICE.GET_FAILED', + }); + } + if (userId) { - if (!resp || (resp.user_id && resp.user_id !== userId)) { - return reject({ - message: 'ERROR.INVOICE.GET_FAILED', - }); + try { + const currentTeamData: any = await this.teamService.getCurrentTeam(userId); + const currentTeamId = currentTeamData.data.user_team[0].team.id; + + const { ROLE_OWNER, ROLE_INVOICES_MANAGER } = this.roleCollaborationService.ROLES_IDS; + + const isOwner = + currentTeamData.data.user_team[0].role_collaboration_id === ROLE_OWNER; + + const isInvoicesManager = + currentTeamData.data.user_team[0].role_collaboration_id === ROLE_INVOICES_MANAGER; + + const invoiceAdmins = []; + if (isInvoicesManager || isOwner) { + const [{user_id: ownerId}] = ((await this.userService.getUserByRoleInTeam( + currentTeamId, + ROLE_OWNER, + )) as AxiosResponse).data.user_team; + + const invoiceManagers = ((await this.userService.getUserByRoleInTeam( + currentTeamId, + ROLE_INVOICES_MANAGER, + )) as AxiosResponse).data.user_team; + + invoiceAdmins.push(ownerId, ...invoiceManagers.map(({user_id}) => user_id)); + } else { + return reject({ + message: 'ERROR.INVOICE.GET_FAILED', + }); + } + + if (resp.user_id && !invoiceAdmins.includes(resp.user_id)) { + return reject({ + message: 'ERROR.INVOICE.GET_FAILED', + }); + } + return resolve(resp); + } catch (error) { + return reject(error) } } return resolve(resp); @@ -973,7 +1094,13 @@ export class InvoiceService { id: string, paymentStatus: boolean ): Promise { - const invoice: Invoice = await this.getInvoice(id, userId); + let invoice: Invoice | any = null; + try { + invoice = await this.getInvoice(id, userId); + } catch (error) { + return Promise.reject(error); + } + let invoiceStatus = 'draft'; if (paymentStatus) { invoiceStatus = 'paid'; @@ -987,13 +1114,31 @@ export class InvoiceService { } } + const currentTeamData: any = await this.teamService.getCurrentTeam(userId); + const currentTeamId = currentTeamData.data.user_team[0].team.id; + + const { ROLE_OWNER, ROLE_INVOICES_MANAGER } = this.roleCollaborationService.ROLES_IDS; + + const invoiceAdmins = []; + const [{user_id: ownerId}] = ((await this.userService.getUserByRoleInTeam( + currentTeamId, + ROLE_OWNER, + )) as AxiosResponse).data.user_team; + + const invoiceManagers = ((await this.userService.getUserByRoleInTeam( + currentTeamId, + ROLE_INVOICES_MANAGER, + )) as AxiosResponse).data.user_team; + + invoiceAdmins.push(ownerId, ...invoiceManagers.map(({user_id}) => user_id)); + const variables = { where: { id: { _eq: id, }, user_id: { - _eq: userId, + _in: invoiceAdmins, }, }, set: { @@ -1152,6 +1297,7 @@ export class InvoiceService { invoiceNumber, timezoneOffset, discount, + reference, }): Promise { const { total, sumSubTotal, sumTaxTotal, projects } = this.getInvoiceProjectTotals( invoiceProjects, @@ -1181,24 +1327,57 @@ export class InvoiceService { const previousInvoiceNumber: string = invoice.invoice_number; + const { ROLE_OWNER, ROLE_INVOICES_MANAGER } = this.roleCollaborationService.ROLES_IDS; + + const currentTeamData: any = await this.teamService.getCurrentTeam(userId); + const currentTeamId = currentTeamData.data.user_team[0].team.id; + + const isOwner = + currentTeamData.data.user_team[0].role_collaboration_id === ROLE_OWNER; + + const isInvoicesManager = + currentTeamData.data.user_team[0].role_collaboration_id === ROLE_INVOICES_MANAGER; + + const invoiceAdmins = []; + if (isInvoicesManager || isOwner) { + const [{user_id: ownerId}] = ((await this.userService.getUserByRoleInTeam( + currentTeamId, + ROLE_OWNER, + )) as AxiosResponse).data.user_team; + + const invoiceManagers = ((await this.userService.getUserByRoleInTeam( + currentTeamId, + ROLE_INVOICES_MANAGER, + )) as AxiosResponse).data.user_team; + + invoiceAdmins.push(ownerId, ...invoiceManagers.map(({user_id}) => user_id)); + } + const variables = { invoiceComment: comment, + invoiceReference: reference, + invoiceNumber: invoiceNumber || previousInvoiceNumber, + invoiceAdmins, }; - const query = `mutation updateInvoice($invoiceComment: String){ + const query = `mutation updateInvoice( + $invoiceComment: String, + $invoiceReference: String, + $invoiceNumber: String, + $invoiceAdmins: [uuid!]){ update_invoice( where: { id: { _eq: "${invoiceId}" }, user_id: { - _eq: "${userId}" + _in: $invoiceAdmins } }, _set: { status: "${invoiceStatus}", timezone_offset: ${timezoneOffset}, - invoice_number: ${invoiceNumber ? '"' + invoiceNumber + '"' : '"' + previousInvoiceNumber + '"'}, + invoice_number: $invoiceNumber, client_id: "${clientId}", comment: $invoiceComment, currency: "${currency ? currency : `usd`}", @@ -1210,7 +1389,8 @@ export class InvoiceService { discount: ${discount}, sub_total: ${sumSubTotal}, tax_total: ${sumTaxTotal}, - overdue : ${overdueStatus} + overdue : ${overdueStatus}, + reference: $invoiceReference } ) { returning { @@ -1311,13 +1491,39 @@ export class InvoiceService { } async deleteInvoice(userId: string, id: string): Promise { + const currentTeamData: any = await this.teamService.getCurrentTeam(userId); + const currentTeamId = currentTeamData.data.user_team[0].team.id; + + const { ROLE_OWNER, ROLE_INVOICES_MANAGER } = this.roleCollaborationService.ROLES_IDS; + + const isOwner = + currentTeamData.data.user_team[0].role_collaboration_id === ROLE_OWNER; + + const isInvoicesManager = + currentTeamData.data.user_team[0].role_collaboration_id === ROLE_INVOICES_MANAGER; + + const invoiceAdmins = []; + if (isInvoicesManager || isOwner) { + const [{user_id: ownerId}] = ((await this.userService.getUserByRoleInTeam( + currentTeamId, + ROLE_OWNER, + )) as AxiosResponse).data.user_team; + + const invoiceManagers = ((await this.userService.getUserByRoleInTeam( + currentTeamId, + ROLE_INVOICES_MANAGER, + )) as AxiosResponse).data.user_team; + + invoiceAdmins.push(ownerId, ...invoiceManagers.map(({user_id}) => user_id)); + } + const variables = { where: { id: { _eq: id, }, user_id: { - _eq: userId, + _in: invoiceAdmins, }, }, }; diff --git a/app/src/project/project.controller.ts b/app/src/project/project.controller.ts index f0f9a56..a10a275 100644 --- a/app/src/project/project.controller.ts +++ b/app/src/project/project.controller.ts @@ -20,13 +20,17 @@ import { ProjectService, PROJECT_TYPES_TO_SYNC } from './project.service'; import { TeamService } from '../team/team.service'; import { AuthService } from '../auth/auth.service'; import { Project } from './interfaces/project.interface'; +import {RoleCollaborationService} from "../role-collaboration/role-collaboration.service"; +import {UserService} from "../user/user.service"; @Controller('project') export class ProjectController { constructor( + private readonly userService: UserService, private readonly projectService: ProjectService, private readonly teamService: TeamService, - private readonly authService: AuthService + private readonly authService: AuthService, + private readonly roleCollaborationService: RoleCollaborationService, ) {} @Get('list') @@ -70,9 +74,10 @@ export class ProjectController { return res.status(HttpStatus.FORBIDDEN).json({ message: 'ERROR.CHECK_REQUEST_PARAMS' }); } - let teamId; + let teamId = null; + let currentTeamRes = null; try { - const currentTeamRes = await this.teamService.getCurrentTeam(userId); + currentTeamRes = await this.teamService.getCurrentTeam(userId); teamId = (currentTeamRes as AxiosResponse).data.user_team[0].team.id; } catch (err) { const error: AxiosError = err; @@ -83,6 +88,23 @@ export class ProjectController { return res.status(HttpStatus.FORBIDDEN).json({ message: 'ERROR.USER.NOT_MEMBER' }); } + const isAdmin = + currentTeamRes.data.user_team[0].role_collaboration_id === + this.roleCollaborationService.ROLES_IDS.ROLE_ADMIN; + + const isOwner = + currentTeamRes.data.user_team[0].role_collaboration_id === + this.roleCollaborationService.ROLES_IDS.ROLE_OWNER; + + if (!isOwner && !isAdmin) { + try { + const user: any = await this.userService.getUserById(userId); + params.userEmails = [user.email]; + } catch (error) { + console.log(error); + } + } + try { const reportsProjectRes = await this.projectService.getReportsProject( teamId, @@ -115,9 +137,10 @@ export class ProjectController { return res.status(HttpStatus.FORBIDDEN).json({ message: 'ERROR.CHECK_REQUEST_PARAMS' }); } - let teamId; + let teamId = null; + let currentTeamRes = null; try { - const currentTeamRes = await this.teamService.getCurrentTeam(userId); + currentTeamRes = await this.teamService.getCurrentTeam(userId); teamId = (currentTeamRes as AxiosResponse).data.user_team[0].team.id; } catch (err) { const error: AxiosError = err; @@ -127,6 +150,22 @@ export class ProjectController { if (!teamId) { return res.status(HttpStatus.FORBIDDEN).json({ message: 'ERROR.USER.NOT_MEMBER' }); } + const isAdmin = + currentTeamRes.data.user_team[0].role_collaboration_id === + this.roleCollaborationService.ROLES_IDS.ROLE_ADMIN; + + const isOwner = + currentTeamRes.data.user_team[0].role_collaboration_id === + this.roleCollaborationService.ROLES_IDS.ROLE_OWNER; + + if (!isOwner && !isAdmin) { + try { + const user: any = await this.userService.getUserById(userId); + params.userEmails = [user.email]; + } catch (error) { + console.log(error); + } + } try { const reportsProjectsRes = await this.projectService.getReportsProjects( diff --git a/app/src/project/project.service.ts b/app/src/project/project.service.ts index 224bdce..52e0dd6 100644 --- a/app/src/project/project.service.ts +++ b/app/src/project/project.service.ts @@ -113,59 +113,76 @@ export class ProjectService { userEmails: string[], startDate: string, endDate: string, - taskName?: string + taskName?: string, ) { - const timerStatementArray = [ - `_or: [ - {start_datetime: {_gte: "${startDate}", _lt: "${endDate}"}}, - {end_datetime: {_gte: "${startDate}", _lt: "${endDate}"}}, - {start_datetime: {_lt: "${startDate}"}, end_datetime: {_gt: "${endDate}"}} - ]`, - ]; - - const userWhereStatement = userEmails.length - ? `user: {email: {_in: [${userEmails.map(userEmail => `"${userEmail}"`).join(',')}]}}` - : ''; - - if (userWhereStatement) { - timerStatementArray.push(userWhereStatement); - } - - const taskWhereStatement = taskName ? `title: {_ilike: "%${taskName}%"}` : ''; - - if (taskWhereStatement) { - timerStatementArray.push(taskWhereStatement); - } - - const timerStatementString = timerStatementArray.join(', '); - - const timerWhereStatement = timerStatementString - ? `(where: {${timerStatementString}}, order_by: {end_datetime: desc})` - : '(order_by: {end_datetime: desc})'; - - const query = `{ - project_v2(where: {team_id: {_eq: "${teamId}"}, name: {_eq: "${projectName}"}}) { - timer ${timerWhereStatement} { + const projectWhereStatement: any = { + team_id: { + _eq: teamId, + }, + name: { + _eq: projectName, + }, + }; + + const timerWhereStatement: any = { + _or: [ + { + start_datetime: { + _gte: startDate, + _lt: endDate, + }, + }, + { + end_datetime: { + _gte: startDate, + _lt: endDate, + }, + }, + { + start_datetime: { + _lt: startDate, + }, + end_datetime: { + _gt: endDate, + }, + }, + ], + user: userEmails.length ? + {email: { _in : userEmails }} + : null, + title: taskName ? + {_ilike : taskName.toLowerCase().trim().replace(/%/g, '\\%')} + : null, + }; + + const variables = { + projectWhere: projectWhereStatement, + timerWhere: timerWhereStatement, + }; + + const query = `query project_v2($projectWhere:project_v2_bool_exp, $timerWhere:timer_v2_bool_exp) { + project_v2:project_v2(where:$projectWhere){ + timer:timer(where:$timerWhere, order_by:{end_datetime: desc}) { issue project { name } user { + id email username } start_datetime end_datetime } + id } - } - `; + }`; return new Promise((resolve, reject) => { - this.httpRequestsService.request(query).subscribe( + this.httpRequestsService.graphql(query, variables).subscribe( (res: AxiosResponse) => { this.prepareReportsProjectData(res.data.project_v2, startDate, endDate); - return resolve(res); }, (error: AxiosError) => reject(error) @@ -180,51 +197,61 @@ export class ProjectService { startDate: string, endDate: string ) { - const projectWhereStatement = projectNames.length - ? `( - where: { - team_id: {_eq: "${teamId}"}, - name: {_in: [${projectNames.map(projectName => `"${projectName}"`).join(',')}]} - }, - order_by: {name: asc} - )` - : `( - where: { - team_id: {_eq: "${teamId}"} - }, - order_by: {name: asc} - )`; - - const userWhereStatement = userEmails.length - ? `user: {email: {_in: [${userEmails.map(userEmail => `"${userEmail}"`).join(',')}]}}` - : ''; - let dateStatement = ''; - + const projectWhereStatement: any = { + team_id: { + _eq: teamId, + }, + name: projectNames.length + ? {_in: projectNames} + : null, + }; + + let timerWhereStatement: any = null; if (startDate) { endDate = endDate ? endDate : startDate; - - dateStatement = `_or: [ - {start_datetime: {_gte: "${startDate}", _lte: "${endDate}"}}, - {end_datetime: {_gte: "${startDate}", _lte: "${endDate}"}}, - {start_datetime: {_lt: "${startDate}"}, end_datetime: {_gt: "${endDate}"}} - ]`; - } - - let timerStatementArray = []; - if (userWhereStatement) { - timerStatementArray.push(userWhereStatement); - } - if (dateStatement) { - timerStatementArray.push(dateStatement); + timerWhereStatement = { + _or: [ + { + start_datetime: { + _gte: startDate, + _lte: endDate, + }, + }, + { + end_datetime: { + _gte: startDate, + _lte: endDate, + }, + }, + { + start_datetime: { + _lt: startDate, + }, + end_datetime: { + _gt: endDate, + }, + }, + ], + user: userEmails.length + ? {email: {_in: userEmails}} + : null, + }; } - const timerStatementString = timerStatementArray.join(', '); - const timerWhereStatement = timerStatementString ? `(where: {${timerStatementString}})` : ''; - - const query = `{ - project_v2 ${projectWhereStatement} { + const variables = { + projectWhere: projectWhereStatement, + timerWhere: timerWhereStatement, + }; + + const query = `query project_v2($projectWhere:project_v2_bool_exp, $timerWhere:timer_v2_bool_exp){ + project_v2 ( + where: $projectWhere, + order_by: {name: asc} + ) { id name - timer ${timerWhereStatement} { + timer ( + where: $timerWhere + ) { user { id email @@ -237,13 +264,12 @@ export class ProjectService { }`; return new Promise((resolve, reject) => { - this.httpRequestsService.request(query).subscribe( + this.httpRequestsService.graphql(query, variables).subscribe( (res: AxiosResponse) => { for (let i = 0; i < res.data.project_v2.length; i++) { const project = res.data.project_v2[i]; this.timerService.limitTimeEntriesByStartEndDates(project.timer, startDate, endDate); } - return resolve(res); }, (error: AxiosError) => reject(error) @@ -280,38 +306,32 @@ export class ProjectService { } } - let clientQueryParam = ''; - if (isAdmin || isOwner) { - if (clientId) { - clientQueryParam = `client_id: "${clientId}"`; - } else if (clientId === null) { - clientQueryParam = `client_id: null`; - } - } - - const query = `mutation { - insert_project_v2( - objects: [ - { - name: "${name}", - slug: "${slug}", - project_color_id: "${projectColorId}", - team_id: "${currentTeamId}" - ${clientQueryParam} - jira_project_id: ${jiraProjectId} + const query = `mutation insert_project_v2($objects:[project_v2_insert_input!]!){ + insert_project_v2:insert_project_v2( + objects: $objects + ){ + returning { + id } - ] - ){ - returning { - id } - } - } - `; + }`; + + const variables: any = { + objects: [ + { + name, + slug, + project_color_id: projectColorId, + team_id: currentTeamId, + client_id: isAdmin || isOwner ? clientId ? clientId : null : null, + jira_project_id: jiraProjectId, + } + ], + }; return new Promise((resolve, reject) => { this.httpRequestsService - .request(query) + .graphql(query, variables) .subscribe((res: AxiosResponse) => resolve(res), (error: AxiosError) => reject(error)); }); } @@ -365,16 +385,6 @@ export class ProjectService { currentTeamData.data.user_team[0].role_collaboration_id === this.roleCollaborationService.ROLES_IDS.ROLE_OWNER; - let clientQueryParam = ''; - - if (isAdmin || isOwner) { - if (clientId) { - clientQueryParam = `client_id: "${clientId}"`; - } else if (clientId === null) { - clientQueryParam = `client_id: null`; - } - } - if (jiraProjectId) { const projectList: any = await this.getProjectListWithJiraProject(userId, 'jira'); const filteredProjects = projectList.data.project_v2.filter( @@ -393,26 +403,44 @@ export class ProjectService { role = this.roleCollaborationService.ROLES_IDS.ROLE_OWNER; } - const query = `mutation { - update_project_v2( - where: {id: {_eq: "${id}"}, team: {team_users: {user_id: {_eq: "${userId}"}, role_collaboration_id: {_eq: "${role}"}}}}, - _set: { - name: "${name}", - slug: "${slug}", - project_color_id: "${projectColorId}" - ${clientQueryParam} - jira_project_id: ${jiraProjectId} - } + const query = `mutation update_project_v2($where: project_v2_bool_exp!,$_set: project_v2_set_input){ + update_project_v2:update_project_v2( + where:$where, + _set: $_set ) { - returning { - id + returning { + id + } } - } - } - `; + }`; + + const variables: any = { + where: { + id: { + _eq: id, + }, + team: { + team_users: { + user_id: { + _eq: userId, + }, + role_collaboration_id: { + _eq: role, + }, + }, + }, + }, + _set: { + name, + slug, + project_color_id: projectColorId, + client_id: isAdmin || isOwner ? clientId ? clientId : null : null, + jira_project_id: jiraProjectId, + }, + }; return new Promise((resolve, reject) => { - this.httpRequestsService.request(query).subscribe( + this.httpRequestsService.graphql(query, variables).subscribe( (res: AxiosResponse) => { if (!res.data.update_project_v2.returning[0]) { return reject({ @@ -452,7 +480,7 @@ export class ProjectService { const timeEntry = projectV2.timer[j]; const { issue, start_datetime: startDatetime, end_datetime: endDatetime, project, user } = timeEntry; const { name: projectName } = project; - const { email: userEmail, username } = user; + const { email: userEmail, username, id } = user; const uniqueTimeEntryKey = `${issue}-${projectName}-${userEmail}-${j}`; const previousDuration = projectV2Report[uniqueTimeEntryKey] @@ -464,6 +492,8 @@ export class ProjectService { projectV2Report[uniqueTimeEntryKey] = { user: { username, + email: userEmail, + id, }, issue, durationTimestamp: previousDuration + currentDuration, diff --git a/app/src/report/report.controller.ts b/app/src/report/report.controller.ts index 0020a7b..a6a023e 100644 --- a/app/src/report/report.controller.ts +++ b/app/src/report/report.controller.ts @@ -25,7 +25,17 @@ export class ReportController { @Get('export') @UseGuards(AuthGuard()) - async reportExport(@Headers() headers: any, @Response() res: any, @Query() params) { + async reportExport( + @Headers() headers: any, + @Response() res: any, + @Query() params: { + startDate?: string, + endDate?: string, + userEmails?: string[], + projectNames?: string[], + timezoneOffset?: number, + detailed?: string, + }) { const userId = await this.authService.getVerifiedUserId(headers.authorization); if (!userId) { throw new UnauthorizedException(); @@ -63,7 +73,8 @@ export class ReportController { params.projectNames || [], params.startDate, params.endDate, - params.timezoneOffset + params.timezoneOffset, + params.detailed === 'true', ); return res.status(HttpStatus.OK).json(reportExportRes); diff --git a/app/src/report/report.service.ts b/app/src/report/report.service.ts index 545f8c5..c5f3fb6 100644 --- a/app/src/report/report.service.ts +++ b/app/src/report/report.service.ts @@ -21,7 +21,8 @@ export class ReportService { projectNames: string[], startDate: string, endDate: string, - timezoneOffset: number = 0 + timezoneOffset: number = 0, + detailed: boolean = true, ): Promise<{ path: string } | AxiosError> { const userWhereStatement = userEmails.length ? `user: {email: {_in: [${userEmails.map(userEmail => `"${userEmail}"`).join(',')}]}}` @@ -68,7 +69,9 @@ export class ReportService { return new Promise((resolve, reject) => { this.httpRequestsService.request(query).subscribe( (res: AxiosResponse) => { - const reportData = this.prepareReportData(res.data, startDate, endDate, timezoneOffset); + const reportData = detailed + ? this.prepareReportData(res.data, startDate, endDate, timezoneOffset) + : this.prepareGeneralReportData(res.data, startDate, endDate, timezoneOffset); const reportPath = this.generateReport(reportData, timezoneOffset); return resolve({ path: reportPath }); @@ -89,7 +92,18 @@ export class ReportService { const { name: projectName } = project; const { email: userEmail, username } = user; - const uniqueTimeEntryKey = `${issue}-${projectName}-${userEmail}`; + const decodedIssue = issue + ? decodeURI(issue).replace(/(\r\n|\n|\r)/g, '') + : ''; + + const re = /[\d]+[a-z]+(\s*)+\|+(\s*)/; // match pattern "2h | WOB-1252" when before issue located estimate from Jira + const findEstimateFromJira = decodedIssue.match(re); + + const issueWithoutEstimateFromJira = Array.isArray(findEstimateFromJira) + ? decodedIssue.replace(re, '') + : decodedIssue; + + const uniqueTimeEntryKey = `${issueWithoutEstimateFromJira}-${projectName}-${userEmail}`; const previousDuration = timerEntriesReport[uniqueTimeEntryKey] ? timerEntriesReport[uniqueTimeEntryKey]['Time'] : 0; @@ -99,14 +113,8 @@ export class ReportService { timerEntriesReport[uniqueTimeEntryKey] = { 'User name': username.replace(/,/g, ';'), 'Project name': projectName.replace(/,/g, ';'), - Issue: issue - ? '"' + - decodeURI(issue) - .replace(/(\r\n|\n|\r)/g, '') - .replace(/"/g, '""') + - '"' - : '', - Time: previousDuration + currentDuration, + 'Issue': issueWithoutEstimateFromJira, + 'Time': previousDuration + currentDuration, 'Start date': this.timeService.getTimestampByGivenValue(startDatetime), 'End date': timerEntriesReport[uniqueTimeEntryKey] ? timerEntriesReport[uniqueTimeEntryKey]['End date'] @@ -125,6 +133,52 @@ export class ReportService { return timerEntriesReportValues.reverse(); } + private prepareGeneralReportData(data: any, startDate: string, endDate: string, timezoneOffset: number): any[] { + const { timer_v2: timerV2 } = data; + const timerEntriesReport = {}; + for (let i = 0, timerV2Length = timerV2.length; i < timerV2Length; i++) { + const timerEntry = timerV2[i]; + this.timerService.limitTimeEntryByStartEndDates(timerEntry, startDate, endDate); + + const { issue, start_datetime: startDatetime, end_datetime: endDatetime, project } = timerEntry; + const { name: projectName } = project; + + const decodedIssue = issue + ? decodeURI(issue).replace(/(\r\n|\n|\r)/g, '') + : ''; + + const re = /[\d]+[a-z]+(\s*)+\|+(\s*)/; // match pattern "2h | WOB-1252" when before issue located estimate from Jira + const findEstimateFromJira = decodedIssue.match(re); + + const issueName = Array.isArray(findEstimateFromJira) + ? decodedIssue.replace(re, '').split(' ', 1).join() + : decodedIssue.split(' ', 1).join(); + + const uniqueTimeEntryKey = `${issueName}-${projectName}`; + const previousDuration = timerEntriesReport[uniqueTimeEntryKey] + ? timerEntriesReport[uniqueTimeEntryKey]['Time'] + : 0; + const currentDuration = + this.timeService.getTimestampByGivenValue(endDatetime) - + this.timeService.getTimestampByGivenValue(startDatetime); + timerEntriesReport[uniqueTimeEntryKey] = { + 'Project name': projectName.replace(/,/g, ';'), + 'Issue': issueName, + 'Time': previousDuration + currentDuration, + }; + } + + const timerEntriesReportValues = Object.values(timerEntriesReport); + timerEntriesReportValues.sort((a, b) => (a['Project name'] > b['Project name']) ? 1 : -1); + + for (let i = 0, timerEntriesReportLength = timerEntriesReportValues.length; i < timerEntriesReportLength; i++) { + const timeEntry = timerEntriesReportValues[i]; + timeEntry['Time'] = this.timeService.getTimeDurationByGivenTimestamp(timeEntry['Time']); + } + + return timerEntriesReportValues; + } + private generateReport(data: any[], timezoneOffset: number): string { const filePath = this.fileService.saveCsvFile( data, diff --git a/app/src/role-collaboration/role-collaboration.service.ts b/app/src/role-collaboration/role-collaboration.service.ts index 5593314..c8ebf58 100644 --- a/app/src/role-collaboration/role-collaboration.service.ts +++ b/app/src/role-collaboration/role-collaboration.service.ts @@ -6,12 +6,14 @@ export class RoleCollaborationService { ROLE_ADMIN: 'ROLE_ADMIN', ROLE_MEMBER: 'ROLE_MEMBER', ROLE_OWNER: 'ROLE_OWNER', + ROLE_INVOICES_MANAGER: 'INVOICES_MANAGER', }; ROLES_IDS = { ROLE_ADMIN: '00000000-0000-0000-0000-000000000000', ROLE_MEMBER: '00000000-0000-0000-0000-000000000001', ROLE_OWNER: '00000000-0000-0000-0000-000000000002', + ROLE_INVOICES_MANAGER: '00000000-0000-0000-0000-000000000003', }; constructor() {} diff --git a/app/src/team/team.service.ts b/app/src/team/team.service.ts index e6d15bc..5cfa2f1 100644 --- a/app/src/team/team.service.ts +++ b/app/src/team/team.service.ts @@ -23,12 +23,16 @@ export class TeamService { async createTeam(userId: string, teamName: string = this.DEFAULT_TEAMS.MY_TEAM) { const teamSlug = slugify(`${userId}-${teamName}`, { lower: true }); - const insertTeamQuery = `mutation { + const variables = { + teamName, + teamSlug + }; + const insertTeamQuery = `mutation addTeam($teamName: String, $teamSlug: String){ insert_team( objects: [ { - name: "${teamName}", - slug: "${teamSlug}", + name: $teamName, + slug: $teamSlug, owner_id: "${userId}" } ] @@ -40,7 +44,7 @@ export class TeamService { }`; return new Promise((resolve, reject) => { - this.httpRequestsService.request(insertTeamQuery).subscribe( + this.httpRequestsService.graphql(insertTeamQuery, variables).subscribe( (insertTeamRes: AxiosResponse) => { const returningRows = insertTeamRes.data.insert_team.returning; @@ -479,14 +483,20 @@ export class TeamService { const ownerId = teamData.team.owner_id; const newSlug = slugify(`${ownerId}-${newName}`, { lower: true }); - const renameTeamQuery = `mutation{ + + const variables = { + teamName: newName, + teamSlug: newSlug, + }; + + const renameTeamQuery = `mutation renameTeam($teamName: String, $teamSlug: String){ update_team( where: { id: { _eq: "${teamId}" } } _set: { - name: "${newName}" - slug: "${newSlug}" + name: $teamName + slug: $teamSlug } ) { returning { @@ -498,7 +508,7 @@ export class TeamService { `; this.httpRequestsService - .request(renameTeamQuery) + .graphql(renameTeamQuery, variables) .subscribe( (renameTeamRes: AxiosResponse) => resolve(renameTeamRes), (renameTeamError: AxiosError) => reject(renameTeamError) diff --git a/app/src/technology/technology.service.ts b/app/src/technology/technology.service.ts index 2baaf05..aa3a7b5 100644 --- a/app/src/technology/technology.service.ts +++ b/app/src/technology/technology.service.ts @@ -47,6 +47,7 @@ export class TechnologyService { where: { title: { _ilike: title, + }, }, }; diff --git a/app/src/timer-current-v2/timer-current-v2.service.ts b/app/src/timer-current-v2/timer-current-v2.service.ts index 7814c45..ec26ace 100644 --- a/app/src/timer-current-v2/timer-current-v2.service.ts +++ b/app/src/timer-current-v2/timer-current-v2.service.ts @@ -302,10 +302,7 @@ export class TimerCurrentV2Service { async () => { const { issue, startDatetime, user, project } = timerCurrent; const timers = []; - const endOfDayTime = this.timeService.getEndOfDayByGivenTimezoneOffset( - startDatetime, - user.timezoneOffset - ); + const endOfDayTime = this.timeService.getEndOfDayByGivenTimezoneOffset(startDatetime, user.timezoneOffset); const endDatetime = this.timeService.getISOTimeWithZeroMilliseconds(); if ( @@ -328,10 +325,7 @@ export class TimerCurrentV2Service { }; const secondDayTimer = { issue, - startDatetime: this.timeService.getStartOfDayByGivenTimezoneOffset( - endDatetime, - user.timezoneOffset - ), + startDatetime: this.timeService.getStartOfDayByGivenTimezoneOffset(endDatetime, user.timezoneOffset), endDatetime, userId: user.id, projectId: project.id, diff --git a/app/src/timer/timer.controller.ts b/app/src/timer/timer.controller.ts index 235f325..269b88e 100644 --- a/app/src/timer/timer.controller.ts +++ b/app/src/timer/timer.controller.ts @@ -20,14 +20,18 @@ import { TeamService } from '../team/team.service'; import { AuthService } from '../auth/auth.service'; import { Timer } from './interfaces/timer.interface'; import { PaymentService } from '../payment/payment.service'; +import {RoleCollaborationService} from "../role-collaboration/role-collaboration.service"; +import {UserService} from "../user/user.service"; @Controller('timer') export class TimerController { constructor( + private readonly userService: UserService, private readonly timerService: TimerService, private readonly teamService: TeamService, private readonly authService: AuthService, - private readonly paymentService: PaymentService + private readonly paymentService: PaymentService, + private readonly roleCollaborationService: RoleCollaborationService, ) {} @Get('user-list') @@ -80,10 +84,10 @@ export class TimerController { return res.status(HttpStatus.FORBIDDEN).json({ message: 'ERROR.CHECK_REQUEST_PARAMS' }); } - let currentTeam; - + let currentTeam = null; + let currentTeamRes = null; try { - const currentTeamRes = await this.teamService.getCurrentTeam(userId); + currentTeamRes = await this.teamService.getCurrentTeam(userId); currentTeam = (currentTeamRes as AxiosResponse).data.user_team[0]; } catch (err) { const error: AxiosError = err; @@ -94,6 +98,22 @@ export class TimerController { return res.status(HttpStatus.FORBIDDEN).json({ message: 'ERROR.USER.NOT_MEMBER' }); } + const isAdmin = + currentTeamRes.data.user_team[0].role_collaboration_id === + this.roleCollaborationService.ROLES_IDS.ROLE_ADMIN; + + const isOwner = + currentTeamRes.data.user_team[0].role_collaboration_id === + this.roleCollaborationService.ROLES_IDS.ROLE_OWNER; + + if (!isOwner && !isAdmin) { + try { + const user: any = await this.userService.getUserById(userId); + params.userEmails = [user.email]; + } catch (error) { + console.log(error); + } + } try { const userTimerListRes = await this.timerService.getReportsTimerList( currentTeam.team.id, diff --git a/app/src/timer/timer.module.ts b/app/src/timer/timer.module.ts index a33fdc0..6798cb6 100644 --- a/app/src/timer/timer.module.ts +++ b/app/src/timer/timer.module.ts @@ -7,7 +7,7 @@ import { TeamModule } from '../team/team.module'; import { TimerController } from './timer.controller'; import { TimerService } from './timer.service'; import { PaymentModule } from '../payment/payment.module'; -import { UserModule } from '../user/user.module'; +import {UserModule} from '../user/user.module'; @Module({ imports: [CoreModule, AuthModule, TimeModule, TeamModule, PaymentModule, UserModule], diff --git a/app/src/timer/timer.service.ts b/app/src/timer/timer.service.ts index 574dd91..790b5b0 100644 --- a/app/src/timer/timer.service.ts +++ b/app/src/timer/timer.service.ts @@ -12,7 +12,7 @@ export class TimerService { constructor( private readonly httpRequestsService: HttpRequestsService, private readonly timeService: TimeService, - private readonly userService: UserService + private readonly userService: UserService, ) {} getTimer(userId: string, jiraWorklogId?: string): Promise { @@ -274,42 +274,54 @@ export class TimerService { startDate: string, endDate: string ) { - const userWhereStatement = userEmails.length - ? `user: {email: {_in: [${userEmails.map(userEmail => `"${userEmail}"`).join(',')}]}}` - : ''; - - const projectWhereStatement = projectNames.length - ? `project: { - team_id: {_eq: "${teamId}"}, - name: {_in: [${projectNames.map(projectName => `"${projectName}"`).join(',')}]} - }` - : `project: {team_id: {_eq: "${teamId}"}}`; - - const timerStatementArray = [ - `_or: [ - {start_datetime: {_gte: "${startDate}", _lte: "${endDate}"}}, - {end_datetime: {_gte: "${startDate}", _lte: "${endDate}"}}, - {start_datetime: {_lt: "${startDate}"}, end_datetime: {_gt: "${endDate}"}} - ]`, - ]; - - if (userWhereStatement) { - timerStatementArray.push(userWhereStatement); - } - - timerStatementArray.push(projectWhereStatement); - - const where = `where: {${timerStatementArray.join(',')}}`; - const query = `{ - timer_v2(${where}, order_by: {start_datetime: asc}) { + const query = `query timer_v2($where: timer_v2_bool_exp){ + timer_v2(where: $where, + order_by: {start_datetime: asc}) { start_datetime end_datetime } }`; + const variables = { + where: { + _or: [ + { + start_datetime: { + _gte: startDate, + _lte: endDate, + }, + }, + { + end_datetime: { + _gte: startDate, + _lte: endDate, + }, + }, + { + start_datetime: { + _lt: startDate, + }, + end_datetime: { + _gt: endDate, + }, + }, + ], + user: userEmails.length + ? {email: {_in: userEmails}} + : null, + project: { + team_id: { + _eq: teamId, + }, + name: projectNames.length + ? {_in: projectNames} + : null, + }, + }, + }; return new Promise((resolve, reject) => { - this.httpRequestsService.request(query).subscribe( + this.httpRequestsService.graphql(query, variables).subscribe( (res: AxiosResponse) => { this.limitTimeEntriesByStartEndDates(res.data.timer_v2, startDate, endDate); @@ -318,7 +330,7 @@ export class TimerService { return resolve(res); }, - (error: AxiosError) => reject(error) + (error: AxiosError) => reject(error), ); }); } @@ -333,7 +345,7 @@ export class TimerService { timezoneOffset = user.timezoneOffset; } } catch (err) { - console.log(err); + console.log(err) } const setParams: { [k: string]: any } = { issue: `${issue || ''}`, diff --git a/app/src/user/user.controller.ts b/app/src/user/user.controller.ts index 9f5611d..b2fb525 100644 --- a/app/src/user/user.controller.ts +++ b/app/src/user/user.controller.ts @@ -257,7 +257,11 @@ export class UserController { if (await this.userService.compareHash(body.password, user.password)) { try { - await this.userService.updateUserTimezoneOffset(user.id, body.timezoneOffset); + const nowDate: string = new Date().toISOString(); + await Promise.all([ + this.userService.updateUserLastLogin(user.id, nowDate), + this.userService.updateUserTimezoneOffset(user.id, body.timezoneOffset), + ]); } catch (error) { console.log(error); } @@ -287,8 +291,16 @@ export class UserController { } if (userFb) { + try { + const nowDate: string = new Date().toISOString(); + await Promise.all([ + this.userService.updateUserLastLogin(userFb.id, nowDate), + this.userService.updateUserTimezoneOffset(userFb.id, body.timezoneOffset), + ]); + } catch (error) { + console.log(error); + } const token = await this.userService.signIn(userFb); - return res.status(HttpStatus.OK).json({ token }); } else { let user = null; @@ -309,8 +321,16 @@ export class UserController { socialId = await this.socialService.createSocialTable(); await this.socialService.addSocialTable(user.id, socialId); } - - await this.socialService.setSocial(socialId, 'facebook', facebookId); + try { + const nowDate: string = new Date().toISOString(); + await Promise.all([ + this.socialService.setSocial(socialId, 'facebook', facebookId), + this.userService.updateUserLastLogin(user.id, nowDate), + this.userService.updateUserTimezoneOffset(user.id, body.timezoneOffset), + ]); + } catch (error) { + console.log(error); + } const token = await this.userService.signIn(user); return res.status(HttpStatus.OK).json({ token }); @@ -586,6 +606,21 @@ export class UserController { }); try { + let userExists = false; + let userToFind = null; + try { + userExists = await this.userService.checkUserExists({ + email: this.mailService.emailStandardize(body.email), + }); + userToFind = await this.userService.getUserByEmail(body.email); + } catch (error) { + console.log(error); + } + + if (userExists === true && userToFind && userToFind.id !== user.id) { + return res.status(HttpStatus.FORBIDDEN).json({ message: 'ERROR.USER.EMAIL_EXISTS' }); + } + await this.userService.updateUser(userId, userData); let userUpdated = null; @@ -751,20 +786,21 @@ export class UserController { if (!adminTeamId) { return res.status(HttpStatus.FORBIDDEN).json({ message: 'ERROR.USER.UPDATE_USER_FAILED' }); } + const { ROLE_OWNER, ROLE_ADMIN, ROLE_MEMBER, ROLE_INVOICES_MANAGER } = this.roleCollaborationService.ROLES; // Check the user who what to update is ADMIN or OWNER and ACTIVE const checkAdminIsAdmin = - (currentUserTeamData.role_collaboration || {}).title === this.roleCollaborationService.ROLES.ROLE_ADMIN; + (currentUserTeamData.role_collaboration || {}).title === ROLE_ADMIN; const checkAdminIsActive = currentUserTeamData.is_active || false; const checkAdminIsOwner = - (currentUserTeamData.role_collaboration || {}).title === this.roleCollaborationService.ROLES.ROLE_OWNER; + (currentUserTeamData.role_collaboration || {}).title === ROLE_OWNER; if ((!checkAdminIsAdmin && !checkAdminIsOwner) || !checkAdminIsActive) { return res.status(HttpStatus.FORBIDDEN).json({ message: 'ERROR.USER.UPDATE_USER_FAILED' }); } - // Retreive all the team information of the user who will be updated + // Retrieve all the team information of the user who will be updated let userTeam = null; try { const userDataByTeamData = await this.userService.getUserDataByTeam(userId, adminTeamId); @@ -778,13 +814,15 @@ export class UserController { } const isUserAdmin = - (userTeam.role_collaboration || {}).title === this.roleCollaborationService.ROLES.ROLE_ADMIN; - const userIsActive = userTeam.is_active || false; - + (userTeam.role_collaboration || {}).title === ROLE_ADMIN; + const isUserInvoiceManager = + (userTeam.role_collaboration || {}).title === ROLE_INVOICES_MANAGER; const isUserOwner = - (userTeam.role_collaboration || {}).title === this.roleCollaborationService.ROLES.ROLE_OWNER; + (userTeam.role_collaboration || {}).title === ROLE_OWNER; + + const userIsActive = userTeam.is_active || false; - // Retreive all the user information of the user who will be updated + // Retrieve all the user information of the user who will be updated let user = null; try { user = await this.userService.getUserById(userId); @@ -796,38 +834,39 @@ export class UserController { return res.status(HttpStatus.FORBIDDEN).json({ message: 'ERROR.USER.UPDATE_USER_FAILED' }); } - let newUserData: any = { + const newUserData: any = { username: body.username, email: this.mailService.emailStandardize(body.email), technologies: body.technologies || [], - roleName: body.roleName, - isActive: body.isActive, }; - let userData = { + const userData = { username: user.username, email: this.mailService.emailStandardize(user.email), isActive: userIsActive, roleName: isUserAdmin - ? this.roleCollaborationService.ROLES.ROLE_ADMIN - : this.roleCollaborationService.ROLES.ROLE_MEMBER, + ? ROLE_ADMIN + : isUserInvoiceManager + ? ROLE_INVOICES_MANAGER + : ROLE_MEMBER, technologies: user.userTechnologies.length ? user.userTechnologies.map(el => el.technology.id) : [], }; - //OWNER can update ADMIN or MEMBER, partly himself + // OWNER can update ADMIN, MEMBER, MANAGER or partly himself if (checkAdminIsOwner && checkAdminIsActive) { - const ownerRole = this.roleCollaborationService.ROLES.ROLE_OWNER; + newUserData.isActive = body.isActive; + newUserData.roleName = body.roleName; if (isUserOwner && !(body.isActive === userData.isActive)) { - let errorMessage = !(body.isActive === userData.isActive) + const errorMessage = !(body.isActive === userData.isActive) ? 'ERROR.USER.UPDATE_TEAM_OWNER_ACTIVE_STATUS_FAILED' : 'ERROR.USER.UPDATE_USER_FAILED'; return res.status(HttpStatus.BAD_REQUEST).json({ message: errorMessage }); } - if (isUserOwner && !(body.roleName === ownerRole)) { - const errorMessage = !(body.roleName === ownerRole) + if (isUserOwner && !(body.roleName === ROLE_OWNER)) { + const errorMessage = !(body.roleName === ROLE_OWNER) ? 'ERROR.USER.UPDATE_TEAM_OWNER_ROLE_FAILED' : 'ERROR.USER.UPDATE_USER_FAILED'; @@ -836,15 +875,28 @@ export class UserController { if (isUserOwner) { newUserData.isActive = userData.isActive; - newUserData.roleName = this.roleCollaborationService.ROLES.ROLE_OWNER; + newUserData.roleName = ROLE_OWNER; } } - //ADMIN can update member, but not another ADMIN or OWNER or himself - if (checkAdminIsAdmin && checkAdminIsActive && !isUserOwner && !isUserAdmin) { - userData.roleName = this.roleCollaborationService.ROLES.ROLE_MEMBER; - newUserData.isActive = body.isActive; - newUserData.roleName = body.roleName; + // ADMIN can update only member, not another ADMIN, OWNER, MANAGER or himself + if (checkAdminIsAdmin && checkAdminIsActive) { + if (isUserInvoiceManager || body.roleName === ROLE_INVOICES_MANAGER) { + const errorMessage = body.roleName === ROLE_INVOICES_MANAGER + ? 'ERROR.USER.UPDATE_TEAM_ROLE_INVOICES_MANAGER_ROLE_FAILED' + : 'ERROR.USER.UPDATE_USER_FAILED'; + + return res.status(HttpStatus.BAD_REQUEST).json({ message: errorMessage }); + } + if (isUserAdmin) { + const errorMessage = 'ERROR.USER.UPDATE_USER_FAILED'; + return res.status(HttpStatus.BAD_REQUEST).json({ message: errorMessage }); + } + if (!isUserOwner && !isUserAdmin && !isUserInvoiceManager && body.roleName !== ROLE_INVOICES_MANAGER) { + userData.roleName = ROLE_MEMBER; + newUserData.isActive = body.isActive; + newUserData.roleName = body.roleName; + } } Object.keys(userData).forEach(prop => { diff --git a/app/src/user/user.service.ts b/app/src/user/user.service.ts index 695471f..627d5f2 100644 --- a/app/src/user/user.service.ts +++ b/app/src/user/user.service.ts @@ -53,10 +53,16 @@ export class UserService { } async getUserById(id: string): Promise { - const whereStatements = [`id: { _eq: "${id}" }`]; + const whereStatements = { + where: { + id: { + _eq: id, + }, + }, + }; return new Promise((resolve, reject) => { - this.getUserData(whereStatements.join(',')).then( + this.getUserData(whereStatements).then( (res: User) => resolve(res), (error: AxiosError) => reject(error) ); @@ -64,10 +70,16 @@ export class UserService { } async getUserByEmail(email: string): Promise { - const whereStatements = [`email: { _eq: "${email}" }`]; + const whereStatements = { + where: { + email: { + _eq: email, + }, + }, + }; return new Promise((resolve, reject) => { - this.getUserData(whereStatements.join(',')).then( + this.getUserData(whereStatements).then( (res: User) => resolve(res), (error: AxiosError) => reject(error) ); @@ -75,10 +87,18 @@ export class UserService { } async getUserBySocial(socialKey: string, socialId: string): Promise { - const whereStatements = [`social: {${socialKey}: {_eq: "${socialId}"}}`]; + const whereStatements = { + where: { + social: { + [socialKey]: { + _eq: socialId, + }, + }, + }, + }; return new Promise((resolve, reject) => { - this.getUserData(whereStatements.join(',')).then( + this.getUserData(whereStatements).then( (res: User) => resolve(res), (error: AxiosError) => reject(error) ); @@ -86,19 +106,25 @@ export class UserService { } async getUserByResetPasswordHash(token: string): Promise { - const whereStatements = [`reset_password_hash: { _eq: "${token}" }`]; + const whereStatements = { + where: { + reset_password_hash: { + _eq: token, + }, + }, + }; return new Promise((resolve, reject) => { - this.getUserData(whereStatements.join(',')).then( + this.getUserData(whereStatements).then( (res: User) => resolve(res), (error: AxiosError) => reject(error) ); }); } - async getUserData(whereStatement: string): Promise { - const query = `{ - user(where: {${whereStatement}}) { + async getUserData(whereStatement: any): Promise { + const query = `query getUser($where: user_bool_exp){ + user(where: $where) { id username email @@ -132,10 +158,14 @@ export class UserService { } `; + const variables = { + ...whereStatement, + }; + let user: any = null; return new Promise((resolve, reject) => { - this.httpRequestsService.request(query).subscribe( + this.httpRequestsService.graphql(query, variables).subscribe( (res: AxiosResponse) => { const data = res.data.user.shift(); if (data) { @@ -196,7 +226,7 @@ export class UserService { return resolve(user); }, - (error: AxiosError) => reject(error) + (error: AxiosError) => reject(error), ); }); } @@ -302,12 +332,12 @@ export class UserService { const passwordHash = await this.getHash(password); const socialId = await this.socialService.createSocialTable(); - const insertUserQuery = `mutation { + const insertUserQuery = `mutation insert_user($userName: String, $email: String){ insert_user( objects: [ { - username: "${username}" - email: "${email}", + username: $userName + email: $email, password: "${passwordHash}", social_id: "${socialId}", language: "${language || DEFAULT_LANGUAGE}", @@ -323,8 +353,13 @@ export class UserService { } `; + const variables = { + userName: username, + email, + }; + return new Promise((resolve, reject) => { - this.httpRequestsService.request(insertUserQuery).subscribe( + this.httpRequestsService.graphql(insertUserQuery, variables).subscribe( async (insertUserRes: AxiosResponse) => { const returningRows = insertUserRes.data.insert_user.returning; if (returningRows.length) { @@ -392,14 +427,27 @@ export class UserService { const tokenJiraEncrypted = this.jiraAuthService.encrypt(tokenJira); - const query = `mutation { - update_user( + const variables = { + username, + companyName, + state: state ? state : null, + city: city ? city : null, + email, + }; + const query = `mutation updateUser( + $username: String, + $companyName: String, + $state: String, + $city: String, + $email: String + ) { + update_user( where: { id: {_eq: "${userId}"} }, _set: { - username: "${username}" - email: "${email}" + username: $username + email: $email language: "${language}" token_jira: ${tokenJiraEncrypted ? '"' + tokenJiraEncrypted + '"' : null} url_jira: ${tokenJira ? (urlJira ? '"' + urlJira.replace(/\/$/, '') + '"' : null) : null} @@ -414,10 +462,10 @@ export class UserService { phone: ${phone ? '"' + phone + '"' : null}, onboarding_mobile: ${onboardingMobile === true ? true : false}, country: ${country ? '"' + country + '"' : null}, - city: ${city ? '"' + city + '"' : null}, - state: ${state ? '"' + state + '"' : null}, + city: $city, + state: $state, zip: ${zip ? '"' + zip + '"' : null}, - company_name: "${companyName}" + company_name: $companyName } ) { returning { @@ -427,7 +475,7 @@ export class UserService { }`; return new Promise(async (resolve, reject) => { - this.httpRequestsService.request(query).subscribe( + this.httpRequestsService.graphql(query, variables).subscribe( async (res: AxiosResponse) => { await this.updateUserTechnologies(userId, technologies); @@ -504,15 +552,15 @@ export class UserService { const roleId = this.roleCollaborationService.ROLES_IDS[roleName]; - const query = `mutation { + const query = `mutation updateUser($userName: String, $email: String){ update_user( where: { id: {_eq: "${userId}"}, user_teams: {team_id: {_eq: "${adminTeamId}"}} }, _set: { - username: "${username}" - email: "${email}" + username: $userName + email: $email } ) { returning { @@ -521,6 +569,11 @@ export class UserService { } }`; + const updateUserVariables = { + userName: username, + email, + }; + const updateTeamRoleQuery = `mutation{ update_user_team( where: { @@ -573,7 +626,7 @@ export class UserService { return reject(error); } - this.httpRequestsService.request(query).subscribe( + this.httpRequestsService.graphql(query, updateUserVariables).subscribe( async (res: AxiosResponse) => { await this.updateUserTechnologies(userId, technologies); @@ -801,4 +854,56 @@ export class UserService { .subscribe((res: AxiosResponse) => resolve(res), (error: AxiosError) => reject(error)); }); } + + async updateUserLastLogin(userId: string, date: string): Promise { + const query = `mutation { + update_user( + where: { + id: {_eq: "${userId}"} + }, + _set: { + last_login: "${date}" + } + ) { + returning { + id + } + } + }`; + + return new Promise(async (resolve, reject) => { + this.httpRequestsService + .request(query) + .subscribe((res: AxiosResponse) => resolve(res), (error: AxiosError) => reject(error)); + }); + } + + async getUserByRoleInTeam(teamId: string, roleId: string): Promise { + const query = `{ + user_team(where: { + is_active: { + _eq: true + }, + role_collaboration_id: { + _eq: "${roleId}" + }, + team_id: { + _eq: "${teamId}" + } + }) { + is_active + user_id + role_collaboration_id + role_collaboration { + title + } + } + }`; + + return new Promise((resolve, reject) => { + this.httpRequestsService + .request(query) + .subscribe((res: AxiosResponse) => resolve(res), (error: AxiosError) => reject(error)); + }); + } }