diff --git a/prisma/migrations/20250828175750_changed_phone_number_type_to_string/migration.sql b/prisma/migrations/20250828175750_changed_phone_number_type_to_string/migration.sql new file mode 100644 index 0000000..c7ba659 --- /dev/null +++ b/prisma/migrations/20250828175750_changed_phone_number_type_to_string/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "public"."users" ALTER COLUMN "phone_number" SET DATA TYPE TEXT; diff --git a/prisma/mock-seeds-scripts/01-bankCodes.ts b/prisma/mock-seeds-scripts/01-bankCodes.ts index 6d5cec2..14d552f 100644 --- a/prisma/mock-seeds-scripts/01-bankCodes.ts +++ b/prisma/mock-seeds-scripts/01-bankCodes.ts @@ -9,6 +9,7 @@ async function main() { ['EVENING' ,'SHIFT', 1.25, 'G43'], ['Emergency','SHIFT', 2 , 'G48'], ['HOLIDAY' ,'SHIFT', 2.0 , 'G700'], + ['EXPENSES','EXPENSE', 1.0 , 'G517'], ['MILEAGE' ,'EXPENSE', 0.72, 'G57'], diff --git a/prisma/mock-seeds-scripts/02-users.ts b/prisma/mock-seeds-scripts/02-users.ts index 442678e..0352eb9 100644 --- a/prisma/mock-seeds-scripts/02-users.ts +++ b/prisma/mock-seeds-scripts/02-users.ts @@ -1,17 +1,15 @@ import { PrismaClient, Roles } from '@prisma/client'; const prisma = new PrismaClient(); -const BASE_PHONE = '1_100_000_000'; // < 2_147_483_647 + +// base sans underscore, en string +const BASE_PHONE = "1100000000"; function emailFor(i: number) { return `user${i + 1}@example.test`; } async function main() { - // 50 users total: 40 employees + 10 customers - // Roles distribution for the 40 employees: - // 1 ADMIN, 4 SUPERVISOR, 1 HR, 1 ACCOUNTING, 33 EMPLOYEE - // 10 CUSTOMER (non-employees) const usersData: { first_name: string; last_name: string; @@ -24,7 +22,6 @@ async function main() { const firstNames = ['Alex','Sam','Chris','Jordan','Taylor','Morgan','Jamie','Robin','Avery','Casey']; const lastNames = ['Smith','Johnson','Williams','Brown','Jones','Miller','Davis','Wilson','Taylor','Clark']; - // helper to pick const pick = (arr: T[]) => arr[Math.floor(Math.random() * arr.length)]; const rolesForEmployees: Roles[] = [ @@ -37,14 +34,14 @@ async function main() { // 40 employees for (let i = 0; i < 40; i++) { - const fn = pick(firstNames); const ln = pick(lastNames); usersData.push({ first_name: fn, last_name: ln, email: emailFor(i), - phone_number: BASE_PHONE + i, + // on concatène proprement en string + phone_number: BASE_PHONE + i.toString(), residence: Math.random() < 0.5 ? 'QC' : 'ON', role: rolesForEmployees[i], }); @@ -58,7 +55,7 @@ async function main() { first_name: fn, last_name: ln, email: emailFor(i), - phone_number: BASE_PHONE + i, + phone_number: BASE_PHONE + i.toString(), residence: Math.random() < 0.5 ? 'QC' : 'ON', role: Roles.CUSTOMER, }); diff --git a/prisma/mock-seeds-scripts/10-shifts.ts b/prisma/mock-seeds-scripts/10-shifts.ts index 3fe1791..d39c36c 100644 --- a/prisma/mock-seeds-scripts/10-shifts.ts +++ b/prisma/mock-seeds-scripts/10-shifts.ts @@ -2,46 +2,107 @@ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); -function timeAt(hour:number, minute:number) { - // stocker une heure (Postgres TIME) via Date (UTC 1970-01-01) +// Stocker une heure (Postgres TIME) via Date (UTC 1970-01-01) +function timeAt(hour: number, minute: number) { return new Date(Date.UTC(1970, 0, 1, hour, minute, 0)); } -function daysAgo(n:number) { - const d = new Date(); - d.setUTCDate(d.getUTCDate() - n); - d.setUTCHours(0,0,0,0); + +// Lundi de la semaine (en UTC) pour la date courante +function mondayOfThisWeekUTC(now = new Date()) { + // converti en UTC (sans l'heure) + const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); + const day = d.getUTCDay(); // 0=Dim, 1=Lun, ... + const diffToMonday = (day + 6) % 7; // 0 si lundi, 1 si mardi, ... 6 si dimanche + d.setUTCDate(d.getUTCDate() - diffToMonday); + d.setUTCHours(0, 0, 0, 0); return d; } +// Retourne les 5 dates Lundi→Vendredi (UTC, à minuit) +function currentWeekDates() { + const monday = mondayOfThisWeekUTC(); + return Array.from({ length: 5 }, (_, i) => { + const d = new Date(monday); + d.setUTCDate(monday.getUTCDate() + i); + return d; + }); +} + async function main() { - const bankCodes = await prisma.bankCodes.findMany({ where: { categorie: 'SHIFT' }, select: { id: true } }); - if (!bankCodes.length) throw new Error('Need SHIFT bank codes'); + // On récupère les bank codes requis (ajuste le nom de la colonne "code" si besoin) + const BANKS = ['G1', 'G305', 'G105'] as const; + const bcRows = await prisma.bankCodes.findMany({ + where: { bank_code: { in: BANKS as unknown as string[] } }, + select: { id: true, bank_code: true }, + }); + const bcMap = new Map(bcRows.map(b => [b.bank_code, b.id])); + + // Vérifications + for (const c of BANKS) { + if (!bcMap.has(c)) throw new Error(`Bank code manquant: ${c}`); + } const employees = await prisma.employees.findMany({ select: { id: true } }); + if (!employees.length) { + console.log('Aucun employé — rien à insérer.'); + return; + } + + const weekDays = currentWeekDates(); + + // Par défaut: G1 (régulier). 1 employé sur 5 : 1 jour de la semaine passe à G305 OU G105 (au hasard) + // Horaires: on décale par employé pour garantir des horaires différents. + // - start = 7, 7h30, 8, 8h30 selon l’index employé + // - durée = 8h sauf vendredi (7h) pour varier un peu + for (let ei = 0; ei < employees.length; ei++) { + const e = employees[ei]; - for (const e of employees) { const tss = await prisma.timesheets.findMany({ where: { employee_id: e.id }, select: { id: true }, + orderBy: { id: 'asc' }, // ajuste si tu préfères "created_at" etc. }); if (!tss.length) continue; - // 10 shifts / employee - for (let i = 0; i < 10; i++) { - const ts = tss[i % tss.length]; - const bc = bankCodes[i % bankCodes.length]; - const date = daysAgo(7 + i); // la dernière quinzaine - const startH = 8 + (i % 3); // 8..10 - const endH = startH + 7 + (i % 2); // 15..17 + // Horaires spécifiques à l’employé (déterministes) + const startHalfHourSlot = ei % 4; // 0..3 -> 7:00, 7:30, 8:00, 8:30 + const startHourBase = 7 + Math.floor(startHalfHourSlot / 2); // 7 ou 8 + const startMinuteBase = (startHalfHourSlot % 2) * 30; // 0 ou 30 + + // Doit-on donner un jour "différent" de G1 à cet employé ? + const isSpecial = (ei % 5) === 0; // 1 sur 5 + const specialDayIdx = isSpecial ? Math.floor(Math.random() * 5) : -1; + const specialCode = isSpecial ? (Math.random() < 0.5 ? 'G305' : 'G105') : 'G1'; + + // 5 jours (lun→ven) + for (let di = 0; di < weekDays.length; di++) { + const date = weekDays[di]; + + // Bank code du jour + const codeToday = (di === specialDayIdx) ? specialCode : 'G1'; + const bank_code_id = bcMap.get(codeToday)!; + + // Durée : 8h habituellement, 7h le vendredi pour varier (di==4) + const duration = (di === 4) ? 7 : 8; + + // Légère variation journalière (+0..2h) pour casser la monotonie, mais bornée + const dayOffset = di % 3; // 0,1,2 + const startH = Math.min(10, startHourBase + dayOffset); + const startM = startMinuteBase; + + const endH = startH + duration; + const endM = startM; + + const ts = tss[di % tss.length]; await prisma.shifts.create({ data: { timesheet_id: ts.id, - bank_code_id: bc.id, - description: `Shift ${i + 1} for emp ${e.id}`, - date, - start_time: timeAt(startH, 0), - end_time: timeAt(endH, 0), + bank_code_id, + description: `Shift ${di + 1} (Semaine courante) emp ${e.id} — ${codeToday}`, + date, // Date du jour (UTC minuit) + start_time: timeAt(startH, startM), + end_time: timeAt(endH, endM), is_approved: Math.random() < 0.5, }, }); @@ -49,7 +110,7 @@ async function main() { } const total = await prisma.shifts.count(); - console.log(`✓ Shifts: ${total} total rows`); + console.log(`✓ Shifts: ${total} total rows (semaine courante L→V)`); } main().finally(() => prisma.$disconnect()); diff --git a/prisma/mock-seeds-scripts/12-expenses.ts b/prisma/mock-seeds-scripts/12-expenses.ts index 2da1eb5..1b015c1 100644 --- a/prisma/mock-seeds-scripts/12-expenses.ts +++ b/prisma/mock-seeds-scripts/12-expenses.ts @@ -2,43 +2,111 @@ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); -function daysAgo(n:number) { - const d = new Date(); - d.setUTCDate(d.getUTCDate() - n); - d.setUTCHours(0,0,0,0); +// Lundi de la semaine (en UTC) pour la date courante +function mondayOfThisWeekUTC(now = new Date()) { + const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); + const day = d.getUTCDay(); // 0=Dim, 1=Lun, ... + const diffToMonday = (day + 6) % 7; // 0 si lundi + d.setUTCDate(d.getUTCDate() - diffToMonday); + d.setUTCHours(0, 0, 0, 0); return d; } -async function main() { - const expenseCodes = await prisma.bankCodes.findMany({ where: { categorie: 'EXPENSE' }, select: { id: true } }); - if (!expenseCodes.length) throw new Error('Need EXPENSE bank codes'); +// Retourne les 5 dates Lundi→Vendredi (UTC, à minuit) +function currentWeekDates() { + const monday = mondayOfThisWeekUTC(); + return Array.from({ length: 5 }, (_, i) => { + const d = new Date(monday); + d.setUTCDate(monday.getUTCDate() + i); + return d; + }); +} - const timesheets = await prisma.timesheets.findMany({ select: { id: true } }); - if (!timesheets.length) { - console.warn('No timesheets found; aborting expenses seed.'); +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 (ex: "123.45") +} + +async function main() { + // On veut explicitement G503 (mileage) et G517 (remboursement) + const wanted = ['G57', 'G517'] as const; + const codes = await prisma.bankCodes.findMany({ + where: { bank_code: { in: wanted as unknown as string[] } }, + select: { id: true, bank_code: true }, + }); + const map = new Map(codes.map(c => [c.bank_code, c.id])); + for (const c of wanted) { + if (!map.has(c)) throw new Error(`Bank code manquant: ${c}`); + } + + const employees = await prisma.employees.findMany({ select: { id: true } }); + if (!employees.length) { + console.warn('Aucun employé — rien à insérer.'); return; } - // 5 expenses distribuées aléatoirement parmi les employés (via timesheets) - for (let i = 0; i < 5; i++) { - const ts = timesheets[Math.floor(Math.random() * timesheets.length)]; - const bc = expenseCodes[i % expenseCodes.length]; - await prisma.expenses.create({ - data: { - timesheet_id: ts.id, - bank_code_id: bc.id, - date: daysAgo(3 + i), - amount: (50 + i * 10).toFixed(2), - attachement: null, - description: `Expense #${i + 1}`, - is_approved: Math.random() < 0.5, - supervisor_comment: Math.random() < 0.3 ? 'OK' : null, - }, + const weekDays = currentWeekDates(); + + // Règles: + // - (index % 5) === 0 -> mileage G503 (km) + // - (index % 5) === 1 -> remboursement G517 ($) + // Les autres: pas de dépense + // On met la dépense un des jours de la semaine (déterministe mais varié). + let created = 0; + + for (let ei = 0; ei < employees.length; ei++) { + const e = employees[ei]; + + const ts = await prisma.timesheets.findFirst({ + where: { employee_id: e.id }, + select: { id: true }, + orderBy: { id: 'asc' }, // ajuste si tu préfères par date }); + if (!ts) continue; + + const dayIdx = ei % 5; // 0..4 -> répartit sur la semaine + const date = weekDays[dayIdx]; + + if (ei % 5 === 0) { + // Mileage (G503) — amount = km + const km = rndInt(10, 180); // 10..180 km + await prisma.expenses.create({ + data: { + timesheet_id: ts.id, + bank_code_id: map.get('G57')!, + date, + amount: km.toString(), // on stocke le nombre de km dans amount (si tu as un champ "quantity_km", remplace ici) + attachement: null, + description: `Mileage ${km} km (emp ${e.id})`, + is_approved: Math.random() < 0.6, + supervisor_comment: Math.random() < 0.2 ? 'OK' : null, + }, + }); + created++; + } else if (ei % 5 === 1) { + // Remboursement (G517) — amount = $ + const dollars = rndAmount(2000, 25000); // 20.00$..250.00$ + await prisma.expenses.create({ + data: { + timesheet_id: ts.id, + bank_code_id: map.get('G517')!, + date, + amount: dollars, + attachement: null, + description: `Remboursement ${dollars}$ (emp ${e.id})`, + is_approved: Math.random() < 0.6, + supervisor_comment: Math.random() < 0.2 ? 'OK' : null, + }, + }); + created++; + } } const total = await prisma.expenses.count(); - console.log(`✓ Expenses: ${total} total rows`); + console.log(`✓ Expenses: ${created} nouvelles lignes, ${total} total rows (semaine courante)`); } main().finally(() => prisma.$disconnect()); diff --git a/src/modules/exports/controllers/csv-exports.controller.ts b/src/modules/exports/controllers/csv-exports.controller.ts index c3363f9..71cde76 100644 --- a/src/modules/exports/controllers/csv-exports.controller.ts +++ b/src/modules/exports/controllers/csv-exports.controller.ts @@ -4,6 +4,7 @@ import { Roles as RoleEnum } from '.prisma/client'; import { CsvExportService } from "../services/csv-exports.service"; // import { ExportCompany, ExportCsvOptionsDto, ExportType } from "../dtos/export-csv-options.dto"; import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { ExportCsvOptionsDto } from "../dtos/export-csv-options.dto"; @Controller('exports') @@ -11,34 +12,29 @@ import { RolesAllowed } from "src/common/decorators/roles.decorators"; export class CsvExportController { constructor(private readonly csvService: CsvExportService) {} - // @Get('csv/:year/:period_no') - // @Header('Content-Type', 'text/csv; charset=utf-8') - // @Header('Content-Disposition', 'attachment; filename="export.csv"') - // //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.ACCOUNTING, RoleEnum.HR) - // async exportCsv(@Query() options: ExportCsvOptionsDto, - // @Query('period') periodId: string ): Promise { - // //modify to accept year and period_number - // //sets default values - // const companies = options.companies && options.companies.length ? options.companies : - // [ ExportCompany.TARGO, ExportCompany.SOLUCOM]; - // const types = options.type && options.type.length ? options.type : - // Object.values(ExportType); - - // //collects all - // const all = await this.csvService.collectTransaction(Number(periodId), companies); - - // //filters by type - // const filtered = all.filter(row => { - // switch (row.bank_code.toLocaleLowerCase()) { - // case 'holiday' : return types.includes(ExportType.HOLIDAY); - // case 'vacation' : return types.includes(ExportType.VACATION); - // case 'expenses' : return types.includes(ExportType.EXPENSES); - // default : return types.includes(ExportType.SHIFTS); - // } - // }); - - // //generating the csv file - // return this.csvService.generateCsv(filtered); - // } + @Get('csv') + @Header('Content-Type', 'text/csv; charset=utf-8') + @Header('Content-Disposition', 'attachment; filename="export.csv"') + //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.ACCOUNTING, RoleEnum.HR) + async exportCsv(@Query() query: ExportCsvOptionsDto ): Promise { + const rows = await this.csvService.collectTransaction( + query.year, + query.period_no, + { + approved: query.approved ?? true, + types: { + shifts: query.shifts ?? true, + expenses: query.expenses ?? true, + holiday: query.holiday ?? true, + vacation: query.vacation ?? true, + }, + companies: { + targo: query.targo ?? true, + solucom: query.solucom ?? true, + }, + } + ); + return this.csvService.generateCsv(rows); + } } \ No newline at end of file diff --git a/src/modules/exports/services/csv-exports.service.ts b/src/modules/exports/services/csv-exports.service.ts index d8bd73e..3035156 100644 --- a/src/modules/exports/services/csv-exports.service.ts +++ b/src/modules/exports/services/csv-exports.service.ts @@ -31,187 +31,222 @@ type Filters = { export class CsvExportService { constructor(private readonly prisma: PrismaService) {} - // async collectTransaction( - // year: number, - // period_no: number, - // filters: Filters, - // approved: boolean = true - // ): Promise { + async collectTransaction( + year: number, + period_no: number, + filters: Filters, + approved: boolean = true + ): Promise { //fetch period - // const period = await this.prisma.payPeriods.findFirst({ - // where: { pay_year: year, pay_period_no: period_no }, - // select: { period_start: true, period_end: true }, - // }); - // if(!period) throw new NotFoundException(`Pay period ${ year }-${ period_no } not found`); + const period = await this.prisma.payPeriods.findFirst({ + where: { pay_year: year, pay_period_no: period_no }, + select: { period_start: true, period_end: true }, + }); + if(!period) throw new NotFoundException(`Pay period ${ year }-${ period_no } not found`); - // const start = period.period_start; - // const end = period.period_end; + const start = period.period_start; + const end = period.period_end; - // //fetch company codes from .env - // const comapany_codes = this.resolveCompanyCodes(filters.companies); - // if(comapany_codes.length === 0) throw new BadRequestException('No company selected'); + //fetch company codes from .env + const company_codes = this.resolveCompanyCodes(filters.companies); + if(company_codes.length === 0) throw new BadRequestException('No company selected'); - // //Flag types - // const { shifts: want_shifts, expenses: want_expense, holiday: want_holiday, vacation: want_vacation } = filters.types; - // if(!want_shifts && !want_expense && !want_holiday && !want_vacation) { - // throw new BadRequestException(' No export type selected '); - // } + //Flag types + const { shifts: want_shifts, expenses: want_expense, holiday: want_holiday, vacation: want_vacation } = filters.types; + if(!want_shifts && !want_expense && !want_holiday && !want_vacation) { + throw new BadRequestException(' No export type selected '); + } - // const approved_filter = filters.approved? { is_approved: true } : {}; + const approved_filter = filters.approved? { is_approved: true } : {}; - // //Prisma queries - // const [shifts, expenses] = await Promise.all([ - // want_shifts || want_expense || want_holiday || want_vacation - // ]) + const {holiday_code, vacation_code} = this.resolveLeaveCodes(); + //Prisma queries + const promises: Array> = []; + if (want_shifts) { + promises.push( this.prisma.shifts.findMany({ + where: { + date: { gte: start, lte: end }, + ...approved_filter, + bank_code: { bank_code: { notIn: [ holiday_code, vacation_code ] } }, + timesheet: { employee: { company_code: { in: company_codes } } }, + }, + select: { + date: true, + start_time: true, + end_time: true, + bank_code: { select: { bank_code: true } }, + timesheet: { select: { + employee: { select: { + company_code: true, + external_payroll_id: true, + user: { select: { first_name: true, last_name: true } }, + }}, + }}, + }, + })); + } else { + promises.push(Promise.resolve([])); + } + if(want_holiday) { + promises.push( this.prisma.shifts.findMany({ + where: { + date: { gte: start, lte: end }, + ...approved_filter, + bank_code: { bank_code: holiday_code }, + timesheet: { employee: { company_code: { in: company_codes } } }, + }, + select: { + date: true, + start_time: true, + end_time: true, + bank_code: { select: { bank_code: true } }, + timesheet: { select: { + employee: { select: { + company_code: true, + external_payroll_id: true, + user: { select: { first_name: true,last_name: true } }, + } }, + } }, + }, + })); + }else { + promises.push(Promise.resolve([])); + } + if(want_vacation) { + promises.push( this.prisma.shifts.findMany({ + where: { + date: { gte: start, lte: end }, + ...approved_filter, + bank_code: { bank_code: vacation_code }, + timesheet: { employee: { company_code: { in: company_codes } } }, + }, + select: { + date: true, + start_time: true, + end_time: true, + bank_code: { select: { bank_code: true } }, + timesheet: { select: { + employee: { select: { + company_code: true, + external_payroll_id: true, + user: { select: { first_name: true,last_name: true } }, + } }, + } }, + }, + })); + }else { + promises.push(Promise.resolve([])); + } + if(want_expense) { + promises.push( this.prisma.expenses.findMany({ + where: { + date: { gte: start, lte: end }, + ...approved_filter, + timesheet: { employee: { company_code: { in: company_codes } } }, + }, + select: { + date: true, + amount: true, + bank_code: { select: { bank_code: true } }, + timesheet: { select: { + employee: { select: { + company_code: true, + external_payroll_id: true, + user: { select: { first_name: true, last_name: true } }, + }}, + }}, + }, + })); + } else { + promises.push(Promise.resolve([])); + } + //array of arrays + const [ base_shifts, holiday_shifts, vacation_shifts, expenses ] = await Promise.all(promises); + //mapping + const rows: CsvRow[] = []; - // const company_codes = companies.map(c => c === ExportCompany.TARGO ? 1 : 2); + const map_shifts = (shift: any, is_holiday: boolean) => { + const employee = shift.timesheet.employee; + const week = this.computeWeekNumber(start, shift.date); + return { + company_code: employee.company_code, + external_payroll_id: employee.external_payroll_id, + full_name: `${employee.first_name} ${ employee.last_name}`, + bank_code: shift.bank_code?.bank_code ?? '', + quantity_hours: this.computeHours(shift.start_time, shift.end_time), + amount: undefined, + week_number: week, + pay_date: this.formatDate(end), + holiday_date: is_holiday? this.formatDate(shift.date) : '', + } as CsvRow; + }; + //final mapping of all shifts based filters + for (const shift of base_shifts) rows.push(map_shifts(shift, false)); + for (const shift of holiday_shifts) rows.push(map_shifts(shift, true )); + for (const shift of vacation_shifts) rows.push(map_shifts(shift, false)); - // const period = await this.prisma.payPeriods.findFirst({ - // where: { pay_period_no: period_id }, - // }); - // if(!period) throw new NotFoundException(`Pay period ${period_id} not found`); + for (const expense of expenses) { + const employee = expense.timesheet.employee; + const week = this.computeWeekNumber(start, expense.date); + rows.push({ + company_code: employee.company_code, + external_payroll_id: employee.external_payroll_id, + full_name: `${employee.first_name} ${ employee.last_name}`, + bank_code: expense.bank_code?.bank_code ?? '', + quantity_hours: undefined, + amount: Number(expense.amount), + week_number: week, + pay_date: this.formatDate(end), + holiday_date: '', + }) + } - // const start_date = period.period_start; - // const end_date = period.period_end; + //Final mapping and sorts + rows.sort((a,b) => { + if(a.external_payroll_id !== b.external_payroll_id) { + return a.external_payroll_id - b.external_payroll_id; + } + const bk_code = String(a.bank_code).localeCompare(String(b.bank_code)); + if(bk_code !== 0) return bk_code; + if(a.bank_code !== b.bank_code) return a.bank_code.localeCompare(b.bank_code); + return 0; + }); - // const included_shifts = await this.prisma.shifts.findMany({ - // where: { } - // }) - - // const approved_filter = approved ? { is_approved: true } : {}; - - // //fetching shifts - // const shifts = await this.prisma.shifts.findMany({ - // where: { - // date: { gte: start_date, lte: end_date }, - // ...approved_filter, - // timesheet: { - // employee: { company_code: { in: company_codes} } }, - // }, - // include: { - // bank_code: true, - // timesheet: { include: { - // employee: { include: { - // user:true, - // supervisor: { include: { - // user:true } } } } } }, - // }, - // }); - - // //fetching expenses - // const expenses = await this.prisma.expenses.findMany({ - // where: { - // date: { gte: start_date, lte: end_date }, - // ...approved_filter, - // timesheet: { employee: { company_code: { in: company_codes} } }, - // }, - // include: { bank_code: true, - // timesheet: { include: { - // employee: { include: { - // user: true, - // supervisor: { include: { - // user:true } } } } } }, - // }, - // }); - - // //fetching leave requests - // const leaves = await this.prisma.leaveRequests.findMany({ - // where : { - // start_date_time: { gte: start_date, lte: end_date }, - // employee: { company_code: { in: company_codes } }, - // }, - // include: { - // bank_code: true, - // employee: { include: { - // user: true, - // supervisor: { include: { - // user: true } } } }, - // }, - // }); - - // const rows: CsvRow[] = []; - - // //Shifts Mapping - // for (const shift of shifts) { - // const emp = shift.timesheet.employee; - // const week_number = this.computeWeekNumber(start_date, shift.date); - // const hours = this.computeHours(shift.start_time, shift.end_time); - - // rows.push({ - // company_code: emp.company_code, - // external_payroll_id: emp.external_payroll_id, - // full_name: `${emp.user.first_name} ${emp.user.last_name}`, - // bank_code: shift.bank_code.bank_code, - // quantity_hours: hours, - // amount: undefined, - // week_number, - // pay_date: this.formatDate(end_date), - // holiday_date: undefined, - // }); - // } - - // //Expenses Mapping - // for (const e of expenses) { - // const emp = e.timesheet.employee; - // const week_number = this.computeWeekNumber(start_date, e.date); - - // rows.push({ - // company_code: emp.company_code, - // external_payroll_id: emp.external_payroll_id, - // full_name: `${emp.user.first_name} ${emp.user.last_name}`, - // bank_code: e.bank_code.bank_code, - // quantity_hours: undefined, - // amount: Number(e.amount), - // week_number, - // pay_date: this.formatDate(end_date), - // holiday_date: undefined, - // }); - // } - - // //Leaves Mapping - // for(const l of leaves) { - // if(!l.bank_code) continue; - // const emp = l.employee; - // const start = l.start_date_time; - // const end = l.end_date_time ?? start; - - // const week_number = this.computeWeekNumber(start_date, start); - // const hours = this.computeHours(start, end); - - // rows.push({ - // company_code: emp.company_code, - // external_payroll_id: emp.external_payroll_id, - // full_name: `${emp.user.first_name} ${emp.user.last_name}`, - // bank_code: l.bank_code.bank_code, - // quantity_hours: hours, - // amount: undefined, - // week_number, - // pay_date: this.formatDate(end_date), - // holiday_date: undefined, - // }); - // } - - //Final Mapping and sorts - // return rows.sort((a,b) => { - // if(a.external_payroll_id !== b.external_payroll_id) { - // return a.external_payroll_id - b.external_payroll_id; - // } - // if(a.bank_code !== b.bank_code) { - // return a.bank_code.localeCompare(b.bank_code); - // } - // return a.week_number - b.week_number; - // }); - // } - resolveCompanyCodes(companies: { targo: boolean; solucom: boolean; }) { - throw new Error("Method not implemented."); + return rows; } + resolveLeaveCodes(): { holiday_code: string; vacation_code: string; } { + const holiday_code = process.env.HOLIDAY_CODE?.trim(); + if(!holiday_code) throw new BadRequestException('Missing Holiday bank code'); + + const vacation_code = process.env.VACATION_CODE?.trim(); + if(!vacation_code) throw new BadRequestException('Missing Vacation bank code'); + + return { holiday_code, vacation_code}; + } + + resolveCompanyCodes(companies: { targo: boolean; solucom: boolean; }): number[] { + const out: number[] = []; + if (companies.targo) { + const code_no = parseInt(process.env.TARGO_NO ?? '', 10); + if(!Number.isFinite(code_no) || code_no <= 0) throw new BadRequestException('Invalid Targo code in env'); + out.push(code_no); + } + if (companies.solucom) { + const code_no = parseInt(process.env.SOLUCOM_NO ?? '', 10); + if(!Number.isFinite(code_no) || code_no <= 0) throw new BadRequestException('Invalid Solucom code in env'); + out.push(code_no); + } + return out; + } + + //csv builder and "mise en page" generateCsv(rows: CsvRow[]): Buffer { const header = [ 'company_code', @@ -225,18 +260,23 @@ export class CsvExportService { 'holiday_date', ].join(',') + '\n'; - const body = rows.map(r => [ - r.company_code, - r.external_payroll_id, - `${r.full_name.replace(/"/g, '""')}`, - r.bank_code, - r.quantity_hours?.toFixed(2) ?? '', - r.week_number, - r.pay_date, - r.holiday_date ?? '', - ].join(',')).join('\n'); - - return Buffer.from('\uFEFF' + header + body, 'utf8'); + const body = rows.map(row => { + const full_name = `${String(row.full_name).replace(/"/g, '""')}`; + const quantity_hours = (typeof row.quantity_hours === 'number') ? row.quantity_hours.toFixed(2) : ''; + const amount = (typeof row.amount === 'number') ? row.amount.toFixed(2) : ''; + return [ + row.company_code, + row.external_payroll_id, + full_name, + row.bank_code, + quantity_hours, + amount, + row.week_number, + row.pay_date, + row.holiday_date ?? '', + ].join(','); + }).join('\n'); + return Buffer.from('\uFEFF' + header + body, 'utf8'); } @@ -246,9 +286,13 @@ export class CsvExportService { } private computeWeekNumber(start: Date, date: Date): number { - const days = Math.floor((date.getTime() - start.getTime()) / (1000*60*60*24)); + const dayMS = 86400000; + const days = Math.floor((this.toUTC(date).getTime() - this.toUTC(start).getTime())/ dayMS); return Math.floor(days / 7 ) + 1; } + toUTC(date: Date) { + return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); + } private formatDate(d:Date): string { return d.toISOString().split('T')[0];