Skip to content

Commit

Permalink
Merge pull request #119 from bcgov/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
Josh-Williams-BC authored Apr 3, 2023
2 parents 5d972e9 + cf7914a commit de60e47
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 88 deletions.
6 changes: 5 additions & 1 deletion api/src/app/models/entities/project.entity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from 'typeorm';
import { Client } from './client.entity';
import { ProjectSector } from './projectSector.entity';
import { MOU } from './mou.entity';
import { ProjectRfx } from './projectRfx.entity';

@Entity()
export class Project {
Expand Down Expand Up @@ -84,6 +85,9 @@ export class Project {

@Column({ type: 'int', nullable: true })
categoryId: ProjectCategory

@OneToMany(() => ProjectRfx, (projectRfx)=> projectRfx.project)
projectRfxs: ProjectRfx[];
}

export enum ProjectCategory {
Expand Down
17 changes: 17 additions & 0 deletions api/src/app/routes/client/controllers/timesheet.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
retrieveForLightTimesheet,
retrieveForLightTimesheetPreview,
retrieveAllTimesheets,
retrieveAllTimesheetsByWeek,
retrieveForLightTimesheetByUser,
retrieveTimesheetByUserAndDate,
retrieveMyTimesheets
Expand Down Expand Up @@ -92,6 +93,21 @@ export const getAllTimesheets = async (ctx: Koa.Context) => {
ctx.throw(err.message);
}
};

export const getAllTimesheetsByWeek = async (ctx: Koa.Context) => {
try {
const auth = ctx.state.auth as IAuth;
if(auth.role.includes(Role.PSB_Admin)){
ctx.body = await retrieveAllTimesheetsByWeek(ctx.params.week);
}else{
ctx.body = await retrieveMyTimesheets(auth.userId)
}
} catch (err) {
ctx.throw(err.message);
}
};


export const getMyTimesheets = async (ctx: Koa.Context) => {
try {
const auth = ctx.state.auth as IAuth;
Expand Down Expand Up @@ -564,6 +580,7 @@ const router: Router = new Router(routerOpts);

// router.get('/', authorize, getTimesheets);
router.get('/all', authorize, getAllTimesheets);
router.get('/week/:week', authorize, getAllTimesheetsByWeek);
router.get('/user', authorize, getMyTimesheets);
router.get('/:id', authorize, getTimesheetbyId);
router.post('/timesheetentries', authorize, timesheetEntries);
Expand Down
172 changes: 90 additions & 82 deletions api/src/app/services/client/project.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getRepository, Repository, getConnection } from "typeorm";
import { getRepository, Repository, getConnection, createQueryBuilder, getManager } from "typeorm";
import {
Project,
ProjectContacts,
Expand Down Expand Up @@ -364,7 +364,7 @@ async function generateExportEntries(
);
}

if (timesheetEntry.expenseAmount && timesheetEntry.expenseAmount > 0) {
if (timesheetEntry.expenseAmount && timesheetEntry.expenseAmount != 0) {
let financeDetailExpense = {} as IFinanceExportDetail;
financeDetailExpense.entryDate = timesheetEntry.entryDate;
financeDetailExpense.description = timesheetEntry.expenseComment;
Expand Down Expand Up @@ -2197,35 +2197,114 @@ export const retrieveExportedPdfs = async (obj) => {
return res;
};

export const retrieveMouProjectsByUserId = async (userId: string) => {
export const retrieveNonArchivedProjectsByUserId = async (userId: string) => {
const repo = projectRepo();
const projects = await repo
.createQueryBuilder("p")
.innerJoinAndSelect("p.mou", "o")
.innerJoinAndSelect("p.client", "c")
.innerJoinAndSelect("p.projectRfxs", "pr")
.orderBy("p.projectName", "ASC")
.addOrderBy("pr.rfxName", "ASC")
.where(
'(p.is_archived IS NULL OR p.is_archived = :is_archived) AND (p."leadUserId" = :userId OR p."backupUserId" = :userId OR p.teamWideProject=true)',
'(p.is_archived IS NULL OR p.is_archived = :is_archived) AND (p.teamWideProject=true OR p."leadUserId" = :userId OR p."backupUserId" = :userId)',
{
is_archived: false,
userId,
}
)
.getMany();
return projects;
}

/**
* Sum all the expenses, revenues and timesheet hours that belongs to the project
* and that were still not locked (lock will happen during report generation).
*/
export const retreiveNonLockedTimesheetBillables = async ( projectIds: string[]) => {
const timeSheetEntryRepo = timesheetEntryRepo();
const openTimeSheetsAmount = await timeSheetEntryRepo
.createQueryBuilder("tse")
.select('ts."projectId"')
.addSelect('SUM(tse."hoursBillable") * COALESCE (c."hourlyRate", 0)', "totalBillableTime")
.addSelect('SUM(tse."revenueHours") * COALESCE (c."revenueRate", 0)',"totalRevenue")
.addSelect('SUM(tse."expenseAmount")', "totalExpenses")
.innerJoin("timesheet", "ts", 'tse."timesheetId" = ts."id"')
.innerJoin("user", "u", 'ts."userId" = u."id"')
.innerJoin("contact", "c", 'u."contactId" = c."id"')
.where('ts."is_locked" = :isLocked AND ts."projectId" IN (:...ids)' , {isLocked: false, ids: projectIds})
.groupBy('ts."projectId"')
.addGroupBy('tse."timesheetId"')
.addGroupBy('c."revenueRate"')
.addGroupBy('c."hourlyRate"')
.getRawMany();
return openTimeSheetsAmount;
}

/**
* Sum all the already billed timesheets.
* When a report is exported for a particular period, the column amountBilled is updated
* on the table timesheet. The process will use the current values for hourly rates and revenue
* rates to calculate the bill amount.
*/
export const retreiveLockedTimesheetBillables = async ( projectIds: string[]) => {
const timeSheetRepo3 = timesheetRepo();
const totalNumbersBilled = await timeSheetRepo3
.createQueryBuilder("t")
.select('t."projectId"')
.addSelect('SUM(t."amountBilled")', "totalBilled")
.where('t."is_locked" = :isLocked AND t."projectId" IN (:...ids)', {isLocked: true, ids: projectIds})
.groupBy('t."projectId"')
.orderBy('t."projectId"')
.getRawMany();
return totalNumbersBilled;
}

const getProjectIdsFromProjectList = (projectList: Project[]) => {
const projectIds = [];
projectList.forEach((project) => {
projectIds.push(project.id)
})
return projectIds;
}

const results = projects.map(async (project) => {
const [totalBillWithForecast, rfxList] = await Promise.all([
retrieveTotalBillAmountWithForecastByProject(project.id),
retrieveRFXByProjectId(project.id),
]);
const calculateProjectBillings = async (projectIds: any[]) => {
const totalNumbersToBeBilled = await retreiveNonLockedTimesheetBillables(projectIds);
const totalNumbersBilled = await retreiveLockedTimesheetBillables(projectIds);
const projectBillings = [];
projectIds.forEach(element => {
var billingSum = 0;
var ts = totalNumbersToBeBilled.find(x => x.projectId === element);
if(ts){
billingSum += ts.totalBillableTime + ts.totalRevenue + ts.totalExpenses;
}
var tsr = totalNumbersBilled.find(x => x.projectId === element);
if(tsr){
billingSum += tsr.totalBilled;
}
projectBillings.push({
projectId: element,
totalBillWithForecast: billingSum
});
});
return projectBillings;
}

export const retrieveMouProjectsByUserId = async (userId: string) => {
//console.time("AllProjects"); //console.timeEnd("AllProjects");
const projects = await retrieveNonArchivedProjectsByUserId(userId);
const projectIds = getProjectIdsFromProjectList(projects);
const projectBillings = await calculateProjectBillings(projectIds);

const results = projects.map((project) => {
return {
id: project.id,
projectName: project.projectName,
mouAmount: project.mouAmount,
mouName: project.mou.name,
mouId: project.mou.id,
totalAmountBilled: round(totalBillWithForecast),
rfxList: rfxList,
totalAmountBilled: round(projectBillings.filter(x => {return x.projectId === project.id})[0].totalBillWithForecast),
rfxList: project.projectRfxs,
isCostRecoverable:
project.categoryId === ProjectCategory.CostRecoverable ||
project.categoryId === null,
Expand All @@ -2247,78 +2326,7 @@ export const retrieveRFXByProjectId = async (id: string) => {
return res;
};

/**
* Retrieve the total bill anount for the project considering the timesheets already locked
* and calculating the forecast for the timesheets that are not locked yet.
* @param id Project ID to be retrieved.
*/
export const retrieveTotalBillAmountWithForecastByProject = async (
id: string
) => {
const [totalLocked, totalUnlocked] = await Promise.all([
retrieveTotalAmountBilledByProjectId(id),
retrieveTotalAmountToBillByProjectId(id),
]);

let totalAmountToBill = 0;
if (totalUnlocked && totalUnlocked.length) {
totalAmountToBill = totalUnlocked
.map((x) => x.totalBillableTime + x.totalRevenue + x.totalExpenses)
.reduce((prev, cur) => prev + cur);
}

return totalLocked + totalAmountToBill;
};

/**
* Sum all the already billed timesheets.
* When a report is exported for a particular period, the column amountBilled is updated
* on the table timesheet. The process will use the current values for hourly rates and revenue
* rates to calculate the bill amount.
* @param id Project ID to be retrieved.
*/
export const retrieveTotalAmountBilledByProjectId = async (id: string) => {
const repo = timesheetRepo();
const lockedAmount = await repo
.createQueryBuilder("t")
.select("SUM(t.amountBilled)")
.where('t."projectId" = :id AND t."is_locked" = :isLocked', {
id: id,
isLocked: true,
})
.getRawOne();
return lockedAmount.sum;
};

/**
* Sum all the expenses, revenues and timesheet hours that belongs to the project
* and that were still not locked (lock will happen during report generation).
* @param id Project ID to be retrieved.
*/
export const retrieveTotalAmountToBillByProjectId = async (id: string) => {
const timeSheetEntryRepo = timesheetEntryRepo();
const openTimeSheetsAmount = await timeSheetEntryRepo
.createQueryBuilder("tse")
.select('tse."timesheetId"')
.addSelect('SUM(tse."hoursBillable") * c."hourlyRate"', "totalBillableTime")
.addSelect(
'SUM(tse."revenueHours") * COALESCE (c."revenueRate", 0)',
"totalRevenue"
)
.addSelect('SUM(tse."expenseAmount")', "totalExpenses")
.innerJoin("timesheet", "ts", 'tse."timesheetId" = ts.Id')
.innerJoin("user", "u", 'ts."userId" = u.Id')
.innerJoin("contact", "c", 'u."contactId" = c.Id')
.where('ts."projectId" = :id AND ts."is_locked" = :isLocked', {
id: id,
isLocked: false,
})
.groupBy('tse."timesheetId"')
.addGroupBy('c."revenueRate"')
.addGroupBy('c."hourlyRate"')
.getRawMany();
return openTimeSheetsAmount;
};

export const retrieveAllProjects = async () => {
const repo = projectRepo();
Expand Down
19 changes: 19 additions & 0 deletions api/src/app/services/client/timesheet.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,25 @@ export const retrieveAllTimesheets = async () => {
.orderBy('t.startDate', 'DESC')
.getMany();
};
export const retrieveAllTimesheetsByWeek = async (startOfWeek) => {
const repo = timesheetRepo();
return await repo
.createQueryBuilder('t')
.innerJoinAndSelect('t.project', 'p')
.leftJoinAndSelect('t.user', 'u')
.leftJoinAndSelect('u.contact', 'c')
.innerJoin('t.timesheetEntries', 'te')
.innerJoin('t.projectRfx', 'pr')
.orderBy('t.startDate', 'DESC')
.where(' t."startDate" = :startOfWeek', {
startOfWeek,
})
/*.where(' t."startDate" = :startOfWeek AND t."endDate" = :endOfWeek', {
startOfWeek,
endOfWeek,
})*/
.getMany();
};
export const retrieveMyTimesheets = async (userId) => {
const repo = timesheetRepo();
return await repo
Expand Down
1 change: 1 addition & 0 deletions api/src/app/services/common/authorize.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const commonForPSBAdminAndUser = [
'POST/api/timesheet/batch',
'POST/api/timesheet/getLight',
'GET/api/timesheet/all',
'GET/api/timesheet/week/:week',
'GET/api/procurement/:id',
'GET/api/projectnotes/:id',
'PATCH/api/procurement/:id',
Expand Down
12 changes: 8 additions & 4 deletions web/src/components/timeMachine/timesheets/TimesheetsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,13 @@ export default {
this.$refs.spinner.open();
}
if (this.$refs.timesheetstoolbar.selectedFilter === 'All') {
await this.$store.dispatch('fetchAllTimesheets');
//await this.$store.dispatch('fetchAllTimesheets');
await this.$store.dispatch('fetchTimesheetsByWeek');
this.$store.dispatch('fetchUserTimesheets');
} else {
await this.$store.dispatch('fetchUserTimesheets');
this.$store.dispatch('fetchAllTimesheets');
//this.$store.dispatch('fetchAllTimesheets');
this.$store.dispatch('fetchTimesheetsByWeek');
}
if (this.$refs.spinner) {
Expand Down Expand Up @@ -284,12 +286,14 @@ export default {
}
},
async fetchAllTimesheets() {
await this.$store.dispatch('fetchAllTimesheets');
//await this.$store.dispatch('fetchAllTimesheets');
await this.$store.dispatch('fetchTimesheetsByWeek');
},
async initalfetch() {
await this.$store.dispatch('fetchAllProjects'); // Needed in AddTimeRecord
await this.$store.dispatch('fetchUserTimesheets');
await this.$store.dispatch('fetchAllTimesheets');
//await this.$store.dispatch('fetchAllTimesheets');
await this.$store.dispatch('fetchTimesheetsByWeek');
},
},
created() {
Expand Down
Loading

0 comments on commit de60e47

Please sign in to comment.