|
|
|
|
@@ -13,10 +13,18 @@ function getMondayOfWeek(date: Date): Date {
|
|
|
|
|
return d;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the Sunday (end of week) for a given Monday
|
|
|
|
|
function getSundayOfWeek(monday: Date): Date {
|
|
|
|
|
const d = new Date(monday);
|
|
|
|
|
d.setUTCDate(d.getUTCDate() + 6);
|
|
|
|
|
// Returns the first day of the month for a given date
|
|
|
|
|
function getFirstOfMonth(date: Date): Date {
|
|
|
|
|
const d = new Date(date);
|
|
|
|
|
d.setUTCDate(1);
|
|
|
|
|
d.setUTCHours(0, 0, 0, 0);
|
|
|
|
|
return d;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the last day of the month for a given date
|
|
|
|
|
function getLastOfMonth(date: Date): Date {
|
|
|
|
|
const d = new Date(date);
|
|
|
|
|
d.setUTCMonth(d.getUTCMonth() + 1, 0);
|
|
|
|
|
d.setUTCHours(23, 59, 59, 999);
|
|
|
|
|
return d;
|
|
|
|
|
}
|
|
|
|
|
@@ -34,13 +42,56 @@ function getWeekMondays(startDate: Date): Date[] {
|
|
|
|
|
return mondays;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface WeekBalance {
|
|
|
|
|
weekStart: string; // ISO date string (Monday)
|
|
|
|
|
weekEnd: string; // ISO date string (Sunday)
|
|
|
|
|
// Returns all month starts from startDate up to and including the current month
|
|
|
|
|
function getMonthStarts(startDate: Date): Date[] {
|
|
|
|
|
const months: Date[] = [];
|
|
|
|
|
const currentMonthStart = getFirstOfMonth(new Date());
|
|
|
|
|
let cursor = getFirstOfMonth(new Date(startDate));
|
|
|
|
|
while (cursor <= currentMonthStart) {
|
|
|
|
|
months.push(new Date(cursor));
|
|
|
|
|
cursor.setUTCMonth(cursor.getUTCMonth() + 1);
|
|
|
|
|
}
|
|
|
|
|
return months;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get all working days in a period that fall on the given weekdays
|
|
|
|
|
function getWorkingDaysInPeriod(
|
|
|
|
|
periodStart: Date,
|
|
|
|
|
periodEnd: Date,
|
|
|
|
|
workDays: number[]
|
|
|
|
|
): Date[] {
|
|
|
|
|
const workingDays: Date[] = [];
|
|
|
|
|
const current = new Date(periodStart);
|
|
|
|
|
current.setUTCHours(0, 0, 0, 0);
|
|
|
|
|
|
|
|
|
|
while (current <= periodEnd) {
|
|
|
|
|
const dayOfWeek = current.getUTCDay() === 0 ? 7 : current.getUTCDay(); // Convert to ISO (1=Mon, 7=Sun)
|
|
|
|
|
if (workDays.includes(dayOfWeek)) {
|
|
|
|
|
workingDays.push(new Date(current));
|
|
|
|
|
}
|
|
|
|
|
current.setUTCDate(current.getUTCDate() + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return workingDays;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get working days in a period up to and including today
|
|
|
|
|
function getWorkingDaysUpToToday(
|
|
|
|
|
periodStart: Date,
|
|
|
|
|
periodEnd: Date,
|
|
|
|
|
workDays: number[]
|
|
|
|
|
): Date[] {
|
|
|
|
|
const today = new Date();
|
|
|
|
|
today.setUTCHours(0, 0, 0, 0);
|
|
|
|
|
const endDate = periodEnd < today ? periodEnd : today;
|
|
|
|
|
return getWorkingDaysInPeriod(periodStart, endDate, workDays);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface DayBalance {
|
|
|
|
|
date: string; // YYYY-MM-DD
|
|
|
|
|
trackedSeconds: number;
|
|
|
|
|
targetSeconds: number;
|
|
|
|
|
correctionHours: number;
|
|
|
|
|
balanceSeconds: number; // positive = overtime, negative = undertime
|
|
|
|
|
balanceSeconds: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ClientTargetWithBalance {
|
|
|
|
|
@@ -48,7 +99,9 @@ export interface ClientTargetWithBalance {
|
|
|
|
|
clientId: string;
|
|
|
|
|
clientName: string;
|
|
|
|
|
userId: string;
|
|
|
|
|
weeklyHours: number;
|
|
|
|
|
periodType: 'weekly' | 'monthly';
|
|
|
|
|
targetHours: number;
|
|
|
|
|
workDays: number[];
|
|
|
|
|
startDate: string;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
updatedAt: string;
|
|
|
|
|
@@ -59,10 +112,10 @@ export interface ClientTargetWithBalance {
|
|
|
|
|
description: string | null;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
}>;
|
|
|
|
|
totalBalanceSeconds: number; // running total across all weeks
|
|
|
|
|
currentWeekTrackedSeconds: number;
|
|
|
|
|
currentWeekTargetSeconds: number;
|
|
|
|
|
weeks: WeekBalance[];
|
|
|
|
|
totalBalanceSeconds: number;
|
|
|
|
|
currentPeriodTrackedSeconds: number;
|
|
|
|
|
currentPeriodTargetSeconds: number;
|
|
|
|
|
days: DayBalance[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class ClientTargetService {
|
|
|
|
|
@@ -90,11 +143,30 @@ export class ClientTargetService {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async create(userId: string, data: CreateClientTargetInput): Promise<ClientTargetWithBalance> {
|
|
|
|
|
// Validate startDate is a Monday
|
|
|
|
|
// Validate startDate format
|
|
|
|
|
const startDate = new Date(data.startDate + 'T00:00:00Z');
|
|
|
|
|
const dayOfWeek = startDate.getUTCDay();
|
|
|
|
|
if (dayOfWeek !== 1) {
|
|
|
|
|
throw new BadRequestError('startDate must be a Monday');
|
|
|
|
|
if (isNaN(startDate.getTime())) {
|
|
|
|
|
throw new BadRequestError('Invalid startDate format');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate startDate based on periodType
|
|
|
|
|
if (data.periodType === 'weekly') {
|
|
|
|
|
const dayOfWeek = startDate.getUTCDay();
|
|
|
|
|
if (dayOfWeek !== 1) {
|
|
|
|
|
throw new BadRequestError('For weekly targets, startDate must be a Monday');
|
|
|
|
|
}
|
|
|
|
|
} else if (data.periodType === 'monthly') {
|
|
|
|
|
if (startDate.getUTCDate() !== 1) {
|
|
|
|
|
throw new BadRequestError('For monthly targets, startDate must be the 1st of a month');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate workDays
|
|
|
|
|
if (!data.workDays || data.workDays.length === 0) {
|
|
|
|
|
throw new BadRequestError('At least one working day must be selected');
|
|
|
|
|
}
|
|
|
|
|
if (new Set(data.workDays).size !== data.workDays.length) {
|
|
|
|
|
throw new BadRequestError('workDays must not contain duplicates');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure the client belongs to this user and is not soft-deleted
|
|
|
|
|
@@ -110,7 +182,13 @@ export class ClientTargetService {
|
|
|
|
|
// Reactivate the soft-deleted target with the new settings
|
|
|
|
|
const reactivated = await prisma.clientTarget.update({
|
|
|
|
|
where: { id: existing.id },
|
|
|
|
|
data: { deletedAt: null, weeklyHours: data.weeklyHours, startDate },
|
|
|
|
|
data: {
|
|
|
|
|
deletedAt: null,
|
|
|
|
|
targetHours: data.targetHours,
|
|
|
|
|
periodType: data.periodType,
|
|
|
|
|
workDays: data.workDays,
|
|
|
|
|
startDate,
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
client: { select: { id: true, name: true } },
|
|
|
|
|
corrections: { where: { deletedAt: null }, orderBy: { date: 'asc' } },
|
|
|
|
|
@@ -125,7 +203,9 @@ export class ClientTargetService {
|
|
|
|
|
data: {
|
|
|
|
|
userId,
|
|
|
|
|
clientId: data.clientId,
|
|
|
|
|
weeklyHours: data.weeklyHours,
|
|
|
|
|
targetHours: data.targetHours,
|
|
|
|
|
periodType: data.periodType,
|
|
|
|
|
workDays: data.workDays,
|
|
|
|
|
startDate,
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
@@ -141,16 +221,46 @@ export class ClientTargetService {
|
|
|
|
|
const existing = await this.findById(id, userId);
|
|
|
|
|
if (!existing) throw new NotFoundError('Client target not found');
|
|
|
|
|
|
|
|
|
|
const updateData: { weeklyHours?: number; startDate?: Date } = {};
|
|
|
|
|
const updateData: {
|
|
|
|
|
targetHours?: number;
|
|
|
|
|
periodType?: string;
|
|
|
|
|
workDays?: number[];
|
|
|
|
|
startDate?: Date;
|
|
|
|
|
} = {};
|
|
|
|
|
|
|
|
|
|
if (data.weeklyHours !== undefined) {
|
|
|
|
|
updateData.weeklyHours = data.weeklyHours;
|
|
|
|
|
if (data.targetHours !== undefined) {
|
|
|
|
|
updateData.targetHours = data.targetHours;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.periodType !== undefined) {
|
|
|
|
|
updateData.periodType = data.periodType;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.workDays !== undefined) {
|
|
|
|
|
if (data.workDays.length === 0) {
|
|
|
|
|
throw new BadRequestError('At least one working day must be selected');
|
|
|
|
|
}
|
|
|
|
|
if (new Set(data.workDays).size !== data.workDays.length) {
|
|
|
|
|
throw new BadRequestError('workDays must not contain duplicates');
|
|
|
|
|
}
|
|
|
|
|
updateData.workDays = data.workDays;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.startDate !== undefined) {
|
|
|
|
|
const startDate = new Date(data.startDate + 'T00:00:00Z');
|
|
|
|
|
if (startDate.getUTCDay() !== 1) {
|
|
|
|
|
throw new BadRequestError('startDate must be a Monday');
|
|
|
|
|
if (isNaN(startDate.getTime())) {
|
|
|
|
|
throw new BadRequestError('Invalid startDate format');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const periodType = data.periodType ?? existing.periodType;
|
|
|
|
|
if (periodType === 'weekly') {
|
|
|
|
|
if (startDate.getUTCDay() !== 1) {
|
|
|
|
|
throw new BadRequestError('For weekly targets, startDate must be a Monday');
|
|
|
|
|
}
|
|
|
|
|
} else if (periodType === 'monthly') {
|
|
|
|
|
if (startDate.getUTCDate() !== 1) {
|
|
|
|
|
throw new BadRequestError('For monthly targets, startDate must be the 1st of a month');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
updateData.startDate = startDate;
|
|
|
|
|
}
|
|
|
|
|
@@ -217,89 +327,154 @@ export class ClientTargetService {
|
|
|
|
|
id: string;
|
|
|
|
|
clientId: string;
|
|
|
|
|
userId: string;
|
|
|
|
|
weeklyHours: number;
|
|
|
|
|
targetHours: number;
|
|
|
|
|
periodType: string;
|
|
|
|
|
workDays: number[];
|
|
|
|
|
startDate: Date;
|
|
|
|
|
createdAt: Date;
|
|
|
|
|
updatedAt: Date;
|
|
|
|
|
client: { id: string; name: string };
|
|
|
|
|
corrections: Array<{ id: string; date: Date; hours: number; description: string | null; createdAt: Date }>;
|
|
|
|
|
}): Promise<ClientTargetWithBalance> {
|
|
|
|
|
const mondays = getWeekMondays(target.startDate);
|
|
|
|
|
const startDateObj = new Date(target.startDate);
|
|
|
|
|
startDateObj.setUTCHours(0, 0, 0, 0);
|
|
|
|
|
|
|
|
|
|
if (mondays.length === 0) {
|
|
|
|
|
// Get all periods (weeks or months) from startDate to now
|
|
|
|
|
const periods: Array<{ start: Date; end: Date }> = [];
|
|
|
|
|
if (target.periodType === 'weekly') {
|
|
|
|
|
const mondays = getWeekMondays(startDateObj);
|
|
|
|
|
for (const monday of mondays) {
|
|
|
|
|
const sunday = new Date(monday);
|
|
|
|
|
sunday.setUTCDate(sunday.getUTCDate() + 6);
|
|
|
|
|
sunday.setUTCHours(23, 59, 59, 999);
|
|
|
|
|
periods.push({ start: monday, end: sunday });
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const months = getMonthStarts(startDateObj);
|
|
|
|
|
for (const monthStart of months) {
|
|
|
|
|
const monthEnd = getLastOfMonth(monthStart);
|
|
|
|
|
periods.push({ start: monthStart, end: monthEnd });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (periods.length === 0) {
|
|
|
|
|
return this.emptyBalance(target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch all tracked time for this user on this client's projects in one query
|
|
|
|
|
// covering startDate to end of current week
|
|
|
|
|
const periodStart = mondays[0];
|
|
|
|
|
const periodEnd = getSundayOfWeek(mondays[mondays.length - 1]);
|
|
|
|
|
// Fetch all tracked time for this user on this client's projects
|
|
|
|
|
const periodStart = periods[0].start;
|
|
|
|
|
const periodEnd = periods[periods.length - 1].end;
|
|
|
|
|
|
|
|
|
|
type TrackedRow = { week_start: Date; tracked_seconds: bigint };
|
|
|
|
|
type TrackedRow = { date: string; tracked_seconds: bigint };
|
|
|
|
|
|
|
|
|
|
const rows = await prisma.$queryRaw<TrackedRow[]>(Prisma.sql`
|
|
|
|
|
SELECT
|
|
|
|
|
DATE_TRUNC('week', te.start_time AT TIME ZONE 'UTC') AS week_start,
|
|
|
|
|
DATE(te.start_time AT TIME ZONE 'UTC') AS date,
|
|
|
|
|
COALESCE(SUM(EXTRACT(EPOCH FROM (te.end_time - te.start_time)) - (te.break_minutes * 60)), 0)::bigint AS tracked_seconds
|
|
|
|
|
FROM time_entries te
|
|
|
|
|
JOIN projects p ON p.id = te.project_id
|
|
|
|
|
WHERE te.user_id = ${target.userId}
|
|
|
|
|
AND p.client_id = ${target.clientId}
|
|
|
|
|
AND te.start_time >= ${periodStart}
|
|
|
|
|
AND te.start_time <= ${periodEnd}
|
|
|
|
|
AND DATE(te.start_time AT TIME ZONE 'UTC') >= ${periodStart}::date
|
|
|
|
|
AND DATE(te.start_time AT TIME ZONE 'UTC') <= ${periodEnd}::date
|
|
|
|
|
AND te.deleted_at IS NULL
|
|
|
|
|
AND p.deleted_at IS NULL
|
|
|
|
|
GROUP BY DATE_TRUNC('week', te.start_time AT TIME ZONE 'UTC')
|
|
|
|
|
GROUP BY DATE(te.start_time AT TIME ZONE 'UTC')
|
|
|
|
|
ORDER BY date ASC
|
|
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
// Index tracked seconds by week start (ISO Monday string)
|
|
|
|
|
const trackedByWeek = new Map<string, number>();
|
|
|
|
|
// Index tracked seconds by date (ISO string)
|
|
|
|
|
const trackedByDate = new Map<string, number>();
|
|
|
|
|
for (const row of rows) {
|
|
|
|
|
// DATE_TRUNC with 'week' gives Monday in Postgres (ISO week)
|
|
|
|
|
const monday = getMondayOfWeek(new Date(row.week_start));
|
|
|
|
|
const key = monday.toISOString().split('T')[0];
|
|
|
|
|
trackedByWeek.set(key, Number(row.tracked_seconds));
|
|
|
|
|
trackedByDate.set(row.date, Number(row.tracked_seconds));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Index corrections by week
|
|
|
|
|
const correctionsByWeek = new Map<string, number>();
|
|
|
|
|
// Index corrections by period
|
|
|
|
|
const correctionsByPeriod = new Map<number, number>(); // period index -> total hours
|
|
|
|
|
for (const c of target.corrections) {
|
|
|
|
|
const monday = getMondayOfWeek(new Date(c.date));
|
|
|
|
|
const key = monday.toISOString().split('T')[0];
|
|
|
|
|
correctionsByWeek.set(key, (correctionsByWeek.get(key) ?? 0) + c.hours);
|
|
|
|
|
const correctionDate = new Date(c.date);
|
|
|
|
|
correctionDate.setUTCHours(0, 0, 0, 0);
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < periods.length; i++) {
|
|
|
|
|
if (correctionDate >= periods[i].start && correctionDate <= periods[i].end) {
|
|
|
|
|
correctionsByPeriod.set(i, (correctionsByPeriod.get(i) ?? 0) + c.hours);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const targetSecondsPerWeek = target.weeklyHours * 3600;
|
|
|
|
|
const weeks: WeekBalance[] = [];
|
|
|
|
|
// Compute daily balances
|
|
|
|
|
const days: DayBalance[] = [];
|
|
|
|
|
let totalBalanceSeconds = 0;
|
|
|
|
|
let currentPeriodTrackedSeconds = 0;
|
|
|
|
|
let currentPeriodTargetSeconds = 0;
|
|
|
|
|
|
|
|
|
|
for (const monday of mondays) {
|
|
|
|
|
const key = monday.toISOString().split('T')[0];
|
|
|
|
|
const sunday = getSundayOfWeek(monday);
|
|
|
|
|
const trackedSeconds = trackedByWeek.get(key) ?? 0;
|
|
|
|
|
const correctionHours = correctionsByWeek.get(key) ?? 0;
|
|
|
|
|
const effectiveTargetSeconds = targetSecondsPerWeek - correctionHours * 3600;
|
|
|
|
|
const balanceSeconds = trackedSeconds - effectiveTargetSeconds;
|
|
|
|
|
totalBalanceSeconds += balanceSeconds;
|
|
|
|
|
const today = new Date();
|
|
|
|
|
today.setUTCHours(0, 0, 0, 0);
|
|
|
|
|
|
|
|
|
|
weeks.push({
|
|
|
|
|
weekStart: key,
|
|
|
|
|
weekEnd: sunday.toISOString().split('T')[0],
|
|
|
|
|
trackedSeconds,
|
|
|
|
|
targetSeconds: effectiveTargetSeconds,
|
|
|
|
|
correctionHours,
|
|
|
|
|
balanceSeconds,
|
|
|
|
|
});
|
|
|
|
|
for (let periodIdx = 0; periodIdx < periods.length; periodIdx++) {
|
|
|
|
|
const period = periods[periodIdx];
|
|
|
|
|
|
|
|
|
|
// Get working days for this period up to today
|
|
|
|
|
const workingDaysInPeriod = getWorkingDaysUpToToday(
|
|
|
|
|
period.start,
|
|
|
|
|
period.end,
|
|
|
|
|
target.workDays
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (workingDaysInPeriod.length === 0) {
|
|
|
|
|
continue; // No working days in this period up to today
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate effective target for this period (after corrections)
|
|
|
|
|
const correctionHours = correctionsByPeriod.get(periodIdx) ?? 0;
|
|
|
|
|
const effectiveTargetSeconds = (target.targetHours * 3600) - (correctionHours * 3600);
|
|
|
|
|
const targetSecondsPerDay = effectiveTargetSeconds / workingDaysInPeriod.length;
|
|
|
|
|
|
|
|
|
|
// Is this the current period?
|
|
|
|
|
const isCurrentPeriod = periodIdx === periods.length - 1;
|
|
|
|
|
if (isCurrentPeriod) {
|
|
|
|
|
// For full month/week target, count all working days in the period (for display)
|
|
|
|
|
const allWorkingDaysInPeriod = getWorkingDaysInPeriod(period.start, period.end, target.workDays);
|
|
|
|
|
currentPeriodTargetSeconds = (target.targetHours * 3600) - (correctionHours * 3600);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process each working day
|
|
|
|
|
for (const workingDay of workingDaysInPeriod) {
|
|
|
|
|
const dateStr = workingDay.toISOString().split('T')[0];
|
|
|
|
|
const trackedSeconds = trackedByDate.get(dateStr) ?? 0;
|
|
|
|
|
const balanceSeconds = trackedSeconds - targetSecondsPerDay;
|
|
|
|
|
|
|
|
|
|
days.push({
|
|
|
|
|
date: dateStr,
|
|
|
|
|
trackedSeconds,
|
|
|
|
|
targetSeconds: targetSecondsPerDay,
|
|
|
|
|
balanceSeconds,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
totalBalanceSeconds += balanceSeconds;
|
|
|
|
|
|
|
|
|
|
if (isCurrentPeriod) {
|
|
|
|
|
currentPeriodTrackedSeconds += trackedSeconds;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentWeek = weeks[weeks.length - 1];
|
|
|
|
|
// If no days were added but we have periods, use defaults
|
|
|
|
|
if (days.length === 0) {
|
|
|
|
|
const lastPeriod = periods[periods.length - 1];
|
|
|
|
|
const correctionHours = correctionsByPeriod.get(periods.length - 1) ?? 0;
|
|
|
|
|
currentPeriodTargetSeconds = (target.targetHours * 3600) - (correctionHours * 3600);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: target.id,
|
|
|
|
|
clientId: target.clientId,
|
|
|
|
|
clientName: target.client.name,
|
|
|
|
|
userId: target.userId,
|
|
|
|
|
weeklyHours: target.weeklyHours,
|
|
|
|
|
periodType: target.periodType as 'weekly' | 'monthly',
|
|
|
|
|
targetHours: target.targetHours,
|
|
|
|
|
workDays: target.workDays,
|
|
|
|
|
startDate: target.startDate.toISOString().split('T')[0],
|
|
|
|
|
createdAt: target.createdAt.toISOString(),
|
|
|
|
|
updatedAt: target.updatedAt.toISOString(),
|
|
|
|
|
@@ -311,9 +486,9 @@ export class ClientTargetService {
|
|
|
|
|
createdAt: c.createdAt.toISOString(),
|
|
|
|
|
})),
|
|
|
|
|
totalBalanceSeconds,
|
|
|
|
|
currentWeekTrackedSeconds: currentWeek?.trackedSeconds ?? 0,
|
|
|
|
|
currentWeekTargetSeconds: currentWeek?.targetSeconds ?? targetSecondsPerWeek,
|
|
|
|
|
weeks,
|
|
|
|
|
currentPeriodTrackedSeconds,
|
|
|
|
|
currentPeriodTargetSeconds,
|
|
|
|
|
days,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -321,7 +496,9 @@ export class ClientTargetService {
|
|
|
|
|
id: string;
|
|
|
|
|
clientId: string;
|
|
|
|
|
userId: string;
|
|
|
|
|
weeklyHours: number;
|
|
|
|
|
targetHours: number;
|
|
|
|
|
periodType: string;
|
|
|
|
|
workDays: number[];
|
|
|
|
|
startDate: Date;
|
|
|
|
|
createdAt: Date;
|
|
|
|
|
updatedAt: Date;
|
|
|
|
|
@@ -333,15 +510,17 @@ export class ClientTargetService {
|
|
|
|
|
clientId: target.clientId,
|
|
|
|
|
clientName: target.client.name,
|
|
|
|
|
userId: target.userId,
|
|
|
|
|
weeklyHours: target.weeklyHours,
|
|
|
|
|
periodType: target.periodType as 'weekly' | 'monthly',
|
|
|
|
|
targetHours: target.targetHours,
|
|
|
|
|
workDays: target.workDays,
|
|
|
|
|
startDate: target.startDate.toISOString().split('T')[0],
|
|
|
|
|
createdAt: target.createdAt.toISOString(),
|
|
|
|
|
updatedAt: target.updatedAt.toISOString(),
|
|
|
|
|
corrections: [],
|
|
|
|
|
totalBalanceSeconds: 0,
|
|
|
|
|
currentWeekTrackedSeconds: 0,
|
|
|
|
|
currentWeekTargetSeconds: target.weeklyHours * 3600,
|
|
|
|
|
weeks: [],
|
|
|
|
|
currentPeriodTrackedSeconds: 0,
|
|
|
|
|
currentPeriodTargetSeconds: target.targetHours * 3600,
|
|
|
|
|
days: [],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|