diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 430ea23..fe0a963 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -468,6 +468,88 @@ ] } }, + "/timesheets/{email}": { + "get": { + "operationId": "TimesheetsController_getByEmail", + "parameters": [ + { + "name": "email", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "access-token": [] + } + ], + "tags": [ + "Timesheets" + ] + } + }, + "/timesheets/shifts/{email}": { + "post": { + "operationId": "TimesheetsController_createTimesheetShifts", + "parameters": [ + { + "name": "email", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateWeekShiftsDto" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "security": [ + { + "access-token": [] + } + ], + "tags": [ + "Timesheets" + ] + } + }, "/timesheets/{id}": { "get": { "operationId": "TimesheetsController_findOne", @@ -506,53 +588,6 @@ "Timesheets" ] }, - "patch": { - "operationId": "TimesheetsController_update", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "number" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateTimesheetDto" - } - } - } - }, - "responses": { - "201": { - "description": "Timesheet updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTimesheetDto" - } - } - } - }, - "400": { - "description": "Timesheet not found" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Update timesheet", - "tags": [ - "Timesheets" - ] - }, "delete": { "operationId": "TimesheetsController_remove", "parameters": [ @@ -2370,50 +2405,13 @@ } } }, + "CreateWeekShiftsDto": { + "type": "object", + "properties": {} + }, "CreateTimesheetDto": { "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1, - "description": "timesheet`s unique ID (auto-generated)" - }, - "employee_id": { - "type": "number", - "example": 426433, - "description": "employee`s ID number of linked timsheet" - }, - "is_approved": { - "type": "boolean", - "example": true, - "description": "Timesheet`s status approval" - } - }, - "required": [ - "id", - "employee_id", - "is_approved" - ] - }, - "UpdateTimesheetDto": { - "type": "object", - "properties": { - "id": { - "type": "number", - "example": 1, - "description": "timesheet`s unique ID (auto-generated)" - }, - "employee_id": { - "type": "number", - "example": 426433, - "description": "employee`s ID number of linked timsheet" - }, - "is_approved": { - "type": "boolean", - "example": true, - "description": "Timesheet`s status approval" - } - } + "properties": {} }, "CreateExpenseDto": { "type": "object", diff --git a/prisma/migrations/20250908192545_added_is_remote_to_shifts/migration.sql b/prisma/migrations/20250908192545_added_is_remote_to_shifts/migration.sql new file mode 100644 index 0000000..50adfce --- /dev/null +++ b/prisma/migrations/20250908192545_added_is_remote_to_shifts/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "public"."shifts" ADD COLUMN "is_remote" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/mock-seeds-scripts/10-shifts.ts b/prisma/mock-seeds-scripts/10-shifts.ts index 9175030..ebe8a2b 100644 --- a/prisma/mock-seeds-scripts/10-shifts.ts +++ b/prisma/mock-seeds-scripts/10-shifts.ts @@ -4,14 +4,16 @@ const prisma = new PrismaClient(); // ====== Config ====== const PREVIOUS_WEEKS = 5; -const INCLUDE_CURRENT = false; +const INCLUDE_CURRENT = true; +const INCR = 15; // incrément ferme de 15 minutes (0.25 h) +const DAY_MIN = 5 * 60; // 5h +const DAY_MAX = 11 * 60; // 11h +const HARD_END = 19 * 60 + 30; // 19:30 -// Times-only via Date (UTC 1970-01-01) +// ====== Helpers temps ====== function timeAt(hour: number, minute: number) { return new Date(Date.UTC(1970, 0, 1, hour, minute, 0)); } - -// Lundi (UTC) de la date fournie function mondayOfThisWeekUTC(now = new Date()) { const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); const day = d.getUTCDay(); @@ -20,7 +22,6 @@ function mondayOfThisWeekUTC(now = new Date()) { d.setUTCHours(0, 0, 0, 0); return d; } - function weekDatesFromMonday(monday: Date) { return Array.from({ length: 5 }, (_, i) => { const d = new Date(monday); @@ -28,16 +29,35 @@ function weekDatesFromMonday(monday: Date) { return d; }); } - function mondayNWeeksBefore(monday: Date, n: number) { const d = new Date(monday); d.setUTCDate(d.getUTCDate() - n * 7); return d; } - function rndInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; } +function clamp(n: number, min: number, max: number) { + return Math.min(max, Math.max(min, n)); +} +function addMinutes(h: number, m: number, delta: number) { + const total = h * 60 + m + delta; + const hh = Math.floor(total / 60); + const mm = ((total % 60) + 60) % 60; + return { h: hh, m: mm }; +} +// Aligne vers le multiple de INCR le plus proche +function quantize(mins: number): number { + const q = Math.round(mins / INCR) * INCR; + return q; +} +// Tire un multiple de INCR dans [min,max] (inclus), supposés entiers minutes +function rndQuantized(min: number, max: number): number { + const qmin = Math.ceil(min / INCR); + const qmax = Math.floor(max / INCR); + const q = rndInt(qmin, qmax); + return q * INCR; +} // Helper: garantit le timesheet de la semaine (upsert) async function getOrCreateTimesheet(employee_id: number, start_date: Date) { @@ -50,8 +70,13 @@ async function getOrCreateTimesheet(employee_id: number, start_date: Date) { } async function main() { - // Bank codes utilisés + // --- Bank codes (pondérés: surtout G1 = régulier) --- const BANKS = ['G1', 'G56', 'G48', 'G700', 'G105', 'G305', 'G43'] as const; + const WEIGHTED_CODES = [ + 'G1','G1','G1','G1','G1','G1','G1','G1', // 8x régulier + 'G56','G48','G700','G105','G305','G43' + ] as const; + const bcRows = await prisma.bankCodes.findMany({ where: { bank_code: { in: BANKS as unknown as string[] } }, select: { id: true, bank_code: true }, @@ -70,59 +95,140 @@ async function main() { const mondayThisWeek = mondayOfThisWeekUTC(); const mondays: Date[] = []; if (INCLUDE_CURRENT) mondays.push(mondayThisWeek); - for (let n = 1; n <= PREVIOUS_WEEKS; n++) { - mondays.push(mondayNWeeksBefore(mondayThisWeek, n)); - } + for (let n = 1; n <= PREVIOUS_WEEKS; n++) mondays.push(mondayNWeeksBefore(mondayThisWeek, n)); let created = 0; for (let wi = 0; wi < mondays.length; wi++) { const monday = mondays[wi]; - const weekDays = weekDatesFromMonday(monday); + const days = weekDatesFromMonday(monday); for (let ei = 0; ei < employees.length; ei++) { const e = employees[ei]; - const baseStartHour = 6 + (ei % 5); - const baseStartMinute = (ei * 15) % 60; + // Cible hebdo 35–45h, multiple de 15 min + const weeklyTargetMin = rndQuantized(35 * 60, 45 * 60); - for (let di = 0; di < weekDays.length; di++) { - const date = weekDays[di]; + // Start de base (7:00, 7:15, 7:30, 7:45, 8:00, 8:15, 8:30, 8:45, 9:00 ...) + const baseStartH = 7 + (ei % 3); // 7,8,9 + const baseStartM = ( (ei * 15) % 60 ); // aligné 15 min - // 1) Trouver/Créer le timesheet de CETTE semaine pour CET employé - const weekStart = mondayOfThisWeekUTC(date); - const ts = await getOrCreateTimesheet(e.id, weekStart); + // Planification journalière (5 jours) ~8h ± 45 min, quantisée 15 min + const plannedDaily: number[] = []; + for (let d = 0; d < 5; d++) { + const jitter = rndInt(-3, 3) * INCR; // -45..+45 par pas de 15 + const base = 8 * 60 + jitter; + plannedDaily.push(quantize(clamp(base, DAY_MIN, DAY_MAX))); + } - // 2) Tirage aléatoire du bank_code - const randomCode = BANKS[Math.floor(Math.random() * BANKS.length)]; - const bank_code_id = bcMap.get(randomCode)!; + // Ajuster le 5e jour pour atteindre la cible hebdo exactement (par pas de 15) + const sumFirst4 = plannedDaily.slice(0, 4).reduce((a, b) => a + b, 0); + plannedDaily[4] = quantize(clamp(weeklyTargetMin - sumFirst4, DAY_MIN, DAY_MAX)); - // 3) Horaire - const duration = rndInt(4, 10); - const dayWeekOffset = (di + wi + (ei % 3)) % 3; - const startH = Math.min(12, baseStartHour + dayWeekOffset); - const startM = baseStartMinute; - const endH = startH + duration; - const endM = startM; + // Corriger le petit écart restant (devrait être multiple de 15) en redistribuant ±15 + let diff = weeklyTargetMin - plannedDaily.reduce((a, b) => a + b, 0); + const step = diff > 0 ? INCR : -INCR; + let guard = 100; // anti-boucle + while (diff !== 0 && guard-- > 0) { + for (let d = 0; d < 5 && diff !== 0; d++) { + const next = plannedDaily[d] + step; + if (next >= DAY_MIN && next <= DAY_MAX) { + plannedDaily[d] = next; + diff -= step; + } + } + } + // Upsert du timesheet (semaine) + const ts = await getOrCreateTimesheet(e.id, mondayOfThisWeekUTC(days[0])); + + for (let di = 0; di < 5; di++) { + const date = days[di]; + const targetWorkMin = plannedDaily[di]; // multiple de 15 + + // Départ ~ base + jitter (par pas de 15 min aussi) + const startJitter = rndInt(-1, 2) * INCR; // -15,0,+15,+30 + const { h: startH, m: startM } = addMinutes(baseStartH, baseStartM, startJitter); + + // Pause: entre 11:00 et 14:00, mais pas avant start+3h ni après start+6h (le tout quantisé 15) + const earliestLunch = Math.max((startH * 60 + startM) + 3 * 60, 11 * 60); + const latestLunch = Math.min((startH * 60 + startM) + 6 * 60, 14 * 60); + const lunchStartMin = rndQuantized(earliestLunch, latestLunch); + const lunchDur = rndQuantized(30, 120); // 30..120 min en pas de 15 + const lunchEndMin = lunchStartMin + lunchDur; + + // Travail = (lunchStart - start) + (end - lunchEnd) + const morningWork = Math.max(0, lunchStartMin - (startH * 60 + startM)); // multiple de 15 + let afternoonWork = Math.max(60, targetWorkMin - morningWork); // multiple de 15 (diff de deux multiples de 15) + if (afternoonWork % INCR !== 0) { + // sécurité (ne devrait pas arriver) + afternoonWork = quantize(afternoonWork); + } + + // Fin de journée (quantisée par construction) + const endMinRaw = lunchEndMin + afternoonWork; + const endMin = Math.min(endMinRaw, HARD_END); + + // Bank codes variés + const bcMorningCode = WEIGHTED_CODES[rndInt(0, WEIGHTED_CODES.length - 1)]; + const bcAfternoonCode = WEIGHTED_CODES[rndInt(0, WEIGHTED_CODES.length - 1)]; + const bcMorningId = bcMap.get(bcMorningCode)!; + const bcAfternoonId = bcMap.get(bcAfternoonCode)!; + + // Shift matin + const lunchStartHM = { h: Math.floor(lunchStartMin / 60), m: lunchStartMin % 60 }; await prisma.shifts.create({ data: { timesheet_id: ts.id, - bank_code_id, - description: `Shift ${di + 1} (semaine du ${monday.toISOString().slice(0, 10)}) emp ${e.id} — ${randomCode}`, + bank_code_id: bcMorningId, + description: `Matin J${di + 1} (sem ${monday.toISOString().slice(0, 10)}) emp ${e.id} — ${bcMorningCode}`, date, start_time: timeAt(startH, startM), - end_time: timeAt(endH, endM), - is_approved: Math.random() < 0.5, + end_time: timeAt(lunchStartHM.h, lunchStartHM.m), + is_approved: Math.random() < 0.6, }, }); created++; + + // Shift après-midi (si >= 30 min — sera de toute façon multiple de 15) + const pmDuration = endMin - lunchEndMin; + if (pmDuration >= 30) { + const lunchEndHM = { h: Math.floor(lunchEndMin / 60), m: lunchEndMin % 60 }; + const finalEndHM = { h: Math.floor(endMin / 60), m: endMin % 60 }; + await prisma.shifts.create({ + data: { + timesheet_id: ts.id, + bank_code_id: bcAfternoonId, + description: `Après-midi J${di + 1} (sem ${monday.toISOString().slice(0, 10)}) emp ${e.id} — ${bcAfternoonCode}`, + date, + start_time: timeAt(lunchEndHM.h, lunchEndHM.m), + end_time: timeAt(finalEndHM.h, finalEndHM.m), + is_approved: Math.random() < 0.6, + }, + }); + created++; + } else { + // Fallback très rare : un seul shift couvrant la journée (tout en multiples de 15) + const fallbackEnd = addMinutes(startH, startM, targetWorkMin + lunchDur); + await prisma.shifts.create({ + data: { + timesheet_id: ts.id, + bank_code_id: bcMap.get('G1')!, + description: `Fallback J${di + 1} (sem ${monday.toISOString().slice(0, 10)}) emp ${e.id} — G1`, + date, + start_time: timeAt(startH, startM), + end_time: timeAt(fallbackEnd.h, fallbackEnd.m), + is_approved: Math.random() < 0.6, + }, + }); + created++; + } } } } const total = await prisma.shifts.count(); - console.log(`✓ Shifts: ${created} nouvelles lignes, ${total} total rows (${INCLUDE_CURRENT ? 'courante +' : ''}${PREVIOUS_WEEKS} semaines précédentes, L→V)`); + console.log(`✓ Shifts créés: ${created} | total en DB: ${total} (${INCLUDE_CURRENT ? 'inclut semaine courante, ' : ''}${PREVIOUS_WEEKS} sem passées, L→V, 2 shifts/jour, pas de décimaux foireux})`); } main().finally(() => prisma.$disconnect()); diff --git a/prisma/mock-seeds-scripts/12-expenses.ts b/prisma/mock-seeds-scripts/12-expenses.ts index 848daae..4318871 100644 --- a/prisma/mock-seeds-scripts/12-expenses.ts +++ b/prisma/mock-seeds-scripts/12-expenses.ts @@ -2,7 +2,12 @@ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); -// Lundi (UTC) de la date fournie +// ====== Config ====== +const WEEKS_BACK = 4; // 4 semaines avant + semaine courante +const INCLUDE_CURRENT = true; // inclure la semaine courante +const STEP_CENTS = 25; // montants en quarts de dollar (.00/.25/.50/.75) + +// ====== Helpers dates ====== function mondayOfThisWeekUTC(now = new Date()) { const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); const day = d.getUTCDay(); @@ -11,10 +16,13 @@ function mondayOfThisWeekUTC(now = new Date()) { d.setUTCHours(0, 0, 0, 0); return d; } - -// Dates Lundi→Vendredi (UTC minuit) -function currentWeekDates() { - const monday = mondayOfThisWeekUTC(); +function mondayNWeeksBefore(monday: Date, n: number) { + const d = new Date(monday); + d.setUTCDate(monday.getUTCDate() - n * 7); + return d; +} +// L→V (UTC minuit) +function weekDatesMonToFri(monday: Date) { return Array.from({ length: 5 }, (_, i) => { const d = new Date(monday); d.setUTCDate(monday.getUTCDate() + i); @@ -22,15 +30,30 @@ function currentWeekDates() { }); } +// ====== Helpers random / amount ====== function rndInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; } -function rndAmount(minCents: number, maxCents: number) { - const cents = rndInt(minCents, maxCents); - return (cents / 100).toFixed(2); +// String "xx.yy" à partir de cents ENTiers (jamais de float) +function centsToAmountString(cents: number): string { + const sign = cents < 0 ? '-' : ''; + const abs = Math.abs(cents); + const dollars = Math.floor(abs / 100); + const c = abs % 100; + return `${sign}${dollars}.${c.toString().padStart(2, '0')}`; +} +// Tire un multiple de STEP_CENTS entre minCents et maxCents (inclus) +function rndQuantizedCents(minCents: number, maxCents: number, step = STEP_CENTS): number { + const qmin = Math.ceil(minCents / step); + const qmax = Math.floor(maxCents / step); + const q = rndInt(qmin, qmax); + return q * step; +} +function rndAmount(minCents: number, maxCents: number): string { + return centsToAmountString(rndQuantizedCents(minCents, maxCents)); } -// Helper: garantit le timesheet de la semaine (upsert) +// ====== Timesheet upsert ====== async function getOrCreateTimesheet(employee_id: number, start_date: Date) { return prisma.timesheets.upsert({ where: { employee_id_start_date: { employee_id, start_date } }, @@ -41,8 +64,10 @@ async function getOrCreateTimesheet(employee_id: number, start_date: Date) { } async function main() { - // Codes autorisés (aléatoires à chaque dépense) - const BANKS = ['G517', 'G56', 'G502', 'G202', 'G234'] as const; + // Codes d'EXPENSES (exemples) + const BANKS = ['G517', 'G503', 'G502', 'G202', 'G234'] as const; + + // Précharger les bank codes const bcRows = await prisma.bankCodes.findMany({ where: { bank_code: { in: BANKS as unknown as string[] } }, select: { id: true, bank_code: true }, @@ -52,58 +77,85 @@ async function main() { if (!bcMap.has(c)) throw new Error(`Bank code manquant: ${c}`); } + // Employés const employees = await prisma.employees.findMany({ select: { id: true } }); if (!employees.length) { console.warn('Aucun employé — rien à insérer.'); return; } - const weekDays = currentWeekDates(); - const monday = weekDays[0]; - const friday = weekDays[4]; + // Liste des lundis (courant + 4 précédents) + const mondayThisWeek = mondayOfThisWeekUTC(); + const mondays: Date[] = []; + if (INCLUDE_CURRENT) mondays.push(mondayThisWeek); + for (let n = 1; n <= WEEKS_BACK; n++) mondays.push(mondayNWeeksBefore(mondayThisWeek, n)); let created = 0; - for (const e of employees) { - // 1) Semaine courante → assurer le timesheet de la semaine - const weekStart = mondayOfThisWeekUTC(); - const ts = await getOrCreateTimesheet(e.id, weekStart); + for (const monday of mondays) { + const weekDays = weekDatesMonToFri(monday); + const friday = weekDays[4]; - // 2) Skip si l’employé a déjà une dépense cette semaine (on garantit ≥1) - const already = await prisma.expenses.findFirst({ - where: { timesheet_id: ts.id, date: { gte: monday, lte: friday } }, - select: { id: true }, - }); - if (already) continue; + for (const e of employees) { + // Upsert timesheet pour CETTE semaine/employee + const ts = await getOrCreateTimesheet(e.id, monday); - // 3) Choix aléatoire du code + jour - const randomCode = BANKS[Math.floor(Math.random() * BANKS.length)]; - const bank_code_id = bcMap.get(randomCode)!; - const date = weekDays[Math.floor(Math.random() * weekDays.length)]; + // Idempotence: si déjà au moins une expense L→V, on skip la semaine + const already = await prisma.expenses.findFirst({ + where: { timesheet_id: ts.id, date: { gte: monday, lte: friday } }, + select: { id: true }, + }); + if (already) continue; - // 4) Montant varié - const amount = - randomCode === 'G56' - ? rndAmount(1000, 7500) // 10.00..75.00 - : rndAmount(2000, 25000); // 20.00..250.00 + // 1 à 3 expenses (jours distincts) + const count = rndInt(1, 3); + const dayIndexes = [0, 1, 2, 3, 4].sort(() => Math.random() - 0.5).slice(0, count); - await prisma.expenses.create({ - data: { - timesheet_id: ts.id, - bank_code_id, - date, - amount, - attachement: null, - description: `Expense ${randomCode} ${amount}$ (emp ${e.id})`, - is_approved: Math.random() < 0.6, - supervisor_comment: Math.random() < 0.2 ? 'OK' : null, - }, - }); - created++; + for (const idx of dayIndexes) { + const date = weekDays[idx]; + const code = BANKS[rndInt(0, BANKS.length - 1)]; + const bank_code_id = bcMap.get(code)!; + + // Montants (cents) quantisés à 25¢ => aucun flottant binaire plus tard + let amount: string; + switch (code) { + case 'G503': // petites fournitures + amount = rndAmount(1000, 7500); // 10.00 à 75.00 + break; + case 'G502': // repas + amount = rndAmount(1500, 3000); // 15.00 à 30.00 + break; + case 'G202': // essence + amount = rndAmount(2000, 15000); // 20.00 à 150.00 + break; + case 'G234': // hébergement + amount = rndAmount(6000, 25000); // 60.00 à 250.00 + break; + case 'G517': // péages / divers + default: + amount = rndAmount(500, 5000); // 5.00 à 50.00 + break; + } + + await prisma.expenses.create({ + data: { + timesheet_id: ts.id, + bank_code_id, + date, + amount, // string "xx.yy" (2 décimales exactes) + attachement: null, + description: `Expense ${code} ${amount}$ (emp ${e.id})`, + is_approved: Math.random() < 0.65, + supervisor_comment: Math.random() < 0.25 ? 'OK' : null, + }, + }); + created++; + } + } } const total = await prisma.expenses.count(); - console.log(`✓ Expenses: ${created} nouvelles lignes, ${total} total rows (≥1 expense/employee pour la semaine courante)`); + console.log(`✓ Expenses: ${created} nouvelles lignes, ${total} total rows (sem courante + ${WEEKS_BACK} précédentes, L→V uniquement, montants en quarts de dollar)`); } main().finally(() => prisma.$disconnect()); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8d9e700..e9ef52c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -182,6 +182,7 @@ model Shifts { start_time DateTime @db.Time(0) end_time DateTime @db.Time(0) is_approved Boolean @default(false) + is_remote Boolean @default(false) archive ShiftsArchive[] @relation("ShiftsToArchive") diff --git a/src/common/utils/date-utils.ts b/src/common/utils/date-utils.ts index e383f98..5d85548 100644 --- a/src/common/utils/date-utils.ts +++ b/src/common/utils/date-utils.ts @@ -49,6 +49,13 @@ export function getYearStart(date:Date): Date { return new Date(date.getFullYear(),0,1,0,0,0,0); } +export function getCurrentWeek(): { start_date_week: Date; end_date_week: Date } { + const now = new Date(); + const start_date_week = getWeekStart(now, 0); + const end_date_week = getWeekEnd(start_date_week); + return { start_date_week, end_date_week }; +} + //cloning methods (helps with notify for overtime in a single day) // export function toDateOnly(day: Date): Date { // const d = new Date(day); diff --git a/src/modules/pay-periods/dtos/overview-employee-period.dto.ts b/src/modules/pay-periods/dtos/overview-employee-period.dto.ts index 8213be9..01119e8 100644 --- a/src/modules/pay-periods/dtos/overview-employee-period.dto.ts +++ b/src/modules/pay-periods/dtos/overview-employee-period.dto.ts @@ -41,4 +41,6 @@ export class EmployeePeriodOverviewDto { description: 'Tous les timesheets de la période sont approuvés pour cet employé', }) is_approved: boolean; + + is_remote: boolean; } diff --git a/src/modules/pay-periods/services/pay-periods-command.service.ts b/src/modules/pay-periods/services/pay-periods-command.service.ts index 8cffe2c..df9bfed 100644 --- a/src/modules/pay-periods/services/pay-periods-command.service.ts +++ b/src/modules/pay-periods/services/pay-periods-command.service.ts @@ -68,38 +68,4 @@ export class PayPeriodsCommandService { }); return {updated}; } - - //function to approve a single pay-period of a single employee (deprecated) - // async approvalPayPeriod(pay_year: number , period_no: number): Promise { - // const period = await this.prisma.payPeriods.findFirst({ - // where: { pay_year, pay_period_no: period_no}, - // }); - // if (!period) throw new NotFoundException(`PayPeriod #${pay_year}-${period_no} not found`); - - // //fetches timesheet of selected period if the timesheet has atleast 1 shift or 1 expense - // const timesheet_ist = await this.prisma.timesheets.findMany({ - // where: { - // OR: [ - // { shift: {some: { date: { gte: period.period_start, - // lte: period.period_end, - // }, - // }}, - // }, - // { expense: { some: { date: { gte: period.period_start, - // lte: period.period_end, - // }, - // }}, - // }, - // ], - // }, - // select: { id: true }, - // }); - - // //approval of both timesheet (cascading to the approval of related shifts and expenses) - // await this.prisma.$transaction(async (transaction)=> { - // for(const {id} of timesheet_ist) { - // await this.timesheets_approval.updateApprovalWithTransaction(transaction,id, true); - // } - // }) - // } } \ No newline at end of file diff --git a/src/modules/pay-periods/services/pay-periods-query.service.ts b/src/modules/pay-periods/services/pay-periods-query.service.ts index d5ac7ad..fcc4a12 100644 --- a/src/modules/pay-periods/services/pay-periods-query.service.ts +++ b/src/modules/pay-periods/services/pay-periods-query.service.ts @@ -40,6 +40,7 @@ export class PayPeriodsQueryService { } as any); } + //find crew member associated with supervisor private async resolveCrew(supervisor_id: number, include_subtree: boolean): Promise> { const result: Array<{ id: number; first_name: string; last_name: string; email: string; }> = []; @@ -69,6 +70,7 @@ export class PayPeriodsQueryService { return result; } + //fetchs crew emails async resolveCrewEmails(supervisor_id: number, include_subtree: boolean): Promise> { const crew = await this.resolveCrew(supervisor_id, include_subtree); return new Set(crew.map(crew_member => crew_member.email).filter(Boolean)); @@ -149,6 +151,7 @@ export class PayPeriodsQueryService { select: { start_time: true, end_time: true, + is_remote: true, timesheet: { select: { is_approved: true, employee: { select: { @@ -161,9 +164,7 @@ export class PayPeriodsQueryService { } }, }, }, - bank_code: { select: { - type: true, - categorie: true } }, + bank_code: { select: { categorie: true, type: true } }, }, }); @@ -186,10 +187,7 @@ export class PayPeriodsQueryService { } }, } }, } }, - bank_code: { select: { - type: true, - categorie: true, - modifier: true } }, + bank_code: { select: { categorie: true, modifier: true, type: true } }, }, }); @@ -208,6 +206,7 @@ export class PayPeriodsQueryService { expenses: 0, mileage: 0, is_approved: true, + is_remote: true, }); } } @@ -224,6 +223,7 @@ export class PayPeriodsQueryService { expenses: 0, mileage: 0, is_approved: true, + is_remote: true, }); } return by_employee.get(id)!; @@ -235,12 +235,12 @@ export class PayPeriodsQueryService { const record = ensure(employee.id, name, employee.user.email); const hours = computeHours(shift.start_time, shift.end_time); - const type = (shift.bank_code?.type).toUpperCase(); - switch (type) { + const categorie = (shift.bank_code?.type).toUpperCase(); + switch (categorie) { case "EVENING": record.evening_hours += hours; break; case "EMERGENCY": record.emergency_hours += hours; break; case "OVERTIME": record.overtime_hours += hours; break; - case "REGULAR": record.regular_hours += hours; break; + case "REGULAR" : record.regular_hours += hours; break; } record.is_approved = record.is_approved && shift.timesheet.is_approved; diff --git a/src/modules/shifts/controllers/shifts.controller.ts b/src/modules/shifts/controllers/shifts.controller.ts index e1c4292..3a33170 100644 --- a/src/modules/shifts/controllers/shifts.controller.ts +++ b/src/modules/shifts/controllers/shifts.controller.ts @@ -108,7 +108,7 @@ export class ShiftsController { r.total_overtime_hrs.toFixed(2), r.total_expenses.toFixed(2), r.total_mileage.toFixed(2), - r.is_validated, + r.is_approved, ].join(','); }).join('\n'); diff --git a/src/modules/shifts/services/shifts-query.service.ts b/src/modules/shifts/services/shifts-query.service.ts index f1bb5f7..0fb38ef 100644 --- a/src/modules/shifts/services/shifts-query.service.ts +++ b/src/modules/shifts/services/shifts-query.service.ts @@ -18,7 +18,7 @@ export interface OverviewRow { total_overtime_hrs: number; total_expenses: number; total_mileage: number; - is_validated: boolean; + is_approved: boolean; } @Injectable() @@ -168,7 +168,7 @@ export class ShiftsQueryService { total_overtime_hrs: 0, total_expenses: 0, total_mileage: 0, - is_validated: false, + is_approved: false, }; } const hours = computeHours(shift.start_time, shift.end_time); @@ -200,7 +200,7 @@ export class ShiftsQueryService { total_overtime_hrs: 0, total_expenses: 0, total_mileage: 0, - is_validated: false, + is_approved: false, }; } const amount = Number(exp.amount); diff --git a/src/modules/timesheets/controllers/timesheets.controller.ts b/src/modules/timesheets/controllers/timesheets.controller.ts index d575d9b..2dff5b4 100644 --- a/src/modules/timesheets/controllers/timesheets.controller.ts +++ b/src/modules/timesheets/controllers/timesheets.controller.ts @@ -1,14 +1,13 @@ -import { BadRequestException, Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common'; +import { BadRequestException, Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query } from '@nestjs/common'; import { TimesheetsQueryService } from '../services/timesheets-query.service'; -import { CreateTimesheetDto } from '../dtos/create-timesheet.dto'; +import { CreateTimesheetDto, CreateWeekShiftsDto } from '../dtos/create-timesheet.dto'; import { Timesheets } from '@prisma/client'; -import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto'; import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { Roles as RoleEnum } from '.prisma/client'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { TimesheetsCommandService } from '../services/timesheets-command.service'; -import { SearchTimesheetDto } from '../dtos/search-timesheet.dto'; import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto'; +import { TimesheetDto } from '../dtos/overview-timesheet.dto'; @ApiTags('Timesheets') @ApiBearerAuth('access-token') @@ -20,15 +19,6 @@ export class TimesheetsController { private readonly timesheetsCommand: TimesheetsCommandService, ) {} - // @Post() - // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) - // @ApiOperation({ summary: 'Create timesheet' }) - // @ApiResponse({ status: 201, description: 'Timesheet created', type: CreateTimesheetDto }) - // @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) - // create(@Body() dto: CreateTimesheetDto): Promise { - // return this.timesheetsQuery.create(dto); - // } - @Get() //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) async getPeriodByQuery( @@ -39,6 +29,26 @@ export class TimesheetsController { if(!email || !(email = email.trim())) throw new BadRequestException('Query param "email" is mandatory for this route.'); return this.timesheetsQuery.findAll(year, period_no, email); } + + @Get('/:email') + async getByEmail( + @Param('email') email: string, + @Query('offset') offset?: string, + ): Promise { + const week_offset = Number.isFinite(Number(offset)) ? Number(offset) : 0; + return this.timesheetsQuery.getTimesheetByEmail(email, week_offset); + } + + @Post('shifts/:email') + async createTimesheetShifts( + @Param('email') email: string, + @Body() dto: CreateWeekShiftsDto, + @Query('offset') offset?: string, + ): Promise { + const week_offset = Number.isFinite(Number(offset)) ? Number(offset) : 0; + return this.timesheetsCommand.createWeekShiftsAndReturnOverview(email, dto.shifts, week_offset); + } + @Get(':id') //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) @@ -49,18 +59,6 @@ export class TimesheetsController { return this.timesheetsQuery.findOne(id); } - @Patch(':id') - //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) - @ApiOperation({ summary: 'Update timesheet' }) - @ApiResponse({ status: 201, description: 'Timesheet updated', type: CreateTimesheetDto }) - @ApiResponse({ status: 400, description: 'Timesheet not found' }) - update( - @Param('id', ParseIntPipe) id:number, - @Body() dto: UpdateTimesheetDto, - ): Promise { - return this.timesheetsQuery.update(id, dto); - } - @Delete(':id') // @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR) @ApiOperation({ summary: 'Delete timesheet' }) diff --git a/src/modules/timesheets/dtos/create-timesheet.dto.ts b/src/modules/timesheets/dtos/create-timesheet.dto.ts index 6a1ace2..2e1c62a 100644 --- a/src/modules/timesheets/dtos/create-timesheet.dto.ts +++ b/src/modules/timesheets/dtos/create-timesheet.dto.ts @@ -1,28 +1,33 @@ -import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; -import { Allow, IsBoolean, IsInt, IsOptional } from "class-validator"; +import { IsArray, IsOptional, IsString, Length, Matches, ValidateNested } from "class-validator"; export class CreateTimesheetDto { - @ApiProperty({ - example: 1, - description: 'timesheet`s unique ID (auto-generated)', - }) - @Allow() - id?: number; - @ApiProperty({ - example: 426433, - description: 'employee`s ID number of linked timsheet', - }) - @Type(() => Number) - @IsInt() - employee_id: number; + @IsString() + @Matches(/^\d{4}-\d{2}-\d{2}$/) + date!: string; - @ApiProperty({ - example: true, - description: 'Timesheet`s status approval', - }) - @IsOptional() - @IsBoolean() - is_approved?: boolean; + @IsString() + @Length(1,64) + type!: string; + + @IsString() + @Matches(/^\d{2}:\d{2}$/) + start_time!: string; + + @IsString() + @Matches(/^\d{2}:\d{2}$/) + end_time!: string; + + @IsOptional() + @IsString() + @Length(0,512) + description?: string; +} + +export class CreateWeekShiftsDto { + @IsArray() + @ValidateNested({each:true}) + @Type(()=> CreateTimesheetDto) + shifts!: CreateTimesheetDto[]; } diff --git a/src/modules/timesheets/dtos/overview-timesheet.dto.ts b/src/modules/timesheets/dtos/overview-timesheet.dto.ts new file mode 100644 index 0000000..aaa7a95 --- /dev/null +++ b/src/modules/timesheets/dtos/overview-timesheet.dto.ts @@ -0,0 +1,28 @@ +export class TimesheetDto { + is_approved: boolean; + start_day: string; + end_day: string; + label: string; + shifts: ShiftsDto[]; + expenses: ExpensesDto[] +} + +export class ShiftsDto { + bank_type: string; + date: string; + start_time: string; + end_time: string; + description: string; + is_approved: boolean; + is_remote: boolean; +} + +export class ExpensesDto { + bank_type: string; + date: string; + amount: number; + km: number; + description: string; + supervisor_comment: string; + is_approved: boolean; +} \ No newline at end of file diff --git a/src/modules/timesheets/dtos/timesheet-period.dto.ts b/src/modules/timesheets/dtos/timesheet-period.dto.ts index d8c42a6..b948f4d 100644 --- a/src/modules/timesheets/dtos/timesheet-period.dto.ts +++ b/src/modules/timesheets/dtos/timesheet-period.dto.ts @@ -1,7 +1,10 @@ export class ShiftDto { - start: string; - end : string; + date: string; + type: string; + start_time: string; + end_time : string; is_approved: boolean; + is_remote: boolean; } export class ExpenseDto { diff --git a/src/modules/timesheets/services/timesheets-command.service.ts b/src/modules/timesheets/services/timesheets-command.service.ts index abce079..5b58fa4 100644 --- a/src/modules/timesheets/services/timesheets-command.service.ts +++ b/src/modules/timesheets/services/timesheets-command.service.ts @@ -1,16 +1,24 @@ -import { Injectable } from "@nestjs/common"; +import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { Prisma, Timesheets } from "@prisma/client"; import { BaseApprovalService } from "src/common/shared/base-approval.service"; import { PrismaService } from "src/prisma/prisma.service"; +import { TimesheetsQueryService } from "./timesheets-query.service"; +import { CreateTimesheetDto } from "../dtos/create-timesheet.dto"; +import { TimesheetDto } from "../dtos/overview-timesheet.dto"; +import { getWeekEnd, getWeekStart } from "src/common/utils/date-utils"; @Injectable() export class TimesheetsCommandService extends BaseApprovalService{ - constructor(prisma: PrismaService) {super(prisma);} + constructor( + prisma: PrismaService, + private readonly query: TimesheetsQueryService, + ) {super(prisma);} protected get delegate() { return this.prisma.timesheets; } + protected delegateFor(transaction: Prisma.TransactionClient) { return transaction.timesheets; } @@ -37,4 +45,84 @@ export class TimesheetsCommandService extends BaseApprovalService{ return timesheet; } + + //create shifts within timesheet's week - employee overview functions + private parseISODate(iso: string): Date { + const [ y, m, d ] = iso.split('-').map(Number); + return new Date(y, (m ?? 1) - 1, d ?? 1); + } + + private parseHHmm(t: string): Date { + const [ hh, mm ] = t.split(':').map(Number); + return new Date(1970, 0, 1, hh || 0, mm || 0, 0, 0); + } + + async createWeekShiftsAndReturnOverview( + email:string, + shifts: CreateTimesheetDto[], + week_offset = 0, + ): Promise { + + //match user's email with email + const user = await this.prisma.users.findUnique({ + where: { email }, + select: { id: true }, + }); + if(!user) throw new NotFoundException(`user with email ${email} not found`); + + //fetchs employee matchint user's email + const employee = await this.prisma.employees.findFirst({ + where: { user_id: user?.id }, + select: { id: true }, + }); + if(!employee) throw new NotFoundException(`employee for ${ email } not found`); + + //insure that the week starts on sunday and finishes on saturday + const base = new Date(); + base.setDate(base.getDate() + week_offset * 7); + const start_week = getWeekStart(base, 0); + const end_week = getWeekEnd(start_week); + + const timesheet = await this.prisma.timesheets.upsert({ + where: { + employee_id_start_date: { + employee_id: employee.id, + start_date: start_week, + }, + }, + create: { + employee_id: employee.id, + start_date: start_week, + is_approved: false, + }, + update: {}, + select: { id: true }, + }); + + //validations and insertions + for(const shift of shifts) { + const date = this.parseISODate(shift.date); + if (date < start_week || date > end_week) throw new BadRequestException(`date ${shift.date} not in current week`); + + const bank_code = await this.prisma.bankCodes.findFirst({ + where: { type: shift.type }, + select: { id: true }, + }); + if(!bank_code) throw new BadRequestException(`Invalid bank_code type: ${shift.type}`); + + await this.prisma.shifts.create({ + data: { + timesheet_id: timesheet.id, + bank_code_id: bank_code.id, + date: date, + start_time: this.parseHHmm(shift.start_time), + end_time: this.parseHHmm(shift.end_time), + description: shift.description ?? null, + is_approved: false, + is_remote: false, + }, + }); + } + return this.query.getTimesheetByEmail(email, week_offset); + } } \ No newline at end of file diff --git a/src/modules/timesheets/services/timesheets-query.service.ts b/src/modules/timesheets/services/timesheets-query.service.ts index c28c8fd..1849e16 100644 --- a/src/modules/timesheets/services/timesheets-query.service.ts +++ b/src/modules/timesheets/services/timesheets-query.service.ts @@ -1,13 +1,14 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from 'src/prisma/prisma.service'; -import { CreateTimesheetDto } from '../dtos/create-timesheet.dto'; import { Timesheets, TimesheetsArchive } from '@prisma/client'; import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto'; import { OvertimeService } from 'src/modules/business-logics/services/overtime.service'; -import { computeHours } from 'src/common/utils/date-utils'; +import { computeHours, formatDateISO, getCurrentWeek, getWeekEnd, getWeekStart } from 'src/common/utils/date-utils'; import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto'; import { buildPeriod, endOfDayUTC, toUTCDateOnly } from '../utils/timesheet.helpers'; import type { ShiftRow, ExpenseRow } from '../utils/timesheet.helpers'; +import { TimesheetDto } from '../dtos/overview-timesheet.dto'; +import { CreateWeekShiftsDto } from '../dtos/create-timesheet.dto'; @Injectable() @@ -17,17 +18,6 @@ export class TimesheetsQueryService { private readonly overtime: OvertimeService, ) {} - // async create(dto : CreateTimesheetDto): Promise { - // const { employee_id, is_approved } = dto; - // return this.prisma.timesheets.create({ - // data: { employee_id, is_approved: is_approved ?? false }, - // include: { - // employee: { include: { user: true } - // }, - // }, - // }); - // } - async findAll(year: number, period_no: number, email: string): Promise { //finds the employee const employee = await this.prisma.employees.findFirst({ @@ -56,6 +46,7 @@ export class TimesheetsQueryService { start_time: true, end_time: true, is_approved: true, + is_remote: true, bank_code: { select: { type: true } }, }, orderBy:[ { date:'asc'}, { start_time: 'asc'} ], @@ -73,10 +64,11 @@ export class TimesheetsQueryService { bank_code: { select: { type: true } }, }, orderBy: { date: 'asc' }, - }); - - const to_num = (value: any) => typeof value.toNumber === 'function' ? value.toNumber() : Number(value); + }); + const to_num = (value: any) => value && typeof (value as any).toNumber === 'function' + ? (value as any).toNumber() + : Number(value); // data mapping const shifts: ShiftRow[] = raw_shifts.map(shift => ({ @@ -85,12 +77,12 @@ export class TimesheetsQueryService { end_time: shift.end_time, type: String(shift.bank_code?.type ?? '').toUpperCase(), is_approved: shift.is_approved ?? true, + is_remote: shift.is_remote ?? true, })); const expenses: ExpenseRow[] = raw_expenses.map(expense => ({ date: expense.date, - amount: typeof (expense.amount as any)?.to_num === 'function' ? - (expense.amount as any).to_num() : Number(expense.amount), + amount: to_num(expense.amount), type: String(expense.bank_code?.type ?? '').toUpperCase(), is_approved: expense.is_approved ?? true, })); @@ -98,6 +90,103 @@ export class TimesheetsQueryService { return buildPeriod(period.period_start, period.period_end, shifts , expenses); } + async getTimesheetByEmail(email: string, week_offset = 0): Promise { + + //fetch user related to email + const user = await this.prisma.users.findUnique({ + where: { email }, + select: { id: true }, + }); + if(!user) throw new NotFoundException(`user with email ${email} not found`); + + //fetch employee_id matching the email + const employee = await this.prisma.employees.findFirst({ + where: { user_id: user.id }, + select: { id: true }, + }); + if(!employee) throw new NotFoundException(`Employee with email: ${email} not found`); + + //sets current week Sunday -> Saturday + const base = new Date(); + const offset = new Date(base); + offset.setDate(offset.getDate() + (week_offset * 7)); + + const start_date_week = getWeekStart(offset, 0); + const end_date_week = getWeekEnd(start_date_week); + const start_day = formatDateISO(start_date_week); + const end_day = formatDateISO(end_date_week); + + //build the label MM/DD/YYYY.MM/DD.YYYY + const mm_dd = (date: Date) => `${String(date.getFullYear())}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2,'0')}`; + const label = `${mm_dd(start_date_week)}.${mm_dd(end_date_week)}`; + + //fetch timesheet shifts and expenses + const timesheet = await this.prisma.timesheets.findUnique({ + where: { + employee_id_start_date: { + employee_id: employee.id, + start_date: start_date_week, + }, + }, + include: { + shift: { + include: { bank_code: true }, + orderBy: [{ date: 'asc'}, { start_time: 'asc'}], + }, + expense: { + include: { bank_code: true }, + orderBy: [{date: 'asc'}], + }, + }, + }); + + //returns an empty timesheet if not found + if(!timesheet) { + return { + is_approved: false, + start_day, + end_day, + label, + shifts:[], + expenses: [], + } as TimesheetDto; + } + + //small helper to format hours:minutes + const to_HH_mm = (date: Date) => date.toISOString().slice(11, 16); + + //maps all shifts of selected timesheet + const shifts = timesheet.shift.map((sft) => ({ + bank_type: sft.bank_code?.type ?? '', + date: formatDateISO(sft.date), + start_time: to_HH_mm(sft.start_time), + end_time: to_HH_mm(sft.end_time), + description: sft.description ?? '', + is_approved: sft.is_approved ?? false, + is_remote: sft.is_remote ?? false, + })); + + //maps all expenses of selected timsheet + const expenses = timesheet.expense.map((exp) => ({ + bank_type: exp.bank_code?.type ?? '', + date: formatDateISO(exp.date), + amount: Number(exp.amount) || 0, + km: 0, + description: exp.description ?? '', + supervisor_comment: exp.supervisor_comment ?? '', + is_approved: exp.is_approved ?? false, + })); + + return { + is_approved: timesheet.is_approved, + start_day, + end_day, + label, + shifts, + expenses, + } as TimesheetDto; + } + async findOne(id: number): Promise { const timesheet = await this.prisma.timesheets.findUnique({ where: { id }, @@ -126,20 +215,6 @@ export class TimesheetsQueryService { return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours }; } - async update(id: number, dto:UpdateTimesheetDto): Promise { - await this.findOne(id); - const { employee_id, is_approved } = dto; - return this.prisma.timesheets.update({ - where: { id }, - data: { - ...(employee_id !== undefined && { employee_id }), - ...(is_approved !== undefined && { is_approved }), - }, - include: { employee: { include: { user: true } }, - }, - }); - } - async remove(id: number): Promise { await this.findOne(id); return this.prisma.timesheets.delete({ where: { id } }); diff --git a/src/modules/timesheets/utils/timesheet.helpers.ts b/src/modules/timesheets/utils/timesheet.helpers.ts index c94d64d..eadae40 100644 --- a/src/modules/timesheets/utils/timesheet.helpers.ts +++ b/src/modules/timesheets/utils/timesheet.helpers.ts @@ -1,3 +1,4 @@ +import { toDateString } from "src/modules/pay-periods/utils/pay-year.util"; import { DayExpensesDto, DetailedShifts, ShiftDto, TimesheetPeriodDto, WeekDto } from "../dtos/timesheet-period.dto"; //makes the strings indexes for arrays @@ -33,8 +34,8 @@ const EXPENSE_TYPES = { } as const; //DB line types -export type ShiftRow = { date: Date; start_time: Date; end_time: Date; is_approved?: boolean; type: string }; -export type ExpenseRow = { date: Date; amount: number; type: string; is_approved?: boolean }; +export type ShiftRow = { date: Date; start_time: Date; end_time: Date; is_approved?: boolean; is_remote: boolean; type: string }; +export type ExpenseRow = { date: Date; amount: number; type: string; is_approved?: boolean; }; //helper functions export function toUTCDateOnly(date: Date | string): Date { @@ -154,9 +155,12 @@ export function buildWeek( for (const shift of week_shifts) { const key = dayKeyFromDate(shift.date, true); week.shifts[key].shifts.push({ - start: toTimeString(shift.start_time), - end: toTimeString(shift.end_time), + date: toDateString(shift.date), + type: shift.type, + start_time: toTimeString(shift.start_time), + end_time: toTimeString(shift.end_time), is_approved: shift.is_approved ?? true, + is_remote: shift.is_remote, } as ShiftDto); day_times[key].push({ start: shift.start_time, end: shift.end_time});