From 5452641f19606e174ead59bd7c5f7bff922f6fcf Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Mon, 24 Nov 2025 14:59:16 -0500 Subject: [PATCH 1/9] feat(migration): added a split prismaModule to manage legacy DB, created scripts to fetch old data, transform to match the new structure and creates new items in the new DB --- prisma-legacy/schema.prisma | 197 +++++++++++++ .../migration.sql | 67 +++++ .../migration.sql | 2 + .../migration.sql | 12 + prisma/schema.prisma | 8 +- scripts/done/import-employees-from-csv.ts | 270 ++++++++++++++++++ scripts/done/import-users-from-csv.ts | 106 +++++++ scripts/done/init-preferences.ts | 67 +++++ scripts/migrate-expenses.ts | 164 +++++++++++ scripts/migrate-shifts.ts | 210 ++++++++++++++ scripts/migrate-timesheets.ts | 118 ++++++++ scripts/migration.service.ts | 21 ++ src/app.module.ts | 2 + src/common/utils/date-utils.ts | 2 +- src/main.ts | 10 + src/prisma-legacy/prisma.module.ts | 9 + src/prisma-legacy/prisma.service.ts | 18 ++ .../shifts/services/shifts-create.service.ts | 6 +- .../services/shifts-update-delete.service.ts | 10 +- .../expenses-query.service.ts | 2 +- test/customers.e2e-spec.ts | 208 +++++++------- test/pay-periods-approval.e2e-spec.ts | 248 ++++++++-------- 22 files changed, 1515 insertions(+), 242 deletions(-) create mode 100644 prisma-legacy/schema.prisma create mode 100644 prisma/migrations/20251119155820_init_staging_db/migration.sql create mode 100644 prisma/migrations/20251119185059_preparation_to_get_the_staging_db_ready/migration.sql create mode 100644 prisma/migrations/20251121194224_unique_keys_added_to_shifts_and_expenses_for_migration_from_old_db/migration.sql create mode 100644 scripts/done/import-employees-from-csv.ts create mode 100644 scripts/done/import-users-from-csv.ts create mode 100644 scripts/done/init-preferences.ts create mode 100644 scripts/migrate-expenses.ts create mode 100644 scripts/migrate-shifts.ts create mode 100644 scripts/migrate-timesheets.ts create mode 100644 scripts/migration.service.ts create mode 100644 src/prisma-legacy/prisma.module.ts create mode 100644 src/prisma-legacy/prisma.service.ts diff --git a/prisma-legacy/schema.prisma b/prisma-legacy/schema.prisma new file mode 100644 index 0000000..e12d8be --- /dev/null +++ b/prisma-legacy/schema.prisma @@ -0,0 +1,197 @@ +generator client { + provider = "prisma-client-js" + output = "../node_modules/@prisma/client-legacy" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL_LEGACY") +} + +model codeDesjardins { + id String @id @map("_id") @db.VarChar(50) + code String @db.VarChar(50) + label String @db.VarChar(50) + description String @db.VarChar(250) +} + +model customers { + id String @id @map("_id") @db.Uuid + user_id String? @db.VarChar(50) + email String? @db.VarChar(50) + first_name String? @db.VarChar(50) + last_name String? @db.VarChar(50) + phone_number String? @db.VarChar(50) + address String? @db.VarChar(255) + created_at BigInt? + updated_at BigInt? + created_by String? @db.VarChar(50) +} + +model dealers { + id String @id @map("_id") @db.Uuid + user_id String? @db.VarChar(50) + email String? @db.VarChar(50) + first_name String? @db.VarChar(50) + last_name String? @db.VarChar(50) + phone_number String? @db.VarChar(50) + created_at BigInt? + updated_at BigInt? + created_by String? @db.VarChar(50) +} + +model employee_shift_template { + id String @id @map("_id") @db.Uuid + employee_id String @db.VarChar + day_of_the_week String @db.VarChar + start_time BigInt + end_time BigInt + created_at BigInt + updated_at BigInt +} + +model employees { + id String @id @map("_id") @db.Uuid + user_id String? @db.VarChar(50) + employee_number String? @db.VarChar(50) + email String? @db.VarChar(50) + first_name String? @db.VarChar(50) + last_name String? @db.VarChar(50) + phone_number String? @db.VarChar(50) + job_title String? @db.VarChar(50) + company Int? + supervisor String? @db.VarChar(50) + is_supervisor Boolean? + onboarding BigInt? + offboarding BigInt? + regular_hours_day Float? @db.Real + hours_bank_max Int? + created_at BigInt? + updated_at BigInt? + created_by String? @db.VarChar +} + +model expenses { + id String @id @map("_id") @db.Uuid + time_sheet_id String? @db.VarChar(50) + date String? @db.VarChar(50) + code String? @db.VarChar(50) + value Float? @db.Real + description String? @db.VarChar + evidence_id String? @db.VarChar + status Boolean? + created_at BigInt? + updated_at BigInt? + supervisor_note String? @db.VarChar(255) +} + +model hours_bank { + id String @id @map("_id") @db.Uuid + employee_id String? @db.VarChar(50) + hours Float? @db.Real + created_at BigInt? + updated_at BigInt? +} + +model mileage_bank { + id String @id @map("_id") @db.Uuid + employee_id String? @db.VarChar(50) + mileage Int? + year Int? +} + +model shifts { + id String @id @map("_id") @db.Uuid + time_sheet_id String? @db.VarChar(50) + code String? @db.VarChar(50) + type String? @db.VarChar(50) + date DateTime? @db.Date + start_time BigInt? + end_time BigInt? + comment String? @db.VarChar(255) + status Boolean? + created_at BigInt? + updated_at BigInt? + supervisor_note String? @db.VarChar(255) +} + +model shifts_of_template { + id String @id @map("_id") @db.Uuid + model_id String @db.Uuid + day_of_the_week String @db.VarChar(50) + start_time BigInt + end_time BigInt + created_at BigInt + updated_at BigInt +} + +model sick_leave { + id String @id @map("_id") @db.Uuid + employee_id String? @db.VarChar(50) + accumulated Float? @db.Real + consumed Float? @db.Real + year Int? + created_at BigInt? + updated_at BigInt? +} + +model time_sheet_periods { + id String @id @map("_id") @db.Uuid + start_date DateTime? @db.Date + end_date DateTime? @db.Date + payment_date DateTime? @db.Date + period_number Int? + year Int? +} + +model time_sheet_template { + id String @id @map("_id") @db.Uuid + title String @db.VarChar + description String? @db.VarChar + created_at BigInt + updated_at BigInt +} + +model time_sheets { + id String @id @map("_id") @db.Uuid + employee_id String? @db.VarChar + start_date DateTime? @db.Date + end_date DateTime? @db.Date + status Boolean? + banked_hours Float? @db.Real + consumed_vacation Float? @db.Real + consumed_sick Float? @db.Real + period_id String? @db.VarChar(50) + period_number Int? + created_at BigInt? + updated_at BigInt? + blocked_week Boolean? @default(false) +} + +model users { + id String @id @map("_id") @db.VarChar(50) + email String @unique @db.VarChar(50) + password String @db.VarChar(255) + type String @db.VarChar(50) + role String @db.VarChar(50) + is_verified Boolean? + verification_token String? + otp_token String? + refresh_token String? + created_at BigInt? + updated_at BigInt? + created_by String @db.VarChar(255) + last_login BigInt +} + +model vacation_leave { + id String @id @map("_id") @db.Uuid + employee_id String? @db.VarChar(50) + accumulated Float? @db.Real + consumed Float? @db.Real + created_at BigInt? + updated_at BigInt? + start_year Int? + end_year Int? + max_hours_per_year Float? @db.Real +} diff --git a/prisma/migrations/20251119155820_init_staging_db/migration.sql b/prisma/migrations/20251119155820_init_staging_db/migration.sql new file mode 100644 index 0000000..d5ef8f6 --- /dev/null +++ b/prisma/migrations/20251119155820_init_staging_db/migration.sql @@ -0,0 +1,67 @@ +/* + Warnings: + + - You are about to drop the column `patch` on the `attachment_variants` table. All the data in the column will be lost. + - You are about to alter the column `amount` on the `expenses` table. The data in that column could be lost. The data in that column will be cast from `Money` to `Decimal(12,2)`. + - You are about to alter the column `amount` on the `expenses_archive` table. The data in that column could be lost. The data in that column will be cast from `Money` to `Decimal(12,2)`. + - You are about to drop the column `date` on the `leave_requests` table. All the data in the column will be lost. + - You are about to drop the `customers` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `customers_archive` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `employees_archive` table. If the table is not empty, all the data it contains will be lost. + - A unique constraint covering the columns `[employee_id,leave_type,dates]` on the table `leave_requests` will be added. If there are existing duplicate values, this will fail. + - Added the required column `path` to the `attachment_variants` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "public"."customers" DROP CONSTRAINT "customers_user_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."customers_archive" DROP CONSTRAINT "customers_archive_customer_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."customers_archive" DROP CONSTRAINT "customers_archive_user_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."employees_archive" DROP CONSTRAINT "employees_archive_employee_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."employees_archive" DROP CONSTRAINT "employees_archive_supervisor_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."employees_archive" DROP CONSTRAINT "employees_archive_user_id_fkey"; + +-- DropIndex +DROP INDEX "public"."leave_requests_employee_id_date_idx"; + +-- DropIndex +DROP INDEX "public"."leave_requests_employee_id_leave_type_date_key"; + +-- AlterTable +ALTER TABLE "attachment_variants" DROP COLUMN "patch", +ADD COLUMN "path" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "expenses" ALTER COLUMN "amount" DROP NOT NULL, +ALTER COLUMN "amount" SET DATA TYPE DECIMAL(12,2); + +-- AlterTable +ALTER TABLE "expenses_archive" ALTER COLUMN "amount" SET DATA TYPE DECIMAL(12,2); + +-- AlterTable +ALTER TABLE "leave_requests" DROP COLUMN "date", +ADD COLUMN "dates" DATE[]; + +-- DropTable +DROP TABLE "public"."customers"; + +-- DropTable +DROP TABLE "public"."customers_archive"; + +-- DropTable +DROP TABLE "public"."employees_archive"; + +-- CreateIndex +CREATE INDEX "leave_requests_employee_id_dates_idx" ON "leave_requests"("employee_id", "dates"); + +-- CreateIndex +CREATE UNIQUE INDEX "leave_requests_employee_id_leave_type_dates_key" ON "leave_requests"("employee_id", "leave_type", "dates"); diff --git a/prisma/migrations/20251119185059_preparation_to_get_the_staging_db_ready/migration.sql b/prisma/migrations/20251119185059_preparation_to_get_the_staging_db_ready/migration.sql new file mode 100644 index 0000000..d4d9805 --- /dev/null +++ b/prisma/migrations/20251119185059_preparation_to_get_the_staging_db_ready/migration.sql @@ -0,0 +1,2 @@ +-- DropIndex +DROP INDEX "public"."users_phone_number_key"; diff --git a/prisma/migrations/20251121194224_unique_keys_added_to_shifts_and_expenses_for_migration_from_old_db/migration.sql b/prisma/migrations/20251121194224_unique_keys_added_to_shifts_and_expenses_for_migration_from_old_db/migration.sql new file mode 100644 index 0000000..bcb1752 --- /dev/null +++ b/prisma/migrations/20251121194224_unique_keys_added_to_shifts_and_expenses_for_migration_from_old_db/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - A unique constraint covering the columns `[timesheet_id,date,amount,mileage]` on the table `expenses` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[timesheet_id,date,start_time]` on the table `shifts` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "expenses_timesheet_id_date_amount_mileage_key" ON "expenses"("timesheet_id", "date", "amount", "mileage"); + +-- CreateIndex +CREATE UNIQUE INDEX "shifts_timesheet_id_date_start_time_key" ON "shifts"("timesheet_id", "date", "start_time"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b2171ad..d0d2c87 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,7 +11,7 @@ generator client { datasource db { provider = "postgresql" - url = env("DATABASE_URL_DEV") + url = env("DATABASE_URL_STAGING") } model Users { @@ -19,7 +19,7 @@ model Users { first_name String last_name String email String @unique - phone_number String @unique + phone_number String residence String? role Roles @default(GUEST) @@ -180,7 +180,7 @@ model Shifts { comment String? archive ShiftsArchive[] @relation("ShiftsToArchive") - + @@unique([timesheet_id, date, start_time], name: "unique_ts_id_date_start_time") @@map("shifts") } @@ -232,7 +232,7 @@ model Expenses { is_approved Boolean @default(false) archive ExpensesArchive[] @relation("ExpensesToArchive") - + @@unique([timesheet_id, date, amount, mileage], name: "unique_ts_id_date_amount_mileage") @@map("expenses") } diff --git a/scripts/done/import-employees-from-csv.ts b/scripts/done/import-employees-from-csv.ts new file mode 100644 index 0000000..d36cf8a --- /dev/null +++ b/scripts/done/import-employees-from-csv.ts @@ -0,0 +1,270 @@ +// src/scripts/import-employees-from-csv.ts +import { PrismaClient, Roles } from '@prisma/client'; +import * as fs from 'fs'; +import * as path from 'path'; + +const prisma = new PrismaClient(); + +// ⚙️ Chemin vers ton CSV employees +const CSV_PATH = path.resolve(__dirname, '../../data/export_new_employee_table.csv'); + +// Rôles éligibles pour la table Employees +const ELIGIBLE_ROLES: Roles[] = [ + Roles.EMPLOYEE, + Roles.SUPERVISOR, + Roles.HR, + Roles.ACCOUNTING, + Roles.ADMIN, +]; + +// Type correspondant EXACT aux colonnes de ton CSV +type EmployeeCsvRow = { + employee_number: string; + email: string; + job_title: string; + company: string; // sera converti en number + is_supervisor: string; // "True"/"False" (ou variantes) + onboarding: string; // millis + offboarding: string; // millis ou "NULL" +}; + +// Représentation minimale d'un user +type UserSummary = { + id: string; // UUID + email: string; + role: Roles; +}; + +// ============ Helpers CSV ============ + +function splitCsvLine(line: string): string[] { + const result: string[] = []; + let current = ''; + let inQuotes = false; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + + if (char === '"') { + // guillemet échappé "" + if (inQuotes && line[i + 1] === '"') { + current += '"'; + i++; + } else { + inQuotes = !inQuotes; + } + } else if (char === ',' && !inQuotes) { + result.push(current); + current = ''; + } else { + current += char; + } + } + + result.push(current); + return result.map((v) => v.trim()); +} + +// ============ Helpers de parsing ============ + +function parseBoolean(value: string): boolean { + const v = value.trim().toLowerCase(); + return v === 'true' || v === '1' || v === 'yes' || v === 'y' || v === 'oui'; +} + +function parseIntSafe(value: string, fieldName: string): number | null { + const trimmed = value.trim(); + if (!trimmed || trimmed.toUpperCase() === 'NULL') return null; + + const n = Number.parseInt(trimmed, 10); + if (Number.isNaN(n)) { + console.warn(`⚠️ Impossible de parser "${value}" en entier pour le champ ${fieldName}`); + return null; + } + return n; +} + +function millisToDate(value: string): Date | null { + const trimmed = value.trim().toUpperCase(); + if (!trimmed || trimmed === 'NULL') return null; + + const ms = Number(trimmed); + if (!Number.isFinite(ms)) { + console.warn(`⚠️ Impossible de parser "${value}" en millis pour une Date`); + return null; + } + + const d = new Date(ms); + // On normalise au jour (minuit UTC) + const normalized = new Date(Date.UTC( + d.getUTCFullYear(), + d.getUTCMonth(), + d.getUTCDate(), + )); + + return normalized; +} + +// ============ MAIN ============ + +async function main() { + // 1. Lecture du CSV + const fileContent = fs.readFileSync(CSV_PATH, 'utf-8'); + + const lines = fileContent + .split(/\r?\n/) + .map((l) => l.trim()) + .filter((l) => l.length > 0); + + if (lines.length <= 1) { + console.error('CSV vide ou seulement un header'); + return; + } + + const header = splitCsvLine(lines[0]); // ["employee_number","email",...] + const dataLines = lines.slice(1); + + const csvRows: EmployeeCsvRow[] = dataLines.map((line) => { + const values = splitCsvLine(line); + const row: any = {}; + + header.forEach((col, idx) => { + row[col] = values[idx] ?? ''; + }); + + return row as EmployeeCsvRow; + }); + + console.log(`➡️ ${csvRows.length} lignes trouvées dans le CSV employees`); + + // 2. Récupérer tous les emails du CSV + const emails = Array.from( + new Set( + csvRows + .map((r) => r.email.trim()) + .filter((e) => e.length > 0), + ), + ); + + console.log(`➡️ ${emails.length} emails uniques trouvés dans le CSV`); + + // 3. Charger les users correspondants avec les bons rôles + const users = (await prisma.users.findMany({ + where: { + email: { in: emails }, + role: { in: ELIGIBLE_ROLES }, + }, + select: { + id: true, + email: true, + role: true, + }, + })) as UserSummary[]; + + console.log(`➡️ ${users.length} users éligibles trouvés dans la DB`); + + // Map email → user + const userByEmail = new Map(); + for (const user of users) { + const key = user.email.trim().toLowerCase(); + userByEmail.set(key, user); + } + + // 4. Construire les données pour employees.createMany + const employeesToCreate: { + user_id: string; + external_payroll_id: number; + company_code: number; + first_work_day: Date; + last_work_day: Date | null; + job_title: string | null; + is_supervisor: boolean; + supervisor_id?: number | null; + }[] = []; + + const rowsWithoutUser: EmployeeCsvRow[] = []; + const rowsWithInvalidNumbers: EmployeeCsvRow[] = []; + + for (const row of csvRows) { + const emailKey = row.email.trim().toLowerCase(); + const user = userByEmail.get(emailKey); + + if (!user) { + rowsWithoutUser.push(row); + continue; + } + + const external_payroll_id = parseIntSafe(row.employee_number, 'external_payroll_id'); + const company_code = parseIntSafe(String(row.company), 'company_code'); + + if (external_payroll_id === null || company_code === null) { + rowsWithInvalidNumbers.push(row); + continue; + } + + const first_work_day = millisToDate(row.onboarding); + const last_work_day = millisToDate(row.offboarding); + const is_supervisor = parseBoolean(row.is_supervisor); + const job_title = row.job_title?.trim() || null; + + if (!first_work_day) { + console.warn( + `⚠️ Date d'onboarding invalide pour ${row.email} (employee_number=${row.employee_number})`, + ); + continue; + } + + employeesToCreate.push({ + user_id: user.id, + external_payroll_id, + company_code, + first_work_day, + last_work_day, + job_title, + is_supervisor, + supervisor_id: null, // on pourra gérer ça plus tard si tu as les infos + }); + } + + console.log(`➡️ ${employeesToCreate.length} entrées Employees prêtes à être insérées`); + + if (rowsWithoutUser.length > 0) { + console.warn(`⚠️ ${rowsWithoutUser.length} lignes CSV sans user correspondant (email / rôle) :`); + for (const row of rowsWithoutUser) { + console.warn( + ` - email=${row.email}, employee_number=${row.employee_number}, company=${row.company}`, + ); + } + } + + if (rowsWithInvalidNumbers.length > 0) { + console.warn(`⚠️ ${rowsWithInvalidNumbers.length} lignes CSV avec ids/compagnies invalides :`); + for (const row of rowsWithInvalidNumbers) { + console.warn( + ` - email=${row.email}, employee_number="${row.employee_number}", company="${row.company}"`, + ); + } + } + + if (employeesToCreate.length === 0) { + console.warn('⚠️ Aucun Employees à créer, arrêt.'); + return; + } + + // 5. Insert en batch + const result = await prisma.employees.createMany({ + data: employeesToCreate, + skipDuplicates: true, // évite les erreurs si tu relances le script + }); + + console.log(`✅ ${result.count} employees insérés dans la DB`); +} + +main() + .catch((err) => { + console.error('❌ Erreur pendant l’import CSV → Employees', err); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/scripts/done/import-users-from-csv.ts b/scripts/done/import-users-from-csv.ts new file mode 100644 index 0000000..2c0dad2 --- /dev/null +++ b/scripts/done/import-users-from-csv.ts @@ -0,0 +1,106 @@ +// src/scripts/import-users-from-csv.ts +import { PrismaClient } from '@prisma/client'; +import * as fs from 'fs'; +import * as path from 'path'; + +const prisma = new PrismaClient(); + +// ⚙️ Chemin vers ton CSV (à adapter selon où tu le mets) +const CSV_PATH = path.resolve(__dirname, '../../data/export_employee_table.csv'); + +// Type aligné sur les colonnes du CSV +type CsvUserRow = { + email: string; + first_name: string; + last_name: string; + phone_number: string; +}; + +// Petit parseur de ligne CSV sans dépendance +function splitCsvLine(line: string): string[] { + const result: string[] = []; + let current = ''; + let inQuotes = false; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + + if (char === '"') { + // guillemet échappé "" + if (inQuotes && line[i + 1] === '"') { + current += '"'; + i++; // on saute le deuxième + } else { + inQuotes = !inQuotes; + } + } else if (char === ',' && !inQuotes) { + result.push(current); + current = ''; + } else { + current += char; + } + } + + result.push(current); + return result.map((v) => v.trim()); +} + +async function main() { + // 1. Lecture du fichier CSV + const fileContent = fs.readFileSync(CSV_PATH, 'utf-8'); + + const lines = fileContent + .split(/\r?\n/) + .map((l) => l.trim()) + .filter((l) => l.length > 0); + + if (lines.length <= 1) { + console.error('CSV vide ou seulement un header'); + return; + } + + // 2. Header (noms de colonnes) -> ["email", "first_name", "last_name", "phone_number"] + const header = splitCsvLine(lines[0]); + const dataLines = lines.slice(1); + + // 3. Conversion de chaque ligne en objet { email, first_name, last_name, phone_number } + const records: CsvUserRow[] = dataLines.map((line) => { + const values = splitCsvLine(line); + const row: any = {}; + + header.forEach((col, idx) => { + row[col] = values[idx] ?? ''; + }); + + return row as CsvUserRow; + }); + + // 4. Mapping vers le format attendu par Prisma (model Users) + const data = records.map((row) => ({ + email: row.email.trim(), + first_name: row.first_name.trim(), + last_name: row.last_name.trim(), + phone_number: row.phone_number.trim(), + // residence: null, // si tu veux la forcer à null + // role: 'GUEST', // sinon Prisma va appliquer la valeur par défaut + })); + + console.log(`➡️ ${data.length} lignes trouvées dans le CSV`); + console.log('Exemple importé :', data[0]); + + + const result = await prisma.users.createMany({ + data, + }); + + console.log(`✅ ${result.count} utilisateurs insérés dans la DB`); +} + +main() + .catch((err) => { + console.error('❌ Erreur pendant l’import CSV → DB', err); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/scripts/done/init-preferences.ts b/scripts/done/init-preferences.ts new file mode 100644 index 0000000..f520857 --- /dev/null +++ b/scripts/done/init-preferences.ts @@ -0,0 +1,67 @@ +// src/scripts/init-preferences.ts +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +type UserSummary = { + id: string; // UUID + email: string; +}; + +async function main() { + console.log('➡️ Initialisation des préférences utilisateurs…'); + + // 1. Récupérer tous les users + const users = (await prisma.users.findMany({ + select: { + id: true, + email: true, + }, + })) as UserSummary[]; + + console.log(`➡️ ${users.length} users trouvés dans la DB`); + + // 2. Récupérer toutes les préférences existantes + const existingPrefs = await prisma.preferences.findMany({ + select: { + user_id: true, + }, + }); + + const userIdsWithPrefs = new Set(existingPrefs.map((p) => p.user_id)); + + console.log(`➡️ ${existingPrefs.length} users ont déjà des préférences`); + + // 3. Filtrer les users qui n'ont pas encore de preferences + const usersWithoutPrefs = users.filter((u) => !userIdsWithPrefs.has(u.id)); + + console.log(`➡️ ${usersWithoutPrefs.length} users n'ont pas encore de préférences`); + + if (usersWithoutPrefs.length === 0) { + console.log('✅ Rien à faire, toutes les préférences sont déjà créées.'); + return; + } + + // 4. Préparer les entrées pour createMany + const prefsToCreate = usersWithoutPrefs.map((u) => ({ + user_id: u.id, + // tous les autres champs prendront leurs valeurs par défaut (0) + })); + + // 5. Insertion en batch + const result = await prisma.preferences.createMany({ + data: prefsToCreate, + skipDuplicates: true, // sécurité si jamais le script est relancé + }); + + console.log(`✅ ${result.count} préférences créées dans la DB`); +} + +main() + .catch((err) => { + console.error('❌ Erreur pendant l’initialisation des préférences', err); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/scripts/migrate-expenses.ts b/scripts/migrate-expenses.ts new file mode 100644 index 0000000..6c20eb6 --- /dev/null +++ b/scripts/migrate-expenses.ts @@ -0,0 +1,164 @@ +import { PrismaClient as Prisma } from "@prisma/client"; +import { PrismaClient as PrismaLegacy } from "@prisma/client-legacy" +import { toDateFromString, toHHmmFromDate, toStringFromDate } from "src/common/utils/date-utils"; + +const prisma_legacy = new PrismaLegacy({}); +const prisma = new Prisma({}); + +type NewEmployee = { + id: number; + company_code: number; + external_payroll_id: number; +} + +type OldExpense = { + time_sheet_id: string | null; + date: string | null; + code: string | null; + description: string | null; + value: number | null; + status: boolean | null; +} + +export const extractOldExpenses = async () => { + for (let id = 1; id <= 61; id++) { + + console.log(`Start of Expense migration ***************************************************************`); + + const new_employee = await findOneNewEmployee(id); + console.log(`Employee ${id} found in new DB`); + + const new_timesheets = await findManyNewTimesheets(new_employee.id); + console.log(`New Timesheets found for employee ${id}`); + + const old_employee_id = await findOneOldEmployee(new_employee); + console.log(`Employee ${new_employee.id} found in old DB`); + + const old_timesheets = await findManyOldTimesheets(old_employee_id); + console.log(`Timesheets for employee ${old_employee_id}/${new_employee.id} found in old DB`); + + console.log('Start of Expense creation*****************************************************************'); + for (const old_timesheet of old_timesheets) { + if (!old_timesheet.start_date) continue; + const new_timesheet = new_timesheets.find((ts) => ts.start_date.getTime() === old_timesheet.start_date!.getTime()); + if (!new_timesheet) { + console.warn(`No new timesheet matching legacy timesheet ${old_timesheet.id}`); + continue; + } + const old_expenses = await prisma_legacy.expenses.findMany({ + where: { time_sheet_id: old_timesheet.id }, + select: { + time_sheet_id: true, + date: true, + code: true, + description: true, + value: true, + status: true, + + }, + }); + await createManyNewExpenses(new_timesheet.id, old_expenses); + } + + } + await prisma_legacy.$disconnect(); + await prisma.$disconnect(); +} + +const findOneNewEmployee = async (id: number): Promise => { + const new_employee = await prisma.employees.findUnique({ + where: { id: id }, + select: { + id: true, + company_code: true, + external_payroll_id: true, + }, + }); + if (!new_employee) throw new Error(`New Employee with id ${id} not found`) + return new_employee; +} + +const findOneOldEmployee = async (new_employee: NewEmployee): Promise => { + const old_employee = await prisma_legacy.employees.findFirst({ + where: { + company: new_employee.company_code, + employee_number: new_employee.external_payroll_id.toString(), + }, + select: { + id: true, + }, + }); + if (!old_employee) throw new Error(`Old Employee not found`); + return old_employee.id; +} + +const findManyOldTimesheets = async (old_employee_id: string) => { + const old_timesheets = await prisma_legacy.time_sheets.findMany({ + where: { employee_id: old_employee_id }, + select: { id: true, start_date: true, status: true } + }); + return old_timesheets; +} + +const findManyNewTimesheets = async (employee_id: number) => { + const timesheets = await prisma.timesheets.findMany({ + where: { employee_id: employee_id }, + select: { id: true, start_date: true } + }) + return timesheets; +} + +const createManyNewExpenses = async (timesheet_id: number, old_expenses: OldExpense[]) => { + for (const old_expense of old_expenses) { + let mileage: number = 0; + let amount: number = old_expense.value ?? 0; + if (old_expense.code === 'G503') { + mileage = old_expense.value!; + amount = mileage * 0.72; + } + if (mileage < 0) { + console.warn(`expense of value less than '0' found`) + } + + if (old_expense.date == null) { + console.warn(`Expense date invalid ${old_expense.date}`); + continue; + } + const date = toDateFromString(old_expense.date); + + if (old_expense.status == null) { + console.warn(`status null for legacy expense ${old_expense}`); + continue; + } + + if (old_expense.code == null) { + console.warn(`Code null for legacy expense ${old_expense.code}`); + continue; + } + const bank_code_id = await findBankCodeIdUsingOldCode(old_expense.code); + + await prisma.expenses.create({ + // where: { unique_ts_id_date_amount_mileage: { timesheet_id: timesheet_id, date, amount, mileage } }, + // update: { + // is_approved: old_expense.status, + // }, + data: { + date: date, + comment: old_expense.description ?? '', + timesheet_id: timesheet_id, + bank_code_id: bank_code_id, + amount: amount, + mileage: mileage, + } + }); + } +} + +const findBankCodeIdUsingOldCode = async (code: string): Promise => { + const bank_code = await prisma.bankCodes.findFirst({ + where: { bank_code: code }, + select: { id: true }, + }); + if (!bank_code) throw new Error(`Bank_code_id not found for Code ${code}`) + return bank_code.id; +} \ No newline at end of file diff --git a/scripts/migrate-shifts.ts b/scripts/migrate-shifts.ts new file mode 100644 index 0000000..5b966c4 --- /dev/null +++ b/scripts/migrate-shifts.ts @@ -0,0 +1,210 @@ +import { PrismaClient as PrismaNew } from "@prisma/client"; +import { PrismaClient as PrismaLegacy } from "@prisma/client-legacy" + +const prisma_legacy = new PrismaLegacy({}); +const prisma = new PrismaNew({}); + +type NewEmployee = { + id: number; + company_code: number; + external_payroll_id: number; +} + +type OldShifts = { + time_sheet_id: string | null; + code: string | null; + type: string | null; + date: Date | null; + start_time: bigint | null; + end_time: bigint | null; + comment: string | null; + status: boolean | null; +} + +export const extractOldShifts = async () => { + for (let id = 1; id <= 61; id++) { + console.log(`Start of shift migration ***************************************************************`); + const new_employee = await findOneNewEmployee(id); + console.log(`Employee ${id} found in new DB`); + + const new_timesheets = await findManyNewTimesheets(new_employee.id); + console.log(`New Timesheets found for employee ${id}`); + for (const ts of new_timesheets) { + console.log(`start_date = ${ts.start_date} timesheet_id = ${ts.id}`) + + } + console.log('***************************************************************'); + const old_employee_id = await findOneOldEmployee(new_employee); + console.log(`Employee ${new_employee.id} found in old DB`); + + const old_timesheets = await findManyOldTimesheets(old_employee_id); + console.log(`Timesheets for employee ${old_employee_id}/${new_employee.id} found in old DB`); + + for (const old_timesheet of old_timesheets) { + if (!old_timesheet.start_date) continue; + const new_timesheet = new_timesheets.find((ts) => ts.start_date.getTime() === old_timesheet.start_date!.getTime()); + if (!new_timesheet) { + console.warn(`No new timesheet ${new_timesheet} matching legacy timesheet ${old_timesheet.id}`); + continue; + } + const old_shifts = await prisma_legacy.shifts.findMany({ + where: { time_sheet_id: old_timesheet.id }, + select: { + time_sheet_id: true, + code: true, + type: true, + date: true, + start_time: true, + end_time: true, + comment: true, + status: true, + }, + }); + await createManyNewShifts(new_timesheet.id, old_shifts); + } + } + await prisma_legacy.$disconnect(); + await prisma.$disconnect(); +} + +const findOneNewEmployee = async (id: number): Promise => { + const new_employee = await prisma.employees.findUnique({ + where: { id: id }, + select: { + id: true, + company_code: true, + external_payroll_id: true, + }, + }); + if (!new_employee) throw new Error(`New Employee with id ${id} not found`) + return new_employee; +} + +const findOneOldEmployee = async (new_employee: NewEmployee): Promise => { + const old_employee = await prisma_legacy.employees.findFirst({ + where: { + company: new_employee.company_code, + employee_number: new_employee.external_payroll_id.toString(), + }, + select: { + id: true, + }, + }); + if (!old_employee) throw new Error(`Old Employee not found`); + return old_employee.id; +} + +const findManyOldTimesheets = async (old_employee_id: string) => { + const old_timesheets = await prisma_legacy.time_sheets.findMany({ + where: { employee_id: old_employee_id }, + select: { id: true, start_date: true, status: true } + }); + return old_timesheets; +} + +const findManyNewTimesheets = async (employee_id: number) => { + const timesheets = await prisma.timesheets.findMany({ + where: { employee_id: employee_id }, + select: { id: true, start_date: true } + }) + return timesheets; +} + +const createManyNewShifts = async (timesheet_id: number, old_shifts: OldShifts[]) => { + for (const old_shift of old_shifts) { + let is_remote = true; + + const start = toHHmmfromLegacyTimestamp(old_shift.start_time); + if (old_shift.start_time == null || !start) { + console.warn(`Shift start invalid ${old_shift.start_time}`); + continue; + } + + const end = toHHmmfromLegacyTimestamp(old_shift.end_time); + if (old_shift.end_time == null || !end) { + console.warn(`Shift end invalid ${old_shift.end_time}`); + continue; + } + + if (old_shift.date == null) { + console.warn(`Shift date invalid ${old_shift.date}`); + continue; + } + + if (old_shift.status == null) { + console.warn(`status null for legacy shift ${old_shift}`); + continue; + } + if (old_shift.type == null) { + console.warn(`type null for legacy shift ${old_shift.type}`); + continue; + } + + if (old_shift.type === 'office') { + is_remote = false; + } + + if (old_shift.code == null) { + console.warn(`Code null for legacy shift ${old_shift.code}`); + continue; + } + const bank_code_id = await findBankCodeIdUsingOldCode(old_shift.code); + try { + await prisma.shifts.create({ + // where: { unique_ts_id_date_start_time: { + // timesheet_id, + // date: old_shift.date, + // start_time: toDateFromHHmm(start) }}, + // update: { + // start_time: toDateFromHHmm(start), + // end_time: toDateFromHHmm(end), + // comment: old_shift.comment, + // is_approved: old_shift.status, + // is_remote: is_remote, + // bank_code_id: bank_code_id, + // }, + data: { + date: old_shift.date, + start_time: toDateFromHHmm(start), + end_time: toDateFromHHmm(end), + comment: old_shift.comment, + is_approved: old_shift.status, + is_remote: is_remote, + timesheet_id: timesheet_id, + bank_code_id: bank_code_id, + }, + }); + } catch (error) { + console.log('An error occured during shifts creation'); + } + } +} + +const toHHmmfromLegacyTimestamp = (value: bigint | null): string | null => { + if (value == null) return null; + const date = new Date(Number(value)); + const hh = String(date.getHours()).padStart(2, '0'); + const mm = String(date.getMinutes()).padStart(2, '0'); + return `${hh}:${mm}`; +} + +const toDateFromHHmm = (hhmm: string): Date => { + const [hh, mm] = hhmm.split(':'); + const hours = Number(hh); + const minutes = Number(mm); + return new Date(Date.UTC(1970, 0, 1, hours, minutes, 0, 0)); +} + +const findBankCodeIdUsingOldCode = async (code: string): Promise => { + if (code === 'G700') { + code = 'G104'; + } else if (code === 'G140') { + code = 'G56' + } + const bank_code = await prisma.bankCodes.findFirst({ + where: { bank_code: code }, + select: { id: true, bank_code: true }, + }); + if (!bank_code) throw new Error(`Bank_code_id not found for Code ${code}`) + return bank_code.id; +} diff --git a/scripts/migrate-timesheets.ts b/scripts/migrate-timesheets.ts new file mode 100644 index 0000000..da25839 --- /dev/null +++ b/scripts/migrate-timesheets.ts @@ -0,0 +1,118 @@ +import { PrismaClient as Prisma } from "@prisma/client"; +import { PrismaClient as PrismaLegacy } from "@prisma/client-legacy" +import { toStringFromDate } from "src/common/utils/date-utils"; + + +type NewEmployee = { + id: number; + company_code: number; + external_payroll_id: number; +} + +type OldTimesheets = { + id: string; + start_date: Date | null; + status: boolean | null; +} + +const prisma_legacy = new PrismaLegacy({}); +const prisma_new = new Prisma({}); + +export const extractOldTimesheets = async () => { + for (let id = 1; id <= 61; id++) { + const new_employee = await findOneNewEmployee(id); + console.log(`Employee ${id} found in new DB ${new_employee.external_payroll_id}`); + + const old_employee_id = await findOneOldEmployee(new_employee); + console.log(`Employee ${new_employee.id} found in old DB`); + + const old_timesheets = await findManyOldTimesheets(old_employee_id); + console.log(` ${old_timesheets.length} Timesheets for employee ${old_employee_id}/${new_employee.id} found in old DB`); + + await createManyNewTimesheets(old_timesheets, new_employee); + console.log(`${old_timesheets.length} New Timesheets created in new DB for employee ${new_employee.id}`); + } + await prisma_legacy.$disconnect(); + await prisma_new.$disconnect(); +} + +const findOneNewEmployee = async (id: number): Promise => { + const new_employee = await prisma_new.employees.findUnique({ + where: { id: id }, + select: { + id: true, + company_code: true, + external_payroll_id: true, + }, + }); + if (!new_employee) throw new Error(`New Employee with id ${id} not found`) + return new_employee; +} + +const findOneOldEmployee = async (new_employee: NewEmployee): Promise => { + const employee_number = new_employee.external_payroll_id.toString() + const old_employee = await prisma_legacy.employees.findFirst({ + where: { + company: new_employee.company_code, + employee_number: employee_number, + }, + select: { + id: true, + }, + }); + if (!old_employee) throw new Error(`Old Employee not found`); + return old_employee.id; +} + +const findManyOldTimesheets = async (old_employee_id: string) => { + const old_timesheets = await prisma_legacy.time_sheets.findMany({ + where: { employee_id: old_employee_id }, + select: { id: true, start_date: true, status: true } + }); + if (!old_timesheets) throw new Error(`old Timesheets not found for employee_id ${old_employee_id}`) + return old_timesheets; +} + +const createManyNewTimesheets = async (old_timesheets: OldTimesheets[], new_employee: NewEmployee) => { + for (const timesheet of old_timesheets) { + if (timesheet.start_date == null) { + console.warn(`start_date invalid for legacy timesheet ${timesheet.id}`); + continue; + } + if (timesheet.status == null) { + console.warn(`status null for legacy timesheet ${timesheet.id}`); + continue; + } + + try { + const new_timesheet = await prisma_new.timesheets.upsert({ + where: { employee_id_start_date: { employee_id: new_employee.id, start_date: timesheet.start_date } }, + update: { + is_approved: timesheet.status, + }, + create: { + employee_id: new_employee.id, + start_date: timesheet.start_date, + is_approved: timesheet.status, + }, + }); + if (!new_timesheet) throw new Error( + `Timesheet with start_date: ${toStringFromDate(timesheet.start_date!)} for employee ${new_employee.id} not created` + ); + } catch (error) { + throw new Error('An error occured during timesheets creation'); + } + } +} + +extractOldTimesheets() + .then(() => { + console.log("Migration completed"); + }) + .catch((error) => { + console.error("Migration failed:", error); + }) + .finally(async () => { + await prisma_legacy.$disconnect(); + await prisma_new.$disconnect(); + }); \ No newline at end of file diff --git a/scripts/migration.service.ts b/scripts/migration.service.ts new file mode 100644 index 0000000..88dcb82 --- /dev/null +++ b/scripts/migration.service.ts @@ -0,0 +1,21 @@ +import { extractOldTimesheets } from "scripts/migrate-timesheets"; +import { extractOldExpenses } from "scripts/migrate-expenses"; +import { extractOldShifts } from "scripts/migrate-shifts"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class MigrationService { + constructor() {} + + async migrateTimesheets() { + extractOldTimesheets(); + }; + + async migrateShifts() { + extractOldShifts(); + } + + async migrateExpenses() { + extractOldExpenses(); + } +} \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index e03505c..f2b98e5 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,7 @@ import { ValidationError } from 'class-validator'; import { TimeAndAttendanceModule } from 'src/time-and-attendance/time-and-attendance.module'; import { AuthenticationModule } from 'src/identity-and-account/authentication/auth.module'; import { IdentityAndAccountModule } from 'src/identity-and-account/identity-and-account.module'; +import { PrismaLegacyModule } from 'src/prisma-legacy/prisma.module'; @Module({ imports: [ @@ -20,6 +21,7 @@ import { IdentityAndAccountModule } from 'src/identity-and-account/identity-and- ScheduleModule.forRoot(), //cronjobs NotificationsModule, PrismaModule, + PrismaLegacyModule, TimeAndAttendanceModule, IdentityAndAccountModule, ], diff --git a/src/common/utils/date-utils.ts b/src/common/utils/date-utils.ts index da63283..604da75 100644 --- a/src/common/utils/date-utils.ts +++ b/src/common/utils/date-utils.ts @@ -75,7 +75,7 @@ export const toStringFromDate = (date: Date) => //converts HHmm format to string -export const toHHmmFromString = (hhmm: string): Date => { +export const toDateFromHHmm = (hhmm: string): Date => { const [hh, mm] = hhmm.split(':').map(Number); const date = new Date('1970-01-01T00:00:00.000Z'); date.setUTCHours(hh, mm, 0, 0); diff --git a/src/main.ts b/src/main.ts index fbe5afd..5b9134b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,6 +18,10 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { writeFileSync } from 'fs'; import * as session from 'express-session'; import * as passport from 'passport'; +// import { extractOldTimesheets } from 'scripts/migrate-timesheets'; +import { extractOldShifts } from 'scripts/migrate-shifts'; +import { extractOldTimesheets } from 'scripts/migrate-timesheets'; +import { extractOldExpenses } from 'scripts/migrate-expenses'; const SESSION_TOKEN_DURATION_MINUTES = 180 @@ -89,5 +93,11 @@ async function bootstrap() { await ensureAttachmentsTmpDir(); await app.listen(process.env.PORT ?? 3000); + + + // migration function calls + // await extractOldTimesheets(); + // await extractOldShifts(); + // await extractOldExpenses(); } bootstrap(); diff --git a/src/prisma-legacy/prisma.module.ts b/src/prisma-legacy/prisma.module.ts new file mode 100644 index 0000000..add3840 --- /dev/null +++ b/src/prisma-legacy/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaLegacyService } from './prisma.service'; + +@Global() +@Module({ + providers: [PrismaLegacyService], + exports: [PrismaLegacyService], +}) +export class PrismaLegacyModule {} diff --git a/src/prisma-legacy/prisma.service.ts b/src/prisma-legacy/prisma.service.ts new file mode 100644 index 0000000..2e8fe8e --- /dev/null +++ b/src/prisma-legacy/prisma.service.ts @@ -0,0 +1,18 @@ +import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { PrismaClient as PrismaLegacyClient } from '@prisma/client'; + +//Gestion des connections à la DB + +@Injectable() +export class PrismaLegacyService + extends PrismaLegacyClient + implements OnModuleInit, OnModuleDestroy +{ + async onModuleInit() { + await this.$connect(); + } + + async onModuleDestroy() { + await this.$disconnect(); + } +} diff --git a/src/time-and-attendance/shifts/services/shifts-create.service.ts b/src/time-and-attendance/shifts/services/shifts-create.service.ts index 060a761..7c8ec2c 100644 --- a/src/time-and-attendance/shifts/services/shifts-create.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-create.service.ts @@ -5,7 +5,7 @@ import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; import { PrismaService } from "src/prisma/prisma.service"; import { Result } from "src/common/errors/result-error.factory"; -import { toStringFromHHmm, toStringFromDate, toDateFromString, overlaps, toHHmmFromString } from "src/common/utils/date-utils"; +import { toStringFromHHmm, toStringFromDate, toDateFromString, overlaps, toDateFromHHmm } from "src/common/utils/date-utils"; import { ShiftDto } from "src/time-and-attendance/shifts/dtos/shift-create.dto"; @Injectable() @@ -135,8 +135,8 @@ export class ShiftsCreateService { //TODO: validate date and time to ensure "banana" is not accepted using an if statement and a REGEX const date = toDateFromString(dto.date); - const start_time = toHHmmFromString(dto.start_time); - const end_time = toHHmmFromString(dto.end_time); + const start_time = toDateFromHHmm(dto.start_time); + const end_time = toDateFromHHmm(dto.end_time); return { success: true, data: { date, start_time, end_time, bank_code_id: bank_code_id.data } }; } diff --git a/src/time-and-attendance/shifts/services/shifts-update-delete.service.ts b/src/time-and-attendance/shifts/services/shifts-update-delete.service.ts index 85f4d87..ed602b0 100644 --- a/src/time-and-attendance/shifts/services/shifts-update-delete.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-update-delete.service.ts @@ -5,7 +5,7 @@ import { Injectable } from "@nestjs/common"; import { Normalized } from "src/time-and-attendance/utils/type.utils"; import { Result } from "src/common/errors/result-error.factory"; import { EmployeeTimesheetResolver } from "src/common/mappers/timesheet.mapper"; -import { toDateFromString, toStringFromHHmm, toStringFromDate, toHHmmFromString, overlaps } from "src/common/utils/date-utils"; +import { toDateFromString, toStringFromHHmm, toStringFromDate, toDateFromHHmm, overlaps } from "src/common/utils/date-utils"; import { ShiftDto } from "src/time-and-attendance/shifts/dtos/shift-create.dto"; @Injectable() @@ -145,8 +145,8 @@ export class ShiftsUpdateDeleteService { success: true, data: { date: toDateFromString(dto.date), - start_time: toHHmmFromString(dto.start_time), - end_time: toHHmmFromString(dto.end_time), + start_time: toDateFromHHmm(dto.start_time), + end_time: toDateFromHHmm(dto.end_time), bank_code_id: bank_code_id.data } }; @@ -160,8 +160,8 @@ export class ShiftsUpdateDeleteService { if (shift_a.date !== shift_b.date || shift_a.id === shift_b.id) continue; const has_overlap = overlaps( - { start: toHHmmFromString(shift_a.start_time), end: toHHmmFromString(shift_a.end_time) }, - { start: toHHmmFromString(shift_b.start_time), end: toHHmmFromString(shift_b.end_time) }, + { start: toDateFromHHmm(shift_a.start_time), end: toDateFromHHmm(shift_a.end_time) }, + { start: toDateFromHHmm(shift_b.start_time), end: toDateFromHHmm(shift_b.end_time) }, ); if (has_overlap) return { success: false, error: `SHIFT_OVERLAP` }; } diff --git a/src/~misc_deprecated-files/expenses-query.service.ts b/src/~misc_deprecated-files/expenses-query.service.ts index d95f1a7..6eafba2 100644 --- a/src/~misc_deprecated-files/expenses-query.service.ts +++ b/src/~misc_deprecated-files/expenses-query.service.ts @@ -124,7 +124,7 @@ // // } // // async findAll(filters: SearchExpensesDto): Promise { -// // const where = buildPrismaWhere(filters); + // const where = buildPrismaWhere(filters); // // const expenses = await this.prisma.expenses.findMany({ where }) // // return expenses; // // } diff --git a/test/customers.e2e-spec.ts b/test/customers.e2e-spec.ts index 78d52d9..98172db 100644 --- a/test/customers.e2e-spec.ts +++ b/test/customers.e2e-spec.ts @@ -1,125 +1,125 @@ -import * as request from 'supertest'; -import { INestApplication } from '@nestjs/common'; -import { createApp } from './utils/testing-app'; -import { PrismaService } from 'src/prisma/prisma.service'; +// import * as request from 'supertest'; +// import { INestApplication } from '@nestjs/common'; +// import { createApp } from './utils/testing-app'; +// import { PrismaService } from 'src/prisma/prisma.service'; -type CustomerPayload = { - user_id?: string; - first_name: string; - last_name: string; - email?: string; - phone_number: number; - residence?: string; - invoice_id: number; -}; +// type CustomerPayload = { +// user_id?: string; +// first_name: string; +// last_name: string; +// email?: string; +// phone_number: number; +// residence?: string; +// invoice_id: number; +// }; -const BASE = '/customers'; +// const BASE = '/customers'; -const uniqueEmail = () => - `customer+${Date.now()}_${Math.random().toString(36).slice(2,8)}@test.local`; -const uniquePhone = () => - Math.floor(100_000_000 + Math.random() * 900_000_000); +// const uniqueEmail = () => +// `customer+${Date.now()}_${Math.random().toString(36).slice(2,8)}@test.local`; +// const uniquePhone = () => +// Math.floor(100_000_000 + Math.random() * 900_000_000); -function makeCustomerPayload(overrides: Partial = {}): CustomerPayload { - return { - first_name: 'Gandalf', - last_name: 'TheGray', - email: uniqueEmail(), - phone_number: uniquePhone(), - residence: '1 Ringbearer’s Way, Mount Doom, ME', - invoice_id: Math.floor(1_000_000 + Math.random() * 9_000_000), - ...overrides, - }; -} +// function makeCustomerPayload(overrides: Partial = {}): CustomerPayload { +// return { +// first_name: 'Gandalf', +// last_name: 'TheGray', +// email: uniqueEmail(), +// phone_number: uniquePhone(), +// residence: '1 Ringbearer’s Way, Mount Doom, ME', +// invoice_id: Math.floor(1_000_000 + Math.random() * 9_000_000), +// ...overrides, +// }; +// } -describe('Customers (e2e) — autonome', () => { - let app: INestApplication; - let prisma: PrismaService; - let createdId: number | null = null; +// describe('Customers (e2e) — autonome', () => { +// let app: INestApplication; +// let prisma: PrismaService; +// let createdId: number | null = null; - beforeAll(async () => { - app = await createApp(); - prisma = app.get(PrismaService); - }); +// beforeAll(async () => { +// app = await createApp(); +// prisma = app.get(PrismaService); +// }); - afterAll(async () => { - if (createdId) { - try { await prisma.customers.delete({ where: { id: createdId } }); } catch {} - } - await app.close(); - await prisma.$disconnect(); - }); +// afterAll(async () => { +// if (createdId) { +// try { await prisma.customers.delete({ where: { id: createdId } }); } catch {} +// } +// await app.close(); +// await prisma.$disconnect(); +// }); - it(`GET ${BASE} → 200 (array)`, async () => { - const res = await request(app.getHttpServer()).get(BASE); - expect(res.status).toBe(200); - expect(Array.isArray(res.body)).toBe(true); - }); +// it(`GET ${BASE} → 200 (array)`, async () => { +// const res = await request(app.getHttpServer()).get(BASE); +// expect(res.status).toBe(200); +// expect(Array.isArray(res.body)).toBe(true); +// }); - it(`POST ${BASE} (valid) → 201 puis GET /:id → 200`, async () => { - const payload = makeCustomerPayload(); +// it(`POST ${BASE} (valid) → 201 puis GET /:id → 200`, async () => { +// const payload = makeCustomerPayload(); - const createRes = await request(app.getHttpServer()).post(BASE).send(payload); - if (createRes.status !== 201) { +// const createRes = await request(app.getHttpServer()).post(BASE).send(payload); +// if (createRes.status !== 201) { - console.log('Create error:', createRes.body || createRes.text); - } - expect(createRes.status).toBe(201); +// console.log('Create error:', createRes.body || createRes.text); +// } +// expect(createRes.status).toBe(201); - expect(createRes.body).toEqual( - expect.objectContaining({ - id: expect.any(Number), - user_id: expect.any(String), - invoice_id: payload.invoice_id, - }) - ); - expect(createRes.body.user_id).toMatch(/^[0-9a-fA-F-]{36}$/); +// expect(createRes.body).toEqual( +// expect.objectContaining({ +// id: expect.any(Number), +// user_id: expect.any(String), +// invoice_id: payload.invoice_id, +// }) +// ); +// expect(createRes.body.user_id).toMatch(/^[0-9a-fA-F-]{36}$/); - createdId = createRes.body.id; +// createdId = createRes.body.id; - const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`); - expect(getRes.status).toBe(200); - expect(getRes.body).toEqual(expect.objectContaining({ id: createdId })); - }); +// const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`); +// expect(getRes.status).toBe(200); +// expect(getRes.body).toEqual(expect.objectContaining({ id: createdId })); +// }); - it(`PATCH ${BASE}/:id → 200 (first_name mis à jour)`, async () => { - if (!createdId) { - const create = await request(app.getHttpServer()).post(BASE).send(makeCustomerPayload()); - expect(create.status).toBe(201); - createdId = create.body.id; - } +// it(`PATCH ${BASE}/:id → 200 (first_name mis à jour)`, async () => { +// if (!createdId) { +// const create = await request(app.getHttpServer()).post(BASE).send(makeCustomerPayload()); +// expect(create.status).toBe(201); +// createdId = create.body.id; +// } - const patchRes = await request(app.getHttpServer()) - .patch(`${BASE}/${createdId}`) - .send({ first_name: 'Mithrandir' }); - expect([200, 204]).toContain(patchRes.status); +// const patchRes = await request(app.getHttpServer()) +// .patch(`${BASE}/${createdId}`) +// .send({ first_name: 'Mithrandir' }); +// expect([200, 204]).toContain(patchRes.status); - const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`); - expect(getRes.status).toBe(200); - expect(getRes.body.first_name ?? 'Mithrandir').toBe('Mithrandir'); - }); +// const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`); +// expect(getRes.status).toBe(200); +// expect(getRes.body.first_name ?? 'Mithrandir').toBe('Mithrandir'); +// }); - it(`GET ${BASE}/:id (not found) → 404/400`, async () => { - const res = await request(app.getHttpServer()).get(`${BASE}/999999`); - expect([404, 400]).toContain(res.status); - }); +// it(`GET ${BASE}/:id (not found) → 404/400`, async () => { +// const res = await request(app.getHttpServer()).get(`${BASE}/999999`); +// expect([404, 400]).toContain(res.status); +// }); - it(`POST ${BASE} (invalid payload) → 400`, async () => { - const res = await request(app.getHttpServer()).post(BASE).send({}); - expect(res.status).toBeGreaterThanOrEqual(400); - expect(res.status).toBeLessThan(500); - }); +// it(`POST ${BASE} (invalid payload) → 400`, async () => { +// const res = await request(app.getHttpServer()).post(BASE).send({}); +// expect(res.status).toBeGreaterThanOrEqual(400); +// expect(res.status).toBeLessThan(500); +// }); - it(`DELETE ${BASE}/:id → 200/204`, async () => { - let id = createdId; - if (!id) { - const create = await request(app.getHttpServer()).post(BASE).send(makeCustomerPayload()); - expect(create.status).toBe(201); - id = create.body.id; - } +// it(`DELETE ${BASE}/:id → 200/204`, async () => { +// let id = createdId; +// if (!id) { +// const create = await request(app.getHttpServer()).post(BASE).send(makeCustomerPayload()); +// expect(create.status).toBe(201); +// id = create.body.id; +// } - const del = await request(app.getHttpServer()).delete(`${BASE}/${id}`); - expect([200, 204]).toContain(del.status); - if (createdId === id) createdId = null; - }); -}); +// const del = await request(app.getHttpServer()).delete(`${BASE}/${id}`); +// expect([200, 204]).toContain(del.status); +// if (createdId === id) createdId = null; +// }); +// }); diff --git a/test/pay-periods-approval.e2e-spec.ts b/test/pay-periods-approval.e2e-spec.ts index 0e780d5..e061c16 100644 --- a/test/pay-periods-approval.e2e-spec.ts +++ b/test/pay-periods-approval.e2e-spec.ts @@ -1,143 +1,143 @@ -// test/pay-periods-approval.e2e-spec.ts -const supertest = require('supertest'); -import { INestApplication } from '@nestjs/common'; -import { PrismaService } from 'src/prisma/prisma.service'; -import { createApp } from './utils/testing-app'; -import { makeEmployee } from './factories/employee.factory'; -import { makeTimesheet } from './factories/timesheet.factory'; +// // test/pay-periods-approval.e2e-spec.ts +// const supertest = require('supertest'); +// import { INestApplication } from '@nestjs/common'; +// import { PrismaService } from 'src/prisma/prisma.service'; +// import { createApp } from './utils/testing-app'; +// import { makeEmployee } from './factories/employee.factory'; +// import { makeTimesheet } from './factories/timesheet.factory'; -describe('PayPeriods approval (e2e)', () => { - const BASE = '/pay-periods'; - let app: INestApplication; - let prisma: PrismaService; +// describe('PayPeriods approval (e2e)', () => { +// const BASE = '/pay-periods'; +// let app: INestApplication; +// let prisma: PrismaService; - let periodYear: number; - let periodNumber: number; +// let periodYear: number; +// let periodNumber: number; - let employeeId: number; - let timesheetId: number; - let shiftId: number; - let expenseId: number; +// let employeeId: number; +// let timesheetId: number; +// let shiftId: number; +// let expenseId: number; - const isoDay = (d: Date) => - new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())).toISOString(); - const isoTime = (h: number, m = 0) => - new Date(Date.UTC(1970, 0, 1, h, m, 0)).toISOString(); +// const isoDay = (d: Date) => +// new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())).toISOString(); +// const isoTime = (h: number, m = 0) => +// new Date(Date.UTC(1970, 0, 1, h, m, 0)).toISOString(); - beforeAll(async () => { - app = await createApp(); - prisma = app.get(PrismaService); +// beforeAll(async () => { +// app = await createApp(); +// prisma = app.get(PrismaService); - // 1) Récupère un pay period existant - const period = await prisma.payPeriods.findFirst({ orderBy: { period_number: 'asc' } }); - if (!period) throw new Error('Aucun pay period en DB (seed requis).'); +// // 1) Récupère un pay period existant +// const period = await prisma.payPeriods.findFirst({ orderBy: { period_number: 'asc' } }); +// if (!period) throw new Error('Aucun pay period en DB (seed requis).'); - periodYear = period.year; - periodNumber = period.period_number; +// periodYear = period.year; +// periodNumber = period.period_number; - // 2) Crée un employé + timesheet (non approuvé) - const empRes = await supertest(app.getHttpServer()) - .post('/employees') - .send(makeEmployee()); - if (empRes.status !== 201) { - // eslint-disable-next-line no-console - console.warn('Create employee error:', empRes.body || empRes.text); - throw new Error('Impossible de créer un employé pour le test pay-periods.'); - } - employeeId = empRes.body.id; +// // 2) Crée un employé + timesheet (non approuvé) +// const empRes = await supertest(app.getHttpServer()) +// .post('/employees') +// .send(makeEmployee()); +// if (empRes.status !== 201) { +// // eslint-disable-next-line no-console +// console.warn('Create employee error:', empRes.body || empRes.text); +// throw new Error('Impossible de créer un employé pour le test pay-periods.'); +// } +// employeeId = empRes.body.id; - const tsRes = await supertest(app.getHttpServer()) - .post('/timesheets') - .send(makeTimesheet(employeeId, { is_approved: false })); - if (tsRes.status !== 201) { - // eslint-disable-next-line no-console - console.warn('Create timesheet error:', tsRes.body || tsRes.text); - throw new Error('Impossible de créer un timesheet pour le test pay-periods.'); - } - timesheetId = tsRes.body.id; +// const tsRes = await supertest(app.getHttpServer()) +// .post('/timesheets') +// .send(makeTimesheet(employeeId, { is_approved: false })); +// if (tsRes.status !== 201) { +// // eslint-disable-next-line no-console +// console.warn('Create timesheet error:', tsRes.body || tsRes.text); +// throw new Error('Impossible de créer un timesheet pour le test pay-periods.'); +// } +// timesheetId = tsRes.body.id; - // 3) Bank codes - const bcShift = await prisma.bankCodes.findFirst({ - where: { categorie: 'SHIFT' }, - select: { id: true }, - }); - if (!bcShift) throw new Error('Aucun bank code SHIFT trouvé.'); - const bcExpense = await prisma.bankCodes.findFirst({ - where: { categorie: 'EXPENSE' }, - select: { id: true }, - }); - if (!bcExpense) throw new Error('Aucun bank code EXPENSE trouvé.'); +// // 3) Bank codes +// const bcShift = await prisma.bankCodes.findFirst({ +// where: { categorie: 'SHIFT' }, +// select: { id: true }, +// }); +// if (!bcShift) throw new Error('Aucun bank code SHIFT trouvé.'); +// const bcExpense = await prisma.bankCodes.findFirst({ +// where: { categorie: 'EXPENSE' }, +// select: { id: true }, +// }); +// if (!bcExpense) throw new Error('Aucun bank code EXPENSE trouvé.'); - // 4) Crée 1 shift + 1 expense DANS la période choisie - const dateISO = isoDay(period.start_date); +// // 4) Crée 1 shift + 1 expense DANS la période choisie +// const dateISO = isoDay(period.start_date); - const shiftRes = await supertest(app.getHttpServer()) - .post('/shifts') - .send({ - timesheet_id: timesheetId, - bank_code_id: bcShift.id, - date: dateISO, - start_time: isoTime(9), - end_time: isoTime(17), - description: 'PP approval shift', - }); - if (shiftRes.status !== 201) { - // eslint-disable-next-line no-console - console.warn('Create shift error:', shiftRes.body || shiftRes.text); - throw new Error('Création shift échouée.'); - } - shiftId = shiftRes.body.id; +// const shiftRes = await supertest(app.getHttpServer()) +// .post('/shifts') +// .send({ +// timesheet_id: timesheetId, +// bank_code_id: bcShift.id, +// date: dateISO, +// start_time: isoTime(9), +// end_time: isoTime(17), +// description: 'PP approval shift', +// }); +// if (shiftRes.status !== 201) { +// // eslint-disable-next-line no-console +// console.warn('Create shift error:', shiftRes.body || shiftRes.text); +// throw new Error('Création shift échouée.'); +// } +// shiftId = shiftRes.body.id; - const expenseRes = await supertest(app.getHttpServer()) - .post('/Expenses') // <- respecte ta casse de route - .send({ - timesheet_id: timesheetId, - bank_code_id: bcExpense.id, - date: dateISO, - amount: 42, - description: 'PP approval expense', - is_approved: false, - }); - if (expenseRes.status !== 201) { - // eslint-disable-next-line no-console - console.warn('Create expense error:', expenseRes.body || expenseRes.text); - throw new Error('Création expense échouée.'); - } - expenseId = expenseRes.body.id; - }); +// const expenseRes = await supertest(app.getHttpServer()) +// .post('/Expenses') // <- respecte ta casse de route +// .send({ +// timesheet_id: timesheetId, +// bank_code_id: bcExpense.id, +// date: dateISO, +// amount: 42, +// description: 'PP approval expense', +// is_approved: false, +// }); +// if (expenseRes.status !== 201) { +// // eslint-disable-next-line no-console +// console.warn('Create expense error:', expenseRes.body || expenseRes.text); +// throw new Error('Création expense échouée.'); +// } +// expenseId = expenseRes.body.id; +// }); - afterAll(async () => { - await app.close(); - await prisma.$disconnect(); - }); +// afterAll(async () => { +// await app.close(); +// await prisma.$disconnect(); +// }); - it(`PATCH ${BASE}/:year/:periodNumber/approval → 200 (cascade approval)`, async () => { - const res = await supertest(app.getHttpServer()) - .patch(`${BASE}/${periodYear}/${periodNumber}/approval`) - .send(); // aucun body requis par ton contrôleur - expect([200, 204]).toContain(res.status); - if (res.body?.message) { - expect(String(res.body.message)).toContain(`${periodYear}-${periodNumber}`); - } +// it(`PATCH ${BASE}/:year/:periodNumber/approval → 200 (cascade approval)`, async () => { +// const res = await supertest(app.getHttpServer()) +// .patch(`${BASE}/${periodYear}/${periodNumber}/approval`) +// .send(); // aucun body requis par ton contrôleur +// expect([200, 204]).toContain(res.status); +// if (res.body?.message) { +// expect(String(res.body.message)).toContain(`${periodYear}-${periodNumber}`); +// } - // Vérifie cascade: - const tsCheck = await supertest(app.getHttpServer()).get(`/timesheets/${timesheetId}`); - expect(tsCheck.status).toBe(200); - expect(tsCheck.body?.is_approved).toBe(true); +// // Vérifie cascade: +// const tsCheck = await supertest(app.getHttpServer()).get(`/timesheets/${timesheetId}`); +// expect(tsCheck.status).toBe(200); +// expect(tsCheck.body?.is_approved).toBe(true); - const shiftCheck = await supertest(app.getHttpServer()).get(`/shifts/${shiftId}`); - expect(shiftCheck.status).toBe(200); - expect(shiftCheck.body?.is_approved).toBe(true); +// const shiftCheck = await supertest(app.getHttpServer()).get(`/shifts/${shiftId}`); +// expect(shiftCheck.status).toBe(200); +// expect(shiftCheck.body?.is_approved).toBe(true); - const expCheck = await supertest(app.getHttpServer()).get(`/Expenses/${expenseId}`); - expect(expCheck.status).toBe(200); - expect(expCheck.body?.is_approved).toBe(true); - }); +// const expCheck = await supertest(app.getHttpServer()).get(`/Expenses/${expenseId}`); +// expect(expCheck.status).toBe(200); +// expect(expCheck.body?.is_approved).toBe(true); +// }); - it(`PATCH ${BASE}/2099/999/approval → 404 (period not found)`, async () => { - const bad = await supertest(app.getHttpServer()) - .patch(`${BASE}/2099/999/approval`) - .send(); - expect(bad.status).toBe(404); - }); -}); +// it(`PATCH ${BASE}/2099/999/approval → 404 (period not found)`, async () => { +// const bad = await supertest(app.getHttpServer()) +// .patch(`${BASE}/2099/999/approval`) +// .send(); +// expect(bad.status).toBe(404); +// }); +// }); From 58c4b22f0fa7b37ac4263ab736b2c9796b996f88 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Mon, 24 Nov 2025 16:36:30 -0500 Subject: [PATCH 2/9] fix(migration): minor fixes and modifications to DB scripts --- scripts/migrate-expenses.ts | 3 ++- scripts/migrate-shifts.ts | 10 +++++----- scripts/migration.service.ts | 12 ++++++------ src/main.ts | 3 +-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/scripts/migrate-expenses.ts b/scripts/migrate-expenses.ts index 6c20eb6..215f2bb 100644 --- a/scripts/migrate-expenses.ts +++ b/scripts/migrate-expenses.ts @@ -116,8 +116,9 @@ const createManyNewExpenses = async (timesheet_id: number, old_expenses: OldExpe mileage = old_expense.value!; amount = mileage * 0.72; } - if (mileage < 0) { + if (mileage < 0 || amount < 0) { console.warn(`expense of value less than '0' found`) + continue; } if (old_expense.date == null) { diff --git a/scripts/migrate-shifts.ts b/scripts/migrate-shifts.ts index 5b966c4..a3dfce5 100644 --- a/scripts/migrate-shifts.ts +++ b/scripts/migrate-shifts.ts @@ -22,13 +22,13 @@ type OldShifts = { } export const extractOldShifts = async () => { - for (let id = 1; id <= 61; id++) { + // for (let id = 1; id <= 61; id++) { console.log(`Start of shift migration ***************************************************************`); - const new_employee = await findOneNewEmployee(id); - console.log(`Employee ${id} found in new DB`); + const new_employee = await findOneNewEmployee(50); + console.log(`Employee ${50} found in new DB`); const new_timesheets = await findManyNewTimesheets(new_employee.id); - console.log(`New Timesheets found for employee ${id}`); + console.log(`New Timesheets found for employee ${50}`); for (const ts of new_timesheets) { console.log(`start_date = ${ts.start_date} timesheet_id = ${ts.id}`) @@ -62,7 +62,7 @@ export const extractOldShifts = async () => { }); await createManyNewShifts(new_timesheet.id, old_shifts); } - } + // } await prisma_legacy.$disconnect(); await prisma.$disconnect(); } diff --git a/scripts/migration.service.ts b/scripts/migration.service.ts index 88dcb82..2bb37d6 100644 --- a/scripts/migration.service.ts +++ b/scripts/migration.service.ts @@ -1,6 +1,6 @@ -import { extractOldTimesheets } from "scripts/migrate-timesheets"; -import { extractOldExpenses } from "scripts/migrate-expenses"; -import { extractOldShifts } from "scripts/migrate-shifts"; +// import { extractOldTimesheets } from "scripts/migrate-timesheets"; +// import { extractOldExpenses } from "scripts/migrate-expenses"; +// import { extractOldShifts } from "scripts/migrate-shifts"; import { Injectable } from "@nestjs/common"; @Injectable() @@ -8,14 +8,14 @@ export class MigrationService { constructor() {} async migrateTimesheets() { - extractOldTimesheets(); + // extractOldTimesheets(); }; async migrateShifts() { - extractOldShifts(); + // extractOldShifts(); } async migrateExpenses() { - extractOldExpenses(); + // extractOldExpenses(); } } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 5b9134b..6dc2a03 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,7 +18,6 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { writeFileSync } from 'fs'; import * as session from 'express-session'; import * as passport from 'passport'; -// import { extractOldTimesheets } from 'scripts/migrate-timesheets'; import { extractOldShifts } from 'scripts/migrate-shifts'; import { extractOldTimesheets } from 'scripts/migrate-timesheets'; import { extractOldExpenses } from 'scripts/migrate-expenses'; @@ -98,6 +97,6 @@ async function bootstrap() { // migration function calls // await extractOldTimesheets(); // await extractOldShifts(); - // await extractOldExpenses(); + await extractOldExpenses(); } bootstrap(); From 35665d49dd1a53b7f9354af191664c667147147a Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Mon, 24 Nov 2025 16:42:27 -0500 Subject: [PATCH 3/9] fix(migration): commented extractionOldExpenses from main.ts --- src/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 6dc2a03..7228b76 100644 --- a/src/main.ts +++ b/src/main.ts @@ -97,6 +97,6 @@ async function bootstrap() { // migration function calls // await extractOldTimesheets(); // await extractOldShifts(); - await extractOldExpenses(); + // await extractOldExpenses(); } bootstrap(); From c5c96cce22995fd2acece2fc7269b5cf091bdb2c Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 25 Nov 2025 16:32:20 -0500 Subject: [PATCH 4/9] feat(schedulePresets): ajusted the create function. added validation of the name and overlaps checking --- .../controller/schedule-presets.controller.ts | 114 ++--- .../dtos/create-schedule-preset-shifts.dto.ts | 31 +- .../dtos/create-schedule-presets.dto.ts | 18 +- .../schedule-presets-apply.service.ts | 5 +- .../schedule-presets-upsert.service.ts | 423 +++++++++--------- 5 files changed, 291 insertions(+), 300 deletions(-) diff --git a/src/time-and-attendance/schedule-presets/controller/schedule-presets.controller.ts b/src/time-and-attendance/schedule-presets/controller/schedule-presets.controller.ts index 1bc6630..3207f18 100644 --- a/src/time-and-attendance/schedule-presets/controller/schedule-presets.controller.ts +++ b/src/time-and-attendance/schedule-presets/controller/schedule-presets.controller.ts @@ -1,64 +1,66 @@ -// import { Controller, Param, Query, Body, Get, Post, ParseIntPipe, Delete, Patch, Req } from "@nestjs/common"; -// import { RolesAllowed } from "src/common/decorators/roles.decorators"; -// import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes"; -// import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; +import { Controller, Param, Query, Body, Get, Post, ParseIntPipe, Delete, Patch, Req } from "@nestjs/common"; +import { RolesAllowed } from "src/common/decorators/roles.decorators"; +import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes"; +import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; // import { SchedulePresetsUpdateDto } from "src/time-and-attendance/schedule-presets/dtos/update-schedule-presets.dto"; -// import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service"; -// import { SchedulePresetsGetService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-get.service"; -// import { SchedulePresetsUpsertService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service"; +import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service"; +import { SchedulePresetsGetService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-get.service"; +import { SchedulePresetsUpsertService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service"; -// @Controller('schedule-presets') -// @RolesAllowed(...GLOBAL_CONTROLLER_ROLES) -// export class SchedulePresetsController { -// constructor( -// private readonly upsertService: SchedulePresetsUpsertService, -// private readonly getService: SchedulePresetsGetService, -// private readonly applyPresetsService: SchedulePresetsApplyService, -// ) { } +@Controller('schedule-presets') +@RolesAllowed(...GLOBAL_CONTROLLER_ROLES) +export class SchedulePresetsController { + constructor( + private readonly upsertService: SchedulePresetsUpsertService, + private readonly getService: SchedulePresetsGetService, + private readonly applyPresetsService: SchedulePresetsApplyService, + ) { } -// //used to create a schedule preset -// @Post('create') -// @RolesAllowed(...MANAGER_ROLES) -// async createPreset(@Req() req, @Body() dto: SchedulePresetsDto) { -// const email = req.user?.email; -// return await this.upsertService.createPreset(email, dto); -// } + // used to create a schedule preset + @Post('create') + @RolesAllowed(...MANAGER_ROLES) + async createPreset(@Req() req, @Body() dto: SchedulePresetsDto) { + const email = req.user?.email; + return await this.upsertService.createPreset(email, dto); + } -// // //used to update an already existing schedule preset -// // @Patch('update/:preset_id') -// // @RolesAllowed(...MANAGER_ROLES) -// // async updatePreset( -// // @Param('preset_id', ParseIntPipe) preset_id: number, -// // @Body() dto: SchedulePresetsUpdateDto -// // ) { -// // return await this.upsertService.updatePreset(preset_id, dto); -// // } + // //used to update an already existing schedule preset + // @Patch('update/:preset_id') + // @RolesAllowed(...MANAGER_ROLES) + // async updatePreset( + // @Param('preset_id', ParseIntPipe) preset_id: number, + // @Body() dto: SchedulePresetsUpdateDto + // ) { + // return await this.upsertService.updatePreset(preset_id, dto); + // } -// //used to delete a schedule preset -// @Delete('delete/:preset_id') -// @RolesAllowed(...MANAGER_ROLES) -// async deletePreset( -// @Param('preset_id', ParseIntPipe) preset_id: number) { -// return await this.upsertService.deletePreset(preset_id); -// } + //used to delete a schedule preset + // @Delete('delete/:preset_id') + // @RolesAllowed(...MANAGER_ROLES) + // async deletePreset( + // @Param('preset_id', ParseIntPipe) preset_id: number) { + // return await this.upsertService.deletePreset(preset_id); + // } -// //used to show the list of available schedule presets -// @Get('find-list') -// @RolesAllowed(...MANAGER_ROLES) -// async findListById(@Req() req) { -// const email = req.user?.email; -// return this.getService.getSchedulePresets(email); -// } + //used to show the list of available schedule presets + @Get('find-list') + @RolesAllowed(...MANAGER_ROLES) + async findListById( + @Req() req + ) { + const email = req.user?.email; + return this.getService.getSchedulePresets(email); + } -// //used to apply a preset to a timesheet -// @Post('apply-presets') -// async applyPresets( -// @Req() req, -// @Body('preset') preset_id: number, -// @Body('start') start_date: string -// ) { -// const email = req.user?.email; -// return this.applyPresetsService.applyToTimesheet(email, preset_id, start_date); -// } -// } \ No newline at end of file + //used to apply a preset to a timesheet + @Post('apply-presets') + async applyPresets( + @Req() req, + @Body('preset') preset_id: number, + @Body('start') start_date: string + ) { + const email = req.user?.email; + return this.applyPresetsService.applyToTimesheet(email, preset_id, start_date); + } +} \ No newline at end of file diff --git a/src/time-and-attendance/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts b/src/time-and-attendance/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts index 34787da..117e331 100644 --- a/src/time-and-attendance/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts +++ b/src/time-and-attendance/schedule-presets/dtos/create-schedule-preset-shifts.dto.ts @@ -3,28 +3,11 @@ import { HH_MM_REGEX } from "src/common/utils/constants.utils"; import { Weekday } from "@prisma/client"; export class SchedulePresetShiftsDto { - @IsEnum(Weekday) - week_day!: Weekday; - - @IsInt() - preset_id!: number; - - @IsInt() - @Min(1) - sort_order!: number; - - @IsString() - type!: string; - - @IsString() - @Matches(HH_MM_REGEX) - start_time!: string; - - @IsString() - @Matches(HH_MM_REGEX) - end_time!: string; - - @IsOptional() - @IsBoolean() - is_remote?: boolean; + @IsInt() preset_id!: number; + @IsEnum(Weekday) week_day!: Weekday; + @IsInt() @Min(1) sort_order!: number; + @IsString() type!: string; + @IsString() @Matches(HH_MM_REGEX) start_time!: string; + @IsString() @Matches(HH_MM_REGEX) end_time!: string; + @IsOptional() @IsBoolean() is_remote?: boolean; } \ No newline at end of file diff --git a/src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto.ts b/src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto.ts index 7064ee4..7c41ff0 100644 --- a/src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto.ts +++ b/src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto.ts @@ -2,18 +2,8 @@ import { ArrayMinSize, IsArray, IsBoolean, IsInt, IsOptional, IsString } from "c import { SchedulePresetShiftsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-preset-shifts.dto"; export class SchedulePresetsDto { - - @IsInt() - id!: number; - - @IsString() - name!: string; - - @IsBoolean() - @IsOptional() - is_default: boolean; - - @IsArray() - @ArrayMinSize(1) - preset_shifts: SchedulePresetShiftsDto[]; + @IsInt() id!: number; + @IsString() name!: string; + @IsBoolean() @IsOptional() is_default: boolean; + @IsArray() @ArrayMinSize(1) preset_shifts: SchedulePresetShiftsDto[]; } \ No newline at end of file diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts index bc9a51e..a6b17c0 100644 --- a/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service.ts @@ -9,7 +9,10 @@ import { Result } from "src/common/errors/result-error.factory"; @Injectable() export class SchedulePresetsApplyService { - constructor(private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver) { } + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver: EmailToIdResolver + ) { } async applyToTimesheet(email: string, id: number, start_date_iso: string): Promise> { if (!DATE_ISO_FORMAT.test(start_date_iso)) return { success: false, error: 'start_date must be of format :YYYY-MM-DD' }; diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service.ts index 7644f90..21b5c14 100644 --- a/src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service.ts +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service.ts @@ -1,227 +1,240 @@ -// import { Injectable, BadRequestException, NotFoundException, ConflictException } from "@nestjs/common"; -// import { Prisma, Weekday } from "@prisma/client"; -// import { PrismaService } from "src/prisma/prisma.service"; -// import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; -// import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; -// import { Result } from "src/common/errors/result-error.factory"; -// import { toHHmmFromDate, toDateFromString } from "src/common/utils/date-utils"; -// import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; +import { Injectable } from "@nestjs/common"; +import { Weekday } from "@prisma/client"; +import { PrismaService } from "src/prisma/prisma.service"; +import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; +import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; +import { Result } from "src/common/errors/result-error.factory"; +import { overlaps, toDateFromHHmm } from "src/common/utils/date-utils"; +import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; -// @Injectable() -// export class SchedulePresetsUpsertService { -// constructor( -// private readonly prisma: PrismaService, -// private readonly typeResolver: BankCodesResolver, -// private readonly emailResolver: EmailToIdResolver, -// ) { } -// //_________________________________________________________________ -// // CREATE -// //_________________________________________________________________ -// async createPreset(email: string, dto: SchedulePresetsDto): Promise> { -// try { -// const shifts_data = await this.normalizePresetShifts(dto); -// if (!shifts_data.success) return { success: false, error: `Employee with email: ${email} or dto not found` }; +@Injectable() +export class SchedulePresetsUpsertService { + constructor( + private readonly prisma: PrismaService, + private readonly typeResolver: BankCodesResolver, + private readonly emailResolver: EmailToIdResolver, + ) { } + //_________________________________________________________________ + // CREATE + //_________________________________________________________________ + async createPreset(email: string, dto: SchedulePresetsDto): Promise> { + //validate email and fetch employee_id + const employee_id = await this.emailResolver.findIdByEmail(email); + if (!employee_id.success) return { success: false, error: employee_id.error }; -// const employee_id = await this.emailResolver.findIdByEmail(email); -// if (!employee_id.success) return { success: false, error: employee_id.error }; + //validate new unique name + const existing = await this.prisma.schedulePresets.findFirst({ + where: { name: dto.name, employee_id: employee_id.data }, + select: { name: true }, + }); + if (!existing) return { success: false, error: 'INVALID_SCHEDULE_PRESET' }; -// const created = await this.prisma.$transaction(async (tx) => { -// if (dto.is_default) { -// await tx.schedulePresets.updateMany({ -// where: { is_default: true, employee_id: employee_id.data }, -// data: { is_default: false }, -// }); -// await tx.schedulePresets.create({ -// data: { -// id: dto.id, -// employee_id: employee_id.data, -// name: dto.name, -// is_default: !!dto.is_default, -// shifts: { create: shifts_data.data }, -// }, -// }); -// return { success: true, data: created } -// } -// }); -// return { success: true, data: created } -// } catch (error) { -// return { success: false, error: ' An error occured during create. Invalid Schedule data' }; -// } -// } + const normalized_shifts = dto.preset_shifts.map((shift) => ({ + ...shift, + start: toDateFromHHmm(shift.start_time), + end: toDateFromHHmm(shift.end_time), + })); -// //_________________________________________________________________ -// // UPDATE -// //_________________________________________________________________ -// async updatePreset(preset_id: number, dto: SchedulePresetsDto): Promise> { -// try { -// const existing = await this.prisma.schedulePresets.findFirst({ -// where: { id: preset_id }, -// select: { -// id: true, -// is_default: true, -// employee_id: true, -// }, -// }); -// if (!existing) return { success: false, error: `Preset "${dto.name}" not found` }; + for (const preset_shifts of normalized_shifts) { + for (const other_shifts of normalized_shifts) { + //skip if same object or id week_day is not the same + if (preset_shifts === other_shifts) continue; + if (preset_shifts.week_day !== other_shifts.week_day) continue; + //check overlaping possibilities + const has_overlap = overlaps( + { start: preset_shifts.start, end: preset_shifts.end }, + { start: other_shifts.start, end: other_shifts.end }, + ) + if (has_overlap) return { success: false, error: 'SCHEDULE_PRESET_OVERLAP' }; + } + } + //validate bank_code_id/type and map them + const bank_code_results = await Promise.all(dto.preset_shifts.map((shift) => + this.typeResolver.findBankCodeIDByType(shift.type), + )); + for (const result of bank_code_results) { + if (!result.success) return { success: false, error: 'INVALID_SCHEDULE_PRESET' } + } -// const shifts_data = await this.normalizePresetShifts(dto); -// if (!shifts_data.success) return { success: false, error: 'An error occured during normalization' } + await this.prisma.$transaction(async (tx) => { + //check if employee chose this preset has a default preset and ensure all others are false + if (dto.is_default) { + await tx.schedulePresets.updateMany({ + where: { employee_id: employee_id.data, is_default: true }, + data: { is_default: false }, + }); + } -// await this.prisma.$transaction(async (tx) => { -// if (typeof dto.is_default === 'boolean') { -// if (dto.is_default) { -// await tx.schedulePresets.updateMany({ -// where: { -// employee_id: existing.employee_id, -// is_default: true, -// NOT: { id: existing.id }, -// }, -// data: { is_default: false }, -// }); -// } -// await tx.schedulePresets.update({ -// where: { id: existing.id }, -// data: { -// is_default: dto.is_default, -// name: dto.name, -// }, -// }); -// } -// if (shifts_data.data.length <= 0) return { success: false, error: 'Preset shifts to update not found' }; + await tx.schedulePresets.create({ + data: { + employee_id: employee_id.data, + name: dto.name, + is_default: dto.is_default ?? false, + shifts: { + create: dto.preset_shifts.map((shift, index) => { + //validated bank_codes sent as a Result Array to access its data + const result = bank_code_results[index] as { success: true, data: number }; + return { + week_day: shift.week_day, + sort_order: shift.sort_order, + start_time: toDateFromHHmm(shift.start_time), + end_time: toDateFromHHmm(shift.end_time), + is_remote: shift.is_remote ?? false, + bank_code: { + //connect uses the FK links to set the bank_code_id + connect: { id: result.data }, + }, + } + }), + }, + }, + }); + }); + return { success: true, data: true } + } -// await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } }); + // //_________________________________________________________________ + // // UPDATE + // //_________________________________________________________________ + // async updatePreset(preset_id: number, dto: SchedulePresetsDto): Promise> { + // try { + // const existing = await this.prisma.schedulePresets.findFirst({ + // where: { id: preset_id }, + // select: { + // id: true, + // is_default: true, + // employee_id: true, + // }, + // }); + // if (!existing) return { success: false, error: `Preset "${dto.name}" not found` }; -// try { -// const create_many_data: Result = -// shifts_data.data.map((shift) => { -// if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') { -// return { success: false, error: `Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`} -// } -// const bank_code_id = shift.bank_code.connect.id; -// return { -// preset_id: existing.id, -// week_day: shift.week_day, -// sort_order: shift.sort_order, -// start_time: shift.start_time, -// end_time: shift.end_time, -// is_remote: shift.is_remote ?? false, -// bank_code_id: bank_code_id, -// }; -// }); -// if(!create_many_data.success) return { success: false, error: 'Invalid data'} -// await tx.schedulePresetShifts.createMany({ data: create_many_data.data }); + // const shifts_data = await this.normalizePresetShifts(dto); + // if (!shifts_data.success) return { success: false, error: 'An error occured during normalization' } -// return { success: true, data: create_many_data } -// } catch (error) { -// return { success: false, error: 'An error occured. Invalid data detected. ' }; -// } -// }); + // await this.prisma.$transaction(async (tx) => { + // if (typeof dto.is_default === 'boolean') { + // if (dto.is_default) { + // await tx.schedulePresets.updateMany({ + // where: { + // employee_id: existing.employee_id, + // is_default: true, + // NOT: { id: existing.id }, + // }, + // data: { is_default: false }, + // }); + // } + // await tx.schedulePresets.update({ + // where: { id: existing.id }, + // data: { + // is_default: dto.is_default, + // name: dto.name, + // }, + // }); + // } + // if (shifts_data.data.length <= 0) return { success: false, error: 'Preset shifts to update not found' }; -// const saved = await this.prisma.schedulePresets.findUnique({ -// where: { id: existing.id }, -// include: { -// shifts: { -// orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }], -// include: { bank_code: { select: { type: true } } }, -// } -// }, -// }); -// if (!saved) return { success: false, error: `Preset with id: ${existing.id} not found` }; + // await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } }); -// const response_dto: SchedulePresetsDto = { -// id: saved.id, -// name: saved.name, -// is_default: saved.is_default, -// preset_shifts: saved.shifts.map((shift) => ({ -// preset_id: shift.preset_id, -// week_day: shift.week_day, -// sort_order: shift.sort_order, -// type: shift.bank_code.type, -// start_time: toHHmmFromDate(shift.start_time), -// end_time: toHHmmFromDate(shift.end_time), -// is_remote: shift.is_remote, -// })), -// }; + // try { + // const create_many_data: Result = + // shifts_data.data.map((shift) => { + // if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') { + // return { success: false, error: `Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`} + // } + // const bank_code_id = shift.bank_code.connect.id; + // return { + // preset_id: existing.id, + // week_day: shift.week_day, + // sort_order: shift.sort_order, + // start_time: shift.start_time, + // end_time: shift.end_time, + // is_remote: shift.is_remote ?? false, + // bank_code_id: bank_code_id, + // }; + // }); + // if(!create_many_data.success) return { success: false, error: 'Invalid data'} + // await tx.schedulePresetShifts.createMany({ data: create_many_data.data }); -// return { success: true, data: response_dto }; -// } catch (error) { -// return { success: false, error: 'An error occured during update. Invalid data' } -// } -// } + // return { success: true, data: create_many_data } + // } catch (error) { + // return { success: false, error: 'An error occured. Invalid data detected. ' }; + // } + // }); -// //_________________________________________________________________ -// // DELETE -// //_________________________________________________________________ -// async deletePreset(preset_id: number): Promise> { -// try { -// await this.prisma.$transaction(async (tx) => { -// const preset = await tx.schedulePresets.findFirst({ -// where: { id: preset_id }, -// select: { id: true }, -// }); -// if (!preset) return { success: false, error: `Preset with id ${preset_id} not found` }; -// await tx.schedulePresets.delete({ where: { id: preset_id } }); + // const saved = await this.prisma.schedulePresets.findUnique({ + // where: { id: existing.id }, + // include: { + // shifts: { + // orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }], + // include: { bank_code: { select: { type: true } } }, + // } + // }, + // }); + // if (!saved) return { success: false, error: `Preset with id: ${existing.id} not found` }; -// return { success: true }; -// }); -// return { success: true, data: preset_id }; + // const response_dto: SchedulePresetsDto = { + // id: saved.id, + // name: saved.name, + // is_default: saved.is_default, + // preset_shifts: saved.shifts.map((shift) => ({ + // preset_id: shift.preset_id, + // week_day: shift.week_day, + // sort_order: shift.sort_order, + // type: shift.bank_code.type, + // start_time: toHHmmFromDate(shift.start_time), + // end_time: toHHmmFromDate(shift.end_time), + // is_remote: shift.is_remote, + // })), + // }; -// } catch (error) { -// return { success: false, error: `Preset schedule with id ${preset_id} not found` }; -// } -// } + // return { success: true, data: response_dto }; + // } catch (error) { + // return { success: false, error: 'An error occured during update. Invalid data' } + // } + // } -// //PRIVATE HELPERS + // //_________________________________________________________________ + // // DELETE + // //_________________________________________________________________ + // async deletePreset(preset_id: number): Promise> { + // try { + // await this.prisma.$transaction(async (tx) => { + // const preset = await tx.schedulePresets.findFirst({ + // where: { id: preset_id }, + // select: { id: true }, + // }); + // if (!preset) return { success: false, error: `Preset with id ${preset_id} not found` }; + // await tx.schedulePresets.delete({ where: { id: preset_id } }); -// //resolve bank_code_id using type and convert hours to TIME and valid shifts end/start -// private async normalizePresetShifts( -// dto: SchedulePresetsDto -// ): Promise> { -// if (!dto.preset_shifts?.length) return { success: false, error: `Empty or preset shifts not found` } + // return { success: true }; + // }); + // return { success: true, data: preset_id }; -// const types = Array.from(new Set(dto.preset_shifts.map((shift) => shift.type))); -// const bank_code_set = new Map(); + // } catch (error) { + // return { success: false, error: `Preset schedule with id ${preset_id} not found` }; + // } + // } -// for (const type of types) { -// const bank_code = await this.typeResolver.findIdAndModifierByType(type); -// if (!bank_code.success) return { success: false, error: 'Bank_code not found' } -// bank_code_set.set(type, bank_code.data.id); -// } + // //PRIVATE HELPERS -// const pair_set = new Set(); -// for (const shift of dto.preset_shifts) { -// const key = `${shift.week_day}:${shift.sort_order}`; -// if (pair_set.has(key)) { -// return { success: false, error: `Duplicate shift for day/order (${shift.week_day}, ${shift.sort_order})` } -// } -// pair_set.add(key); -// } + //resolve bank_code_id using type and convert hours to TIME and valid shifts end/start + // private async normalizePresetShifts(preset_shift: SchedulePresetShiftsDto, schedul_preset: SchedulePresetsDto): Promise> { -// const items = await dto.preset_shifts.map((shift) => { -// try { -// const bank_code_id = bank_code_set.get(shift.type); -// if (!bank_code_id) return { success: false, error: `Bank code not found for type ${shift.type}` } -// if (!shift.start_time || !shift.end_time) { -// return { success: false, error: `start_time and end_time are required for (${shift.week_day}, ${shift.sort_order})` } -// } -// const start = toDateFromString(shift.start_time); -// const end = toDateFromString(shift.end_time); -// if (end.getTime() <= start.getTime()) { -// return { success: false, error: `end_time must be > start_time ( day: ${shift.week_day}, order: ${shift.sort_order})` } -// } -// return { -// sort_order: shift.sort_order, -// start_time: start, -// end_time: end, -// is_remote: !!shift.is_remote, -// week_day: shift.week_day as Weekday, -// bank_code: { connect: { id: bank_code_id } }, -// } + // const bank_code = await this.typeResolver.findIdAndModifierByType(preset_shift.type); + // if (!bank_code.success) return { success: false, error: 'INVALID_SCHEDULE_PRESET_SHIFT' }; -// } catch (error) { -// return { success: false, error: '' } -// } -// }); -// return { success: true, data: items}; -// } -// } + // const start = await toDateFromHHmm(preset_shift.start_time); + // const end = await toDateFromHHmm(preset_shift.end_time); + + // //TODO: add a way to fetch + + + // const normalized_preset_shift:Normalized = { + // date: , + // start_time : start, + // end_time: end, + // bank_code_id: bank_code.data.id, + // } + // return { success: true data: normalized_preset_shift } + // } +} From 26ea84cf1abc5ac3eaaca95b44a79c2df743d3c8 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 26 Nov 2025 15:05:22 -0500 Subject: [PATCH 5/9] feat(user_module_access): created user_module_access model and module. implemented update, revoke and get methods. --- prisma/schema.prisma | 120 +++++---- .../user-module-access/access.module.ts | 11 + .../controllers/access.controller.ts | 40 +++ .../user-module-access/dtos/acces.dto.ts | 10 + .../services/access-get.service.ts | 42 +++ .../services/access-update.service.ts | 70 +++++ .../controller/schedule-presets.controller.ts | 50 ++-- .../schedule-presets.module.ts | 34 +-- .../schedule-presets-create.service.ts | 125 +++++++++ .../services/schedule-presets-get.service.ts | 23 +- .../schedule-presets-update-delete.service.ts | 125 +++++++++ .../schedule-presets-upsert.service.ts | 240 ------------------ 12 files changed, 547 insertions(+), 343 deletions(-) create mode 100644 src/identity-and-account/user-module-access/access.module.ts create mode 100644 src/identity-and-account/user-module-access/controllers/access.controller.ts create mode 100644 src/identity-and-account/user-module-access/dtos/acces.dto.ts create mode 100644 src/identity-and-account/user-module-access/services/access-get.service.ts create mode 100644 src/identity-and-account/user-module-access/services/access-update.service.ts create mode 100644 src/time-and-attendance/schedule-presets/services/schedule-presets-create.service.ts create mode 100644 src/time-and-attendance/schedule-presets/services/schedule-presets-update-delete.service.ts delete mode 100644 src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d0d2c87..ebd0fa5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -23,20 +23,36 @@ model Users { residence String? role Roles @default(GUEST) - employee Employees? @relation("UserEmployee") - oauth_sessions OAuthSessions[] @relation("UserOAuthSessions") - preferences Preferences? @relation("UserPreferences") + employee Employees? @relation("UserEmployee") + oauth_sessions OAuthSessions[] @relation("UserOAuthSessions") + preferences Preferences? @relation("UserPreferences") + user_module_access userModuleAccess? @relation("UserModuleAccess") @@map("users") } +model userModuleAccess { + id Int @id @default(autoincrement()) + user Users @relation("UserModuleAccess", fields: [user_id], references: [id]) + user_id String @unique @db.Uuid + + timesheets Boolean @default(false) //wich allows an employee to enter shifts and expenses + timesheets_approval Boolean @default(false) //wich allows the approbation of timesheets by a supervisor or above + employee_list Boolean @default(false) //wich shows the lists of employee to show names, emails, titles and profile picture + employee_management Boolean @default(false) //wich offers CRUD for employees, schedule_presets and manage module access + personnal_profile Boolean @default(false) //wich governs profile details, preferances and dashboard access + blocked Boolean @default(false) + + @@map("user_module_access") +} + model Employees { id Int @id @default(autoincrement()) user Users @relation("UserEmployee", fields: [user_id], references: [id]) user_id String @unique @db.Uuid supervisor Employees? @relation("EmployeeSupervisor", fields: [supervisor_id], references: [id]) supervisor_id Int? - + external_payroll_id Int company_code Int first_work_day DateTime @db.Date @@ -44,11 +60,10 @@ model Employees { job_title String? is_supervisor Boolean @default(false) - - crew Employees[] @relation("EmployeeSupervisor") - timesheet Timesheets[] @relation("TimesheetEmployee") - leave_request LeaveRequests[] @relation("LeaveRequestEmployee") - schedule_presets SchedulePresets[] @relation("SchedulePreset") + crew Employees[] @relation("EmployeeSupervisor") + timesheet Timesheets[] @relation("TimesheetEmployee") + leave_request LeaveRequests[] @relation("LeaveRequestEmployee") + schedule_presets SchedulePresets[] @relation("SchedulePreset") @@map("employees") } @@ -65,7 +80,7 @@ model LeaveRequests { payable_hours Decimal? @db.Decimal(5, 2) requested_hours Decimal? @db.Decimal(5, 2) approval_status LeaveApprovalStatus @default(PENDING) - leave_type LeaveTypes + leave_type LeaveTypes archive LeaveRequestsArchive[] @relation("LeaveRequestToArchive") @@ -79,14 +94,14 @@ model LeaveRequestsArchive { leave_request LeaveRequests @relation("LeaveRequestToArchive", fields: [leave_request_id], references: [id]) leave_request_id Int - archived_at DateTime @default(now()) - employee_id Int - date DateTime @db.Date - payable_hours Decimal? @db.Decimal(5, 2) - requested_hours Decimal? @db.Decimal(5, 2) - comment String - leave_type LeaveTypes - approval_status LeaveApprovalStatus + archived_at DateTime @default(now()) + employee_id Int + date DateTime @db.Date + payable_hours Decimal? @db.Decimal(5, 2) + requested_hours Decimal? @db.Decimal(5, 2) + comment String + leave_type LeaveTypes + approval_status LeaveApprovalStatus @@unique([leave_request_id]) @@index([employee_id, date]) @@ -126,9 +141,9 @@ model TimesheetsArchive { timesheet Timesheets @relation("TimesheetsToArchive", fields: [timesheet_id], references: [id]) timesheet_id Int - employee_id Int - is_approved Boolean - archive_at DateTime @default(now()) + employee_id Int + is_approved Boolean + archive_at DateTime @default(now()) @@map("timesheets_archive") } @@ -139,7 +154,7 @@ model SchedulePresets { employee_id Int name String - is_default Boolean @default(false) + is_default Boolean @default(false) shifts SchedulePresetShifts[] @relation("SchedulePresetShiftsSchedulePreset") @@ -149,9 +164,9 @@ model SchedulePresets { model SchedulePresetShifts { id Int @id @default(autoincrement()) - preset SchedulePresets @relation("SchedulePresetShiftsSchedulePreset",fields: [preset_id], references: [id]) + preset SchedulePresets @relation("SchedulePresetShiftsSchedulePreset", fields: [preset_id], references: [id]) preset_id Int - bank_code BankCodes @relation("SchedulePresetShiftsBankCodes",fields: [bank_code_id], references: [id]) + bank_code BankCodes @relation("SchedulePresetShiftsBankCodes", fields: [bank_code_id], references: [id]) bank_code_id Int sort_order Int @@ -180,13 +195,14 @@ model Shifts { comment String? archive ShiftsArchive[] @relation("ShiftsToArchive") + @@unique([timesheet_id, date, start_time], name: "unique_ts_id_date_start_time") @@map("shifts") } model ShiftsArchive { - id Int @id @default(autoincrement()) - shift Shifts @relation("ShiftsToArchive", fields: [shift_id], references: [id]) + id Int @id @default(autoincrement()) + shift Shifts @relation("ShiftsToArchive", fields: [shift_id], references: [id]) shift_id Int date DateTime @db.Date @@ -216,39 +232,40 @@ model BankCodes { } model Expenses { - id Int @id @default(autoincrement()) - timesheet Timesheets @relation("ExpensesTimesheet", fields: [timesheet_id], references: [id]) - timesheet_id Int - bank_code BankCodes @relation("ExpenseBankCodes", fields: [bank_code_id], references: [id]) - bank_code_id Int - attachment_record Attachments? @relation("ExpenseAttachment", fields: [attachment], references: [id], onDelete: SetNull) - attachment Int? + id Int @id @default(autoincrement()) + timesheet Timesheets @relation("ExpensesTimesheet", fields: [timesheet_id], references: [id]) + timesheet_id Int + bank_code BankCodes @relation("ExpenseBankCodes", fields: [bank_code_id], references: [id]) + bank_code_id Int + attachment_record Attachments? @relation("ExpenseAttachment", fields: [attachment], references: [id], onDelete: SetNull) + attachment Int? date DateTime @db.Date - amount Decimal? @db.Decimal(12,2) - mileage Decimal? @db.Decimal(12,2) + amount Decimal? @db.Decimal(12, 2) + mileage Decimal? @db.Decimal(12, 2) comment String supervisor_comment String? is_approved Boolean @default(false) archive ExpensesArchive[] @relation("ExpensesToArchive") + @@unique([timesheet_id, date, amount, mileage], name: "unique_ts_id_date_amount_mileage") @@map("expenses") } model ExpensesArchive { - id Int @id @default(autoincrement()) - expense Expenses @relation("ExpensesToArchive", fields: [expense_id], references: [id]) - expense_id Int - attachment_record Attachments? @relation("ExpenseArchiveAttachment", fields: [attachment], references: [id], onDelete: SetNull) - attachment Int? + id Int @id @default(autoincrement()) + expense Expenses @relation("ExpensesToArchive", fields: [expense_id], references: [id]) + expense_id Int + attachment_record Attachments? @relation("ExpenseArchiveAttachment", fields: [attachment], references: [id], onDelete: SetNull) + attachment Int? timesheet_id Int archived_at DateTime @default(now()) bank_code_id Int date DateTime @db.Date - amount Decimal? @db.Decimal(12,2) - mileage Decimal? @db.Decimal(12,2) + amount Decimal? @db.Decimal(12, 2) + mileage Decimal? @db.Decimal(12, 2) comment String? is_approved Boolean supervisor_comment String? @@ -289,7 +306,7 @@ model Blobs { model Attachments { id Int @id @default(autoincrement()) - blob Blobs @relation("AttachmnentBlob",fields: [sha256], references: [sha256], onUpdate: Cascade) + blob Blobs @relation("AttachmnentBlob", fields: [sha256], references: [sha256], onUpdate: Cascade) sha256 String @db.Char(64) owner_type String //EXPENSES ou éventuellement autre chose comme scan ONU ou photos d'employés, etc @@ -304,7 +321,7 @@ model Attachments { expenses_archive ExpensesArchive[] @relation("ExpenseArchiveAttachment") AttachmentVariants AttachmentVariants[] @relation("attachmentVariantAttachment") - + @@index([owner_type, owner_id, created_at]) @@index([sha256]) @@map("attachments") @@ -313,7 +330,7 @@ model Attachments { model AttachmentVariants { id Int @id @default(autoincrement()) attachment_id Int - attachment Attachments @relation("attachmentVariantAttachment",fields: [attachment_id], references: [id], onDelete: Cascade) + attachment Attachments @relation("attachmentVariantAttachment", fields: [attachment_id], references: [id], onDelete: Cascade) variant String path String bytes Int @@ -326,18 +343,18 @@ model AttachmentVariants { } model Preferences { - id Int @id @default(autoincrement()) - user Users @relation("UserPreferences", fields: [user_id], references: [id]) - user_id String @unique @db.Uuid + id Int @id @default(autoincrement()) + user Users @relation("UserPreferences", fields: [user_id], references: [id]) + user_id String @unique @db.Uuid notifications Int @default(0) dark_mode Int @default(0) lang_switch Int @default(0) lefty_mode Int @default(0) - employee_list_display Int @default(0) - validation_display Int @default(0) - timesheet_display Int @default(0) + employee_list_display Int @default(0) + validation_display Int @default(0) + timesheet_display Int @default(0) @@map("preferences") } @@ -398,4 +415,3 @@ enum Weekday { FRI SAT } - diff --git a/src/identity-and-account/user-module-access/access.module.ts b/src/identity-and-account/user-module-access/access.module.ts new file mode 100644 index 0000000..f120104 --- /dev/null +++ b/src/identity-and-account/user-module-access/access.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { AccessUpdateService } from "src/identity-and-account/user-module-access/services/access-update.service"; +import { AccessController } from "src/identity-and-account/user-module-access/controllers/access.controller"; +import { AccessGetService } from "src/identity-and-account/user-module-access/services/access-get.service"; + +@Module({ + controllers: [AccessController], + providers: [AccessUpdateService, AccessGetService], + exports: [], +}) +export class AccessModule { } \ No newline at end of file diff --git a/src/identity-and-account/user-module-access/controllers/access.controller.ts b/src/identity-and-account/user-module-access/controllers/access.controller.ts new file mode 100644 index 0000000..d9e7a52 --- /dev/null +++ b/src/identity-and-account/user-module-access/controllers/access.controller.ts @@ -0,0 +1,40 @@ +import { Body, Controller, Get, Patch, Query, Req } from "@nestjs/common"; +import { ModuleAccess } from "src/identity-and-account/user-module-access/dtos/acces.dto"; +import { AccessGetService } from "src/identity-and-account/user-module-access/services/access-get.service"; +import { AccessUpdateService } from "src/identity-and-account/user-module-access/services/access-update.service"; + +@Controller() +export class AccessController { + constructor( + private readonly getService: AccessGetService, + private readonly updateService: AccessUpdateService, + ) { } + + @Get() + async findAccess( + @Req() req, + @Query('employee_email') employee_email?: string + ) { + const email = req.user?.email; + await this.getService.findModuleAccess(email, employee_email); + }; + + @Patch() + async updateAccess( + @Req() req, + @Body() dto: ModuleAccess, + @Query('employee_email') employee_email?: string + ) { + const email = req.user?.email; + await this.updateService.updateModuleAccess(email, dto, employee_email); + }; + + @Patch() + async revokeModuleAccess( + @Req() req, + @Query('employee_email') employee_email?: string + ) { + const email = req.user?.email; + await this.updateService.revokeModuleAccess(email, employee_email); + }; +} \ No newline at end of file diff --git a/src/identity-and-account/user-module-access/dtos/acces.dto.ts b/src/identity-and-account/user-module-access/dtos/acces.dto.ts new file mode 100644 index 0000000..a4d5a6f --- /dev/null +++ b/src/identity-and-account/user-module-access/dtos/acces.dto.ts @@ -0,0 +1,10 @@ +import { IsBoolean } from "class-validator"; + +export class ModuleAccess { + @IsBoolean() timesheets!: boolean; + @IsBoolean() timesheets_approval!: boolean; + @IsBoolean() employee_list!: boolean; + @IsBoolean() employee_management!: boolean; + @IsBoolean() personnal_profile!: boolean; + @IsBoolean() blocked!: boolean; +} \ No newline at end of file diff --git a/src/identity-and-account/user-module-access/services/access-get.service.ts b/src/identity-and-account/user-module-access/services/access-get.service.ts new file mode 100644 index 0000000..6f990f8 --- /dev/null +++ b/src/identity-and-account/user-module-access/services/access-get.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from "@nestjs/common"; +import { Result } from "src/common/errors/result-error.factory"; +import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; +import { ModuleAccess } from "src/identity-and-account/user-module-access/dtos/acces.dto"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class AccessGetService { + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver: EmailToIdResolver, + ) { } + + async findModuleAccess(email: string, employee_email?: string): Promise> { + const account_email = employee_email ?? email; + const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email); + if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; + + const access = await this.prisma.userModuleAccess.findUnique({ + where: { user_id: user_id.data }, + select: { + timesheets: true, + timesheets_approval: true, + employee_list: true, + employee_management: true, + personnal_profile: true, + blocked: true, + }, + }); + if (!access) return { success: false, error: 'MODULE_ACCESS_NOT_FOUND' }; + + const granted_access: ModuleAccess = { + timesheets: access.timesheets, + timesheets_approval: access.timesheets_approval, + employee_list: access.employee_list, + employee_management: access.employee_management, + personnal_profile: access.personnal_profile, + blocked: access.blocked, + }; + return { success: true, data: granted_access } + } +} \ No newline at end of file diff --git a/src/identity-and-account/user-module-access/services/access-update.service.ts b/src/identity-and-account/user-module-access/services/access-update.service.ts new file mode 100644 index 0000000..818b013 --- /dev/null +++ b/src/identity-and-account/user-module-access/services/access-update.service.ts @@ -0,0 +1,70 @@ +import { Injectable } from "@nestjs/common"; +import { Result } from "src/common/errors/result-error.factory"; +import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; +import { ModuleAccess } from "src/identity-and-account/user-module-access/dtos/acces.dto"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Injectable() +export class AccessUpdateService { + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver: EmailToIdResolver, + ) { } + + async updateModuleAccess(email: string, dto: ModuleAccess, employee_email?: string): Promise> { + const account_email = employee_email ?? email; + const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email); + if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; + + const orignal_access = await this.prisma.userModuleAccess.findUnique({ + where: { user_id: user_id.data }, + select: { + id: true, + timesheets: true, + timesheets_approval: true, + employee_list: true, + employee_management: true, + personnal_profile: true, + }, + }); + if (!orignal_access) return { success: false, error: 'MODULE_ACCESS_NOT_FOUND' }; + + await this.prisma.userModuleAccess.update({ + where: { id: orignal_access.id }, + data: { + timesheets: dto.timesheets, + timesheets_approval: dto.timesheets_approval, + employee_list: dto.employee_list, + employee_management: dto.employee_management, + personnal_profile: dto.personnal_profile, + } + }) + return { success: true, data: true }; + } + + async revokeModuleAccess(email: string, employee_email?: string): Promise> { + const account_email = employee_email ?? email; + const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email); + if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; + + const access = await this.prisma.userModuleAccess.findUnique({ + where: { user_id: user_id.data }, + select: { id: true }, + }); + if (!access) return { success: false, error: 'MODULE_ACCESS_NOT_FOUND' }; + + await this.prisma.userModuleAccess.update({ + where: { id: access.id }, + data: { + timesheets: false, + timesheets_approval: false, + employee_list: false, + employee_management: false, + personnal_profile: false, + blocked: true, + }, + }); + + return { success: true, data: true }; + } +} \ No newline at end of file diff --git a/src/time-and-attendance/schedule-presets/controller/schedule-presets.controller.ts b/src/time-and-attendance/schedule-presets/controller/schedule-presets.controller.ts index 3207f18..9bf003d 100644 --- a/src/time-and-attendance/schedule-presets/controller/schedule-presets.controller.ts +++ b/src/time-and-attendance/schedule-presets/controller/schedule-presets.controller.ts @@ -2,18 +2,19 @@ import { Controller, Param, Query, Body, Get, Post, ParseIntPipe, Delete, Patch, import { RolesAllowed } from "src/common/decorators/roles.decorators"; import { GLOBAL_CONTROLLER_ROLES, MANAGER_ROLES } from "src/common/shared/role-groupes"; import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; -// import { SchedulePresetsUpdateDto } from "src/time-and-attendance/schedule-presets/dtos/update-schedule-presets.dto"; import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service"; import { SchedulePresetsGetService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-get.service"; -import { SchedulePresetsUpsertService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service"; +import { SchedulePresetsCreateService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-create.service"; +import { SchedulePresetUpdateDeleteService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-update-delete.service"; @Controller('schedule-presets') @RolesAllowed(...GLOBAL_CONTROLLER_ROLES) export class SchedulePresetsController { constructor( - private readonly upsertService: SchedulePresetsUpsertService, + private readonly createService: SchedulePresetsCreateService, private readonly getService: SchedulePresetsGetService, private readonly applyPresetsService: SchedulePresetsApplyService, + private readonly updateDeleteService: SchedulePresetUpdateDeleteService, ) { } // used to create a schedule preset @@ -21,34 +22,33 @@ export class SchedulePresetsController { @RolesAllowed(...MANAGER_ROLES) async createPreset(@Req() req, @Body() dto: SchedulePresetsDto) { const email = req.user?.email; - return await this.upsertService.createPreset(email, dto); + return await this.createService.createPreset(email, dto); } - // //used to update an already existing schedule preset - // @Patch('update/:preset_id') - // @RolesAllowed(...MANAGER_ROLES) - // async updatePreset( - // @Param('preset_id', ParseIntPipe) preset_id: number, - // @Body() dto: SchedulePresetsUpdateDto - // ) { - // return await this.upsertService.updatePreset(preset_id, dto); - // } + //used to update an already existing schedule preset + @Patch('update/:preset_id') + @RolesAllowed(...MANAGER_ROLES) + async updatePreset( + @Param('preset_id', ParseIntPipe) preset_id: number, + @Body() dto: SchedulePresetsDto, + @Req() req, + ) { + const email = req.user?.email; + return await this.updateDeleteService.updatePreset(preset_id, dto, email); + } //used to delete a schedule preset - // @Delete('delete/:preset_id') - // @RolesAllowed(...MANAGER_ROLES) - // async deletePreset( - // @Param('preset_id', ParseIntPipe) preset_id: number) { - // return await this.upsertService.deletePreset(preset_id); - // } - + @Delete('delete/:preset_id') + @RolesAllowed(...MANAGER_ROLES) + async deletePreset(@Param('preset_id', ParseIntPipe) preset_id: number, @Req() req) { + const email = req.user?.email; + return await this.updateDeleteService.deletePreset(preset_id, email); + } //used to show the list of available schedule presets @Get('find-list') @RolesAllowed(...MANAGER_ROLES) - async findListById( - @Req() req - ) { + async findListById(@Req() req) { const email = req.user?.email; return this.getService.getSchedulePresets(email); } @@ -56,9 +56,9 @@ export class SchedulePresetsController { //used to apply a preset to a timesheet @Post('apply-presets') async applyPresets( - @Req() req, @Body('preset') preset_id: number, - @Body('start') start_date: string + @Body('start') start_date: string, + @Req() req ) { const email = req.user?.email; return this.applyPresetsService.applyToTimesheet(email, preset_id, start_date); diff --git a/src/time-and-attendance/schedule-presets/schedule-presets.module.ts b/src/time-and-attendance/schedule-presets/schedule-presets.module.ts index 1c7801d..09be865 100644 --- a/src/time-and-attendance/schedule-presets/schedule-presets.module.ts +++ b/src/time-and-attendance/schedule-presets/schedule-presets.module.ts @@ -1,21 +1,25 @@ import { Module } from "@nestjs/common"; -// import { SchedulePresetsController } from "src/time-and-attendance/time-tracker/schedule-presets/controller/schedule-presets.controller"; -// import { SchedulePresetsApplyService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-apply.service"; -// import { SchedulePresetsGetService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-get.service"; -// import { SchedulePresetsUpsertService } from "src/time-and-attendance/time-tracker/schedule-presets/services/schedule-presets-upsert.service"; +import { SchedulePresetsController } from "src/time-and-attendance/schedule-presets/controller/schedule-presets.controller"; +import { SchedulePresetsApplyService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-apply.service"; +import { SchedulePresetsCreateService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-create.service"; +import { SchedulePresetsGetService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-get.service"; +import { SchedulePresetUpdateDeleteService } from "src/time-and-attendance/schedule-presets/services/schedule-presets-update-delete.service"; + @Module({ - controllers: [/*SchedulePresetsController*/], - // providers: [ - // SchedulePresetsUpsertService, - // SchedulePresetsGetService, - // SchedulePresetsApplyService, - // ], - exports:[ - // SchedulePresetsUpsertService, - // SchedulePresetsGetService, - // SchedulePresetsApplyService, + controllers: [SchedulePresetsController], + providers: [ + SchedulePresetsCreateService, + SchedulePresetUpdateDeleteService, + SchedulePresetsGetService, + SchedulePresetsApplyService, ], -}) export class SchedulePresetsModule {} \ No newline at end of file + exports: [ + SchedulePresetsCreateService, + SchedulePresetUpdateDeleteService, + SchedulePresetsGetService, + SchedulePresetsApplyService, + ], +}) export class SchedulePresetsModule { } \ No newline at end of file diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-create.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-create.service.ts new file mode 100644 index 0000000..6207ba4 --- /dev/null +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-create.service.ts @@ -0,0 +1,125 @@ +import { Injectable } from "@nestjs/common"; +import { Weekday } from "@prisma/client"; +import { PrismaService } from "src/prisma/prisma.service"; +import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; +import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; +import { Result } from "src/common/errors/result-error.factory"; +import { overlaps, toDateFromHHmm } from "src/common/utils/date-utils"; +import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; + +@Injectable() +export class SchedulePresetsCreateService { + constructor( + private readonly prisma: PrismaService, + private readonly typeResolver: BankCodesResolver, + private readonly emailResolver: EmailToIdResolver, + ) { } + //_________________________________________________________________ + // CREATE + //_________________________________________________________________ + async createPreset(email: string, dto: SchedulePresetsDto): Promise> { + try { + + //validate email and fetch employee_id + const employee_id = await this.emailResolver.findIdByEmail(email); + if (!employee_id.success) return { success: false, error: employee_id.error }; + + //validate new unique name + const existing = await this.prisma.schedulePresets.findFirst({ + where: { name: dto.name, employee_id: employee_id.data }, + select: { name: true }, + }); + if (!existing) return { success: false, error: 'INVALID_SCHEDULE_PRESET' }; + + const normalized_shifts = dto.preset_shifts.map((shift) => ({ + ...shift, + start: toDateFromHHmm(shift.start_time), + end: toDateFromHHmm(shift.end_time), + })); + + for (const preset_shifts of normalized_shifts) { + for (const other_shifts of normalized_shifts) { + //skip if same object or id week_day is not the same + if (preset_shifts === other_shifts) continue; + if (preset_shifts.week_day !== other_shifts.week_day) continue; + //check overlaping possibilities + const has_overlap = overlaps( + { start: preset_shifts.start, end: preset_shifts.end }, + { start: other_shifts.start, end: other_shifts.end }, + ) + if (has_overlap) return { success: false, error: 'SCHEDULE_PRESET_OVERLAP' }; + } + } + + //validate bank_code_id/type and map them + const bank_code_results = await Promise.all(dto.preset_shifts.map((shift) => + this.typeResolver.findBankCodeIDByType(shift.type), + )); + for (const result of bank_code_results) { + if (!result.success) return { success: false, error: 'INVALID_SCHEDULE_PRESET' } + } + + await this.prisma.$transaction(async (tx) => { + //check if employee chose this preset has a default preset and ensure all others are false + if (dto.is_default) { + await tx.schedulePresets.updateMany({ + where: { employee_id: employee_id.data, is_default: true }, + data: { is_default: false }, + }); + } + + await tx.schedulePresets.create({ + data: { + employee_id: employee_id.data, + name: dto.name, + is_default: dto.is_default ?? false, + shifts: { + create: dto.preset_shifts.map((shift, index) => { + //validated bank_codes sent as a Result Array to access its data + const result = bank_code_results[index] as { success: true, data: number }; + return { + week_day: shift.week_day, + sort_order: shift.sort_order, + start_time: toDateFromHHmm(shift.start_time), + end_time: toDateFromHHmm(shift.end_time), + is_remote: shift.is_remote ?? false, + bank_code: { + //connect uses the FK links to set the bank_code_id + connect: { id: result.data }, + }, + } + }), + }, + }, + }); + }); + return { success: true, data: true } + } catch (error) { + return { success: false, error: 'INVALID_SCHEDULE_PRESET'} + } + } + + + // //PRIVATE HELPERS + + //resolve bank_code_id using type and convert hours to TIME and valid shifts end/start + // private async normalizePresetShifts(preset_shift: SchedulePresetShiftsDto, schedul_preset: SchedulePresetsDto): Promise> { + + // const bank_code = await this.typeResolver.findIdAndModifierByType(preset_shift.type); + // if (!bank_code.success) return { success: false, error: 'INVALID_SCHEDULE_PRESET_SHIFT' }; + + // const start = await toDateFromHHmm(preset_shift.start_time); + // const end = await toDateFromHHmm(preset_shift.end_time); + + // //TODO: add a way to fetch + + + // const normalized_preset_shift:Normalized = { + // date: , + // start_time : start, + // end_time: end, + // bank_code_id: bank_code.data.id, + // } + // return { success: true data: normalized_preset_shift } + // } +} diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-get.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-get.service.ts index 405d8c7..e7cac2f 100644 --- a/src/time-and-attendance/schedule-presets/services/schedule-presets-get.service.ts +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-get.service.ts @@ -6,32 +6,33 @@ import { Result } from "src/common/errors/result-error.factory"; @Injectable() export class SchedulePresetsGetService { - constructor( + constructor( private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver, - ){} + ) { } async getSchedulePresets(email: string): Promise> { try { const employee_id = await this.emailResolver.findIdByEmail(email); - if(!employee_id.success) return { success: false, error: employee_id.error } + if (!employee_id.success) return { success: false, error: employee_id.error }; + const presets = await this.prisma.schedulePresets.findMany({ where: { employee_id: employee_id.data }, - orderBy: [{is_default: 'desc' }, { name: 'asc' }], + orderBy: [{ is_default: 'desc' }, { name: 'asc' }], include: { shifts: { - orderBy: [{week_day:'asc'}, { sort_order: 'asc'}], + orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }], include: { bank_code: { select: { type: true } } }, }, }, }); - const hhmm = (date: Date) => date.toISOString().slice(11,16); + const hhmm = (date: Date) => date.toISOString().slice(11, 16); const response: PresetResponse[] = presets.map((preset) => ({ id: preset.id, name: preset.name, is_default: preset.is_default, - shifts: preset.shifts.map((shift)=> ({ + shifts: preset.shifts.map((shift) => ({ week_day: shift.week_day, sort_order: shift.sort_order, start_time: hhmm(shift.start_time), @@ -40,10 +41,10 @@ export class SchedulePresetsGetService { type: shift.bank_code?.type, })), })); - return { success: true, data:response}; - } catch ( error) { - return { success: false, error: `Schedule presets for employee with email ${email} not found`}; + return { success: true, data: response }; + } catch (error) { + return { success: false, error: `SCHEDULE_PRESET_NOT_FOUND` }; } } - + } \ No newline at end of file diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-update-delete.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-update-delete.service.ts new file mode 100644 index 0000000..fef84de --- /dev/null +++ b/src/time-and-attendance/schedule-presets/services/schedule-presets-update-delete.service.ts @@ -0,0 +1,125 @@ +import { Injectable } from "@nestjs/common"; +import { Result } from "src/common/errors/result-error.factory"; +import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; +import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; +import { overlaps, toDateFromHHmm } from "src/common/utils/date-utils"; +import { PrismaService } from "src/prisma/prisma.service"; +import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; + +@Injectable() +export class SchedulePresetUpdateDeleteService { + constructor( + private readonly prisma: PrismaService, + private readonly typeResolver: BankCodesResolver, + private readonly emailResolver: EmailToIdResolver, + ) { } + + //_________________________________________________________________ + // UPDATE + //_________________________________________________________________ + async updatePreset(preset_id: number, dto: SchedulePresetsDto, email: string): Promise> { + + const employee_id = await this.emailResolver.findIdByEmail(email); + if (!employee_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' } + //look for existing schedule_preset and return OG's data + const existing = await this.prisma.schedulePresets.findFirst({ + where: { id: preset_id, name: dto.name, employee_id: employee_id.data }, + select: { + id: true, + is_default: true, + employee_id: true, + shifts: true, + }, + }); + if (!existing) return { success: false, error: `SCHEDULE_PRESET_NOT_FOUND` }; + //normalized shifts start and end time to make an overlap check with other shifts + const normalized_shifts = dto.preset_shifts.map((shift) => ({ + ...shift, + start: toDateFromHHmm(shift.start_time), + end: toDateFromHHmm(shift.end_time), + })); + + for (const preset_shifts of normalized_shifts) { + for (const other_shifts of normalized_shifts) { + //skip if same object or id week_day is not the same + if (preset_shifts === other_shifts) continue; + if (preset_shifts.week_day !== other_shifts.week_day) continue; + //check overlaping possibilities + const has_overlap = overlaps( + { start: preset_shifts.start, end: preset_shifts.end }, + { start: other_shifts.start, end: other_shifts.end }, + ) + if (has_overlap) return { success: false, error: 'SCHEDULE_PRESET_OVERLAP' }; + } + } + //validate bank_code_id/type and map them + const bank_code_results = await Promise.all(dto.preset_shifts.map((shift) => + this.typeResolver.findBankCodeIDByType(shift.type), + )); + for (const result of bank_code_results) { + if (!result.success) return { success: false, error: 'INVALID_SCHEDULE_PRESET' } + } + + await this.prisma.$transaction(async (tx) => { + //check if employee chose this preset has a default preset and ensure all others are false + if (dto.is_default) { + await tx.schedulePresets.updateMany({ + where: { + employee_id: existing.employee_id, + is_default: true, + NOT: { id: existing.id }, + }, + data: { is_default: false }, + }); + } + //deletes old preset shifts to make place to new ones + await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } }); + + await tx.schedulePresets.update({ + where: { id: existing.id }, + data: { + name: dto.name, + is_default: dto.is_default ?? false, + shifts: { + create: dto.preset_shifts.map((shift, index) => { + //validated bank_codes sent as a Result Array to access its data + const result = bank_code_results[index] as { success: true, data: number }; + return { + week_day: shift.week_day, + sort_order: shift.sort_order, + start_time: toDateFromHHmm(shift.start_time), + end_time: toDateFromHHmm(shift.end_time), + is_remote: shift.is_remote ?? false, + bank_code: { + //connect uses the FK links to set the bank_code_id + connect: { id: result.data }, + }, + } + }), + }, + }, + }); + }); + return { success: true, data: true }; + } + + //_________________________________________________________________ + // DELETE + //_________________________________________________________________ + async deletePreset(preset_id: number, email: string): Promise> { + const employee_id = await this.emailResolver.findIdByEmail(email); + if (!employee_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' } + + const preset = await this.prisma.schedulePresets.findFirst({ + where: { id: preset_id, employee_id: employee_id.data }, + select: { id: true }, + }); + if (!preset) return { success: false, error: `SCHEDULE_PRESET_NOT_FOUND` }; + + await this.prisma.$transaction(async (tx) => { + await tx.schedulePresetShifts.deleteMany({ where: { preset_id: preset_id } }); + await tx.schedulePresets.delete({ where: { id: preset_id } }); + }); + return { success: true, data: true }; + } +} \ No newline at end of file diff --git a/src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service.ts b/src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service.ts deleted file mode 100644 index 21b5c14..0000000 --- a/src/time-and-attendance/schedule-presets/services/schedule-presets-upsert.service.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { Weekday } from "@prisma/client"; -import { PrismaService } from "src/prisma/prisma.service"; -import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; -import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; -import { Result } from "src/common/errors/result-error.factory"; -import { overlaps, toDateFromHHmm } from "src/common/utils/date-utils"; -import { SchedulePresetsDto } from "src/time-and-attendance/schedule-presets/dtos/create-schedule-presets.dto"; - -@Injectable() -export class SchedulePresetsUpsertService { - constructor( - private readonly prisma: PrismaService, - private readonly typeResolver: BankCodesResolver, - private readonly emailResolver: EmailToIdResolver, - ) { } - //_________________________________________________________________ - // CREATE - //_________________________________________________________________ - async createPreset(email: string, dto: SchedulePresetsDto): Promise> { - //validate email and fetch employee_id - const employee_id = await this.emailResolver.findIdByEmail(email); - if (!employee_id.success) return { success: false, error: employee_id.error }; - - //validate new unique name - const existing = await this.prisma.schedulePresets.findFirst({ - where: { name: dto.name, employee_id: employee_id.data }, - select: { name: true }, - }); - if (!existing) return { success: false, error: 'INVALID_SCHEDULE_PRESET' }; - - const normalized_shifts = dto.preset_shifts.map((shift) => ({ - ...shift, - start: toDateFromHHmm(shift.start_time), - end: toDateFromHHmm(shift.end_time), - })); - - for (const preset_shifts of normalized_shifts) { - for (const other_shifts of normalized_shifts) { - //skip if same object or id week_day is not the same - if (preset_shifts === other_shifts) continue; - if (preset_shifts.week_day !== other_shifts.week_day) continue; - //check overlaping possibilities - const has_overlap = overlaps( - { start: preset_shifts.start, end: preset_shifts.end }, - { start: other_shifts.start, end: other_shifts.end }, - ) - if (has_overlap) return { success: false, error: 'SCHEDULE_PRESET_OVERLAP' }; - } - } - //validate bank_code_id/type and map them - const bank_code_results = await Promise.all(dto.preset_shifts.map((shift) => - this.typeResolver.findBankCodeIDByType(shift.type), - )); - for (const result of bank_code_results) { - if (!result.success) return { success: false, error: 'INVALID_SCHEDULE_PRESET' } - } - - await this.prisma.$transaction(async (tx) => { - //check if employee chose this preset has a default preset and ensure all others are false - if (dto.is_default) { - await tx.schedulePresets.updateMany({ - where: { employee_id: employee_id.data, is_default: true }, - data: { is_default: false }, - }); - } - - await tx.schedulePresets.create({ - data: { - employee_id: employee_id.data, - name: dto.name, - is_default: dto.is_default ?? false, - shifts: { - create: dto.preset_shifts.map((shift, index) => { - //validated bank_codes sent as a Result Array to access its data - const result = bank_code_results[index] as { success: true, data: number }; - return { - week_day: shift.week_day, - sort_order: shift.sort_order, - start_time: toDateFromHHmm(shift.start_time), - end_time: toDateFromHHmm(shift.end_time), - is_remote: shift.is_remote ?? false, - bank_code: { - //connect uses the FK links to set the bank_code_id - connect: { id: result.data }, - }, - } - }), - }, - }, - }); - }); - return { success: true, data: true } - } - - // //_________________________________________________________________ - // // UPDATE - // //_________________________________________________________________ - // async updatePreset(preset_id: number, dto: SchedulePresetsDto): Promise> { - // try { - // const existing = await this.prisma.schedulePresets.findFirst({ - // where: { id: preset_id }, - // select: { - // id: true, - // is_default: true, - // employee_id: true, - // }, - // }); - // if (!existing) return { success: false, error: `Preset "${dto.name}" not found` }; - - // const shifts_data = await this.normalizePresetShifts(dto); - // if (!shifts_data.success) return { success: false, error: 'An error occured during normalization' } - - // await this.prisma.$transaction(async (tx) => { - // if (typeof dto.is_default === 'boolean') { - // if (dto.is_default) { - // await tx.schedulePresets.updateMany({ - // where: { - // employee_id: existing.employee_id, - // is_default: true, - // NOT: { id: existing.id }, - // }, - // data: { is_default: false }, - // }); - // } - // await tx.schedulePresets.update({ - // where: { id: existing.id }, - // data: { - // is_default: dto.is_default, - // name: dto.name, - // }, - // }); - // } - // if (shifts_data.data.length <= 0) return { success: false, error: 'Preset shifts to update not found' }; - - // await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } }); - - // try { - // const create_many_data: Result = - // shifts_data.data.map((shift) => { - // if (!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !== 'number') { - // return { success: false, error: `Bank code is required for updates( ${shift.week_day}, ${shift.sort_order})`} - // } - // const bank_code_id = shift.bank_code.connect.id; - // return { - // preset_id: existing.id, - // week_day: shift.week_day, - // sort_order: shift.sort_order, - // start_time: shift.start_time, - // end_time: shift.end_time, - // is_remote: shift.is_remote ?? false, - // bank_code_id: bank_code_id, - // }; - // }); - // if(!create_many_data.success) return { success: false, error: 'Invalid data'} - // await tx.schedulePresetShifts.createMany({ data: create_many_data.data }); - - // return { success: true, data: create_many_data } - // } catch (error) { - // return { success: false, error: 'An error occured. Invalid data detected. ' }; - // } - // }); - - // const saved = await this.prisma.schedulePresets.findUnique({ - // where: { id: existing.id }, - // include: { - // shifts: { - // orderBy: [{ week_day: 'asc' }, { sort_order: 'asc' }], - // include: { bank_code: { select: { type: true } } }, - // } - // }, - // }); - // if (!saved) return { success: false, error: `Preset with id: ${existing.id} not found` }; - - // const response_dto: SchedulePresetsDto = { - // id: saved.id, - // name: saved.name, - // is_default: saved.is_default, - // preset_shifts: saved.shifts.map((shift) => ({ - // preset_id: shift.preset_id, - // week_day: shift.week_day, - // sort_order: shift.sort_order, - // type: shift.bank_code.type, - // start_time: toHHmmFromDate(shift.start_time), - // end_time: toHHmmFromDate(shift.end_time), - // is_remote: shift.is_remote, - // })), - // }; - - // return { success: true, data: response_dto }; - // } catch (error) { - // return { success: false, error: 'An error occured during update. Invalid data' } - // } - // } - - // //_________________________________________________________________ - // // DELETE - // //_________________________________________________________________ - // async deletePreset(preset_id: number): Promise> { - // try { - // await this.prisma.$transaction(async (tx) => { - // const preset = await tx.schedulePresets.findFirst({ - // where: { id: preset_id }, - // select: { id: true }, - // }); - // if (!preset) return { success: false, error: `Preset with id ${preset_id} not found` }; - // await tx.schedulePresets.delete({ where: { id: preset_id } }); - - // return { success: true }; - // }); - // return { success: true, data: preset_id }; - - // } catch (error) { - // return { success: false, error: `Preset schedule with id ${preset_id} not found` }; - // } - // } - - // //PRIVATE HELPERS - - //resolve bank_code_id using type and convert hours to TIME and valid shifts end/start - // private async normalizePresetShifts(preset_shift: SchedulePresetShiftsDto, schedul_preset: SchedulePresetsDto): Promise> { - - // const bank_code = await this.typeResolver.findIdAndModifierByType(preset_shift.type); - // if (!bank_code.success) return { success: false, error: 'INVALID_SCHEDULE_PRESET_SHIFT' }; - - // const start = await toDateFromHHmm(preset_shift.start_time); - // const end = await toDateFromHHmm(preset_shift.end_time); - - // //TODO: add a way to fetch - - - // const normalized_preset_shift:Normalized = { - // date: , - // start_time : start, - // end_time: end, - // bank_code_id: bank_code.data.id, - // } - // return { success: true data: normalized_preset_shift } - // } -} From 439e0059368e6a8732679e9defa9cffdcd3aee74 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 26 Nov 2025 15:10:02 -0500 Subject: [PATCH 6/9] fix(user_module_access): fix imports and file naming --- docs/swagger/swagger-spec.json | 48 +++++++++++++++++++ .../identity-and-account.module.ts | 8 ++++ .../user-module-access/access.module.ts | 11 ----- ...troller.ts => module-access.controller.ts} | 8 ++-- .../{acces.dto.ts => module-acces.dto.ts} | 0 .../module-access.module.ts | 12 +++++ ...ervice.ts => module-access-get.service.ts} | 2 +- ...ice.ts => module-access-update.service.ts} | 2 +- 8 files changed, 74 insertions(+), 17 deletions(-) delete mode 100644 src/identity-and-account/user-module-access/access.module.ts rename src/identity-and-account/user-module-access/controllers/{access.controller.ts => module-access.controller.ts} (86%) rename src/identity-and-account/user-module-access/dtos/{acces.dto.ts => module-acces.dto.ts} (100%) create mode 100644 src/identity-and-account/user-module-access/module-access.module.ts rename src/identity-and-account/user-module-access/services/{access-get.service.ts => module-access-get.service.ts} (98%) rename src/identity-and-account/user-module-access/services/{access-update.service.ts => module-access-update.service.ts} (98%) diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index fe774db..27f2ade 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -560,6 +560,50 @@ "Preferences" ] } + }, + "/": { + "get": { + "operationId": "ModuleAccessController_findAccess", + "parameters": [ + { + "name": "employee_email", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "ModuleAccess" + ] + }, + "patch": { + "operationId": "ModuleAccessController_revokeModuleAccess", + "parameters": [ + { + "name": "employee_email", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "ModuleAccess" + ] + } } }, "info": { @@ -778,6 +822,10 @@ "PreferencesDto": { "type": "object", "properties": {} + }, + "ModuleAccess": { + "type": "object", + "properties": {} } } } diff --git a/src/identity-and-account/identity-and-account.module.ts b/src/identity-and-account/identity-and-account.module.ts index a656fc2..3476d73 100644 --- a/src/identity-and-account/identity-and-account.module.ts +++ b/src/identity-and-account/identity-and-account.module.ts @@ -7,6 +7,10 @@ import { EmployeesService } from "src/identity-and-account/employees/services/em import { PreferencesController } from "src/identity-and-account/preferences/controllers/preferences.controller"; import { PreferencesModule } from "src/identity-and-account/preferences/preferences.module"; import { PreferencesService } from "src/identity-and-account/preferences/services/preferences.service"; +import { ModuleAccessModule } from "src/identity-and-account/user-module-access/module-access.module"; +import { ModuleAccessController } from "src/identity-and-account/user-module-access/controllers/module-access.controller"; +import { AccessGetService } from "src/identity-and-account/user-module-access/services/module-access-get.service"; +import { AccessUpdateService } from "src/identity-and-account/user-module-access/services/module-access-update.service"; import { UsersService } from "src/identity-and-account/users-management/services/users.service"; import { UsersModule } from "src/identity-and-account/users-management/users.module"; @@ -15,10 +19,12 @@ import { UsersModule } from "src/identity-and-account/users-management/users.mod UsersModule, EmployeesModule, PreferencesModule, + ModuleAccessModule, ], controllers: [ EmployeesController, PreferencesController, + ModuleAccessController, ], providers: [ EmployeesArchivalService, @@ -26,6 +32,8 @@ import { UsersModule } from "src/identity-and-account/users-management/users.mod PreferencesService, UsersService, EmailToIdResolver, + AccessUpdateService, + AccessGetService, ], }) export class IdentityAndAccountModule { }; diff --git a/src/identity-and-account/user-module-access/access.module.ts b/src/identity-and-account/user-module-access/access.module.ts deleted file mode 100644 index f120104..0000000 --- a/src/identity-and-account/user-module-access/access.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from "@nestjs/common"; -import { AccessUpdateService } from "src/identity-and-account/user-module-access/services/access-update.service"; -import { AccessController } from "src/identity-and-account/user-module-access/controllers/access.controller"; -import { AccessGetService } from "src/identity-and-account/user-module-access/services/access-get.service"; - -@Module({ - controllers: [AccessController], - providers: [AccessUpdateService, AccessGetService], - exports: [], -}) -export class AccessModule { } \ No newline at end of file diff --git a/src/identity-and-account/user-module-access/controllers/access.controller.ts b/src/identity-and-account/user-module-access/controllers/module-access.controller.ts similarity index 86% rename from src/identity-and-account/user-module-access/controllers/access.controller.ts rename to src/identity-and-account/user-module-access/controllers/module-access.controller.ts index d9e7a52..7d902c8 100644 --- a/src/identity-and-account/user-module-access/controllers/access.controller.ts +++ b/src/identity-and-account/user-module-access/controllers/module-access.controller.ts @@ -1,10 +1,10 @@ import { Body, Controller, Get, Patch, Query, Req } from "@nestjs/common"; -import { ModuleAccess } from "src/identity-and-account/user-module-access/dtos/acces.dto"; -import { AccessGetService } from "src/identity-and-account/user-module-access/services/access-get.service"; -import { AccessUpdateService } from "src/identity-and-account/user-module-access/services/access-update.service"; +import { ModuleAccess } from "src/identity-and-account/user-module-access/dtos/module-acces.dto"; +import { AccessGetService } from "src/identity-and-account/user-module-access/services/module-access-get.service"; +import { AccessUpdateService } from "src/identity-and-account/user-module-access/services/module-access-update.service"; @Controller() -export class AccessController { +export class ModuleAccessController { constructor( private readonly getService: AccessGetService, private readonly updateService: AccessUpdateService, diff --git a/src/identity-and-account/user-module-access/dtos/acces.dto.ts b/src/identity-and-account/user-module-access/dtos/module-acces.dto.ts similarity index 100% rename from src/identity-and-account/user-module-access/dtos/acces.dto.ts rename to src/identity-and-account/user-module-access/dtos/module-acces.dto.ts diff --git a/src/identity-and-account/user-module-access/module-access.module.ts b/src/identity-and-account/user-module-access/module-access.module.ts new file mode 100644 index 0000000..0a0cd99 --- /dev/null +++ b/src/identity-and-account/user-module-access/module-access.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; +import { AccessUpdateService } from "src/identity-and-account/user-module-access/services/module-access-update.service"; +import { ModuleAccessController } from "src/identity-and-account/user-module-access/controllers/module-access.controller"; +import { AccessGetService } from "src/identity-and-account/user-module-access/services/module-access-get.service"; +import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; + +@Module({ + controllers: [ModuleAccessController], + providers: [AccessUpdateService, AccessGetService, EmailToIdResolver], + exports: [], +}) +export class ModuleAccessModule { } \ No newline at end of file diff --git a/src/identity-and-account/user-module-access/services/access-get.service.ts b/src/identity-and-account/user-module-access/services/module-access-get.service.ts similarity index 98% rename from src/identity-and-account/user-module-access/services/access-get.service.ts rename to src/identity-and-account/user-module-access/services/module-access-get.service.ts index 6f990f8..2d74e82 100644 --- a/src/identity-and-account/user-module-access/services/access-get.service.ts +++ b/src/identity-and-account/user-module-access/services/module-access-get.service.ts @@ -1,7 +1,7 @@ import { Injectable } from "@nestjs/common"; import { Result } from "src/common/errors/result-error.factory"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; -import { ModuleAccess } from "src/identity-and-account/user-module-access/dtos/acces.dto"; +import { ModuleAccess } from "src/identity-and-account/user-module-access/dtos/module-acces.dto"; import { PrismaService } from "src/prisma/prisma.service"; @Injectable() diff --git a/src/identity-and-account/user-module-access/services/access-update.service.ts b/src/identity-and-account/user-module-access/services/module-access-update.service.ts similarity index 98% rename from src/identity-and-account/user-module-access/services/access-update.service.ts rename to src/identity-and-account/user-module-access/services/module-access-update.service.ts index 818b013..f721b04 100644 --- a/src/identity-and-account/user-module-access/services/access-update.service.ts +++ b/src/identity-and-account/user-module-access/services/module-access-update.service.ts @@ -1,7 +1,7 @@ import { Injectable } from "@nestjs/common"; import { Result } from "src/common/errors/result-error.factory"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; -import { ModuleAccess } from "src/identity-and-account/user-module-access/dtos/acces.dto"; +import { ModuleAccess } from "src/identity-and-account/user-module-access/dtos/module-acces.dto"; import { PrismaService } from "src/prisma/prisma.service"; @Injectable() From 5f7f639c62f018917cca21ed3883088cf59fc712 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 26 Nov 2025 15:31:50 -0500 Subject: [PATCH 7/9] feat(preferences): added a getter function and modified schema to use boolean --- prisma/schema.prisma | 16 ++++----- .../controllers/preferences.controller.ts | 10 ++++-- .../preferences/dtos/preferences.dto.ts | 3 +- .../services/preferences.service.ts | 33 +++++++++++++++++-- .../controllers/module-access.controller.ts | 6 ++-- 5 files changed, 49 insertions(+), 19 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ebd0fa5..6aac4ef 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -41,7 +41,7 @@ model userModuleAccess { employee_list Boolean @default(false) //wich shows the lists of employee to show names, emails, titles and profile picture employee_management Boolean @default(false) //wich offers CRUD for employees, schedule_presets and manage module access personnal_profile Boolean @default(false) //wich governs profile details, preferances and dashboard access - blocked Boolean @default(false) + blocked Boolean @default(false) @@map("user_module_access") } @@ -347,14 +347,12 @@ model Preferences { user Users @relation("UserPreferences", fields: [user_id], references: [id]) user_id String @unique @db.Uuid - notifications Int @default(0) - dark_mode Int @default(0) - lang_switch Int @default(0) - lefty_mode Int @default(0) - - employee_list_display Int @default(0) - validation_display Int @default(0) - timesheet_display Int @default(0) + notifications Boolean @default(false) + dark_mode Boolean @default(false) + lang_switch Boolean @default(false) + lefty_mode Boolean @default(false) + employee_list_display Boolean @default(false) + approval_display Boolean @default(false) @@map("preferences") } diff --git a/src/identity-and-account/preferences/controllers/preferences.controller.ts b/src/identity-and-account/preferences/controllers/preferences.controller.ts index 64ecce9..e215581 100644 --- a/src/identity-and-account/preferences/controllers/preferences.controller.ts +++ b/src/identity-and-account/preferences/controllers/preferences.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Patch, Req } from "@nestjs/common"; +import { Body, Controller, Get, Patch, Query, Req } from "@nestjs/common"; import { PreferencesService } from "../services/preferences.service"; import { PreferencesDto } from "../dtos/preferences.dto"; import { Result } from "src/common/errors/result-error.factory"; @@ -7,7 +7,7 @@ import { Result } from "src/common/errors/result-error.factory"; export class PreferencesController { constructor(private readonly service: PreferencesService){} - @Patch('update_preferences') + @Patch('update') async updatePreferences( @Req()req, @Body() payload: PreferencesDto @@ -16,4 +16,10 @@ export class PreferencesController { return this.service.updatePreferences(email, payload); } + @Get() + async findPreferences(@Req() req, @Query() employee_email?:string) { + const email = req.user?.email; + return this.service.findPreferences(email, employee_email); + } + } \ No newline at end of file diff --git a/src/identity-and-account/preferences/dtos/preferences.dto.ts b/src/identity-and-account/preferences/dtos/preferences.dto.ts index 560b8a8..129e009 100644 --- a/src/identity-and-account/preferences/dtos/preferences.dto.ts +++ b/src/identity-and-account/preferences/dtos/preferences.dto.ts @@ -6,6 +6,5 @@ export class PreferencesDto { lang_switch: number; lefty_mode: number; employee_list_display: number; - validation_display: number; - timesheet_display: number; + approval_display: number; } \ No newline at end of file diff --git a/src/identity-and-account/preferences/services/preferences.service.ts b/src/identity-and-account/preferences/services/preferences.service.ts index ae5aea5..c3e59a0 100644 --- a/src/identity-and-account/preferences/services/preferences.service.ts +++ b/src/identity-and-account/preferences/services/preferences.service.ts @@ -10,9 +10,37 @@ export class PreferencesService { constructor( private readonly prisma: PrismaService, private readonly emailResolver: EmailToIdResolver, - ) { } + async findPreferences(email: string, employee_email?: string): Promise> { + const account_email = employee_email ?? email; + const user_id = await this.emailResolver.resolveUserIdWithEmail(account_email); + if (!user_id.success) return { success: false, error: 'EMPLOYEE_NOT_FOUND' }; + + const user_preferences = await this.prisma.preferences.findUnique({ + where: { user_id: user_id.data }, + select: { + dark_mode: true, + lang_switch: true, + lefty_mode: true, + notifications: true, + employee_list_display: true, + approval_display: true, + }, + }); + if (!user_preferences) return { success: false, error: 'PREFERENCES_NOT_FOUND' }; + + const preferences: PreferencesDto = { + dark_mode: user_preferences.dark_mode, + lang_switch: user_preferences.lang_switch, + lefty_mode: user_preferences.lefty_mode, + notifications: user_preferences.notifications, + employee_list_display: user_preferences.employee_list_display, + approval_display: user_preferences.approval_display, + }; + return { success: true, data: preferences }; + } + async updatePreferences(email: string, dto: PreferencesDto): Promise> { const user_id = await this.emailResolver.resolveUserIdWithEmail(email); if (!user_id.success) return { success: false, error: user_id.error } @@ -26,8 +54,7 @@ export class PreferencesService { lang_switch: dto.lang_switch, lefty_mode: dto.lefty_mode, employee_list_display: dto.employee_list_display, - validation_display: dto.validation_display, - timesheet_display: dto.timesheet_display, + approval_display: dto.approval_display, }, include: { user: true }, }) diff --git a/src/identity-and-account/user-module-access/controllers/module-access.controller.ts b/src/identity-and-account/user-module-access/controllers/module-access.controller.ts index 7d902c8..2aeeb8a 100644 --- a/src/identity-and-account/user-module-access/controllers/module-access.controller.ts +++ b/src/identity-and-account/user-module-access/controllers/module-access.controller.ts @@ -3,7 +3,7 @@ import { ModuleAccess } from "src/identity-and-account/user-module-access/dtos/m import { AccessGetService } from "src/identity-and-account/user-module-access/services/module-access-get.service"; import { AccessUpdateService } from "src/identity-and-account/user-module-access/services/module-access-update.service"; -@Controller() +@Controller('module_access') export class ModuleAccessController { constructor( private readonly getService: AccessGetService, @@ -19,7 +19,7 @@ export class ModuleAccessController { await this.getService.findModuleAccess(email, employee_email); }; - @Patch() + @Patch('update') async updateAccess( @Req() req, @Body() dto: ModuleAccess, @@ -29,7 +29,7 @@ export class ModuleAccessController { await this.updateService.updateModuleAccess(email, dto, employee_email); }; - @Patch() + @Patch('revoke') async revokeModuleAccess( @Req() req, @Query('employee_email') employee_email?: string From d7b1bab68fd21e65958c0abfbc4310723d171a81 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 26 Nov 2025 15:33:10 -0500 Subject: [PATCH 8/9] fix(preferences): minor type fix to dto --- docs/swagger/swagger-spec.json | 55 ++++++++++++++++++- .../preferences/dtos/preferences.dto.ts | 12 ++-- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 27f2ade..5ac9abe 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -537,7 +537,7 @@ ] } }, - "/preferences/update_preferences": { + "/preferences/update": { "patch": { "operationId": "PreferencesController_updatePreferences", "parameters": [], @@ -561,7 +561,21 @@ ] } }, - "/": { + "/preferences": { + "get": { + "operationId": "PreferencesController_findPreferences", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Preferences" + ] + } + }, + "/module_access": { "get": { "operationId": "ModuleAccessController_findAccess", "parameters": [ @@ -582,7 +596,42 @@ "tags": [ "ModuleAccess" ] - }, + } + }, + "/module_access/update": { + "patch": { + "operationId": "ModuleAccessController_updateAccess", + "parameters": [ + { + "name": "employee_email", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ModuleAccess" + } + } + } + }, + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "ModuleAccess" + ] + } + }, + "/module_access/revoke": { "patch": { "operationId": "ModuleAccessController_revokeModuleAccess", "parameters": [ diff --git a/src/identity-and-account/preferences/dtos/preferences.dto.ts b/src/identity-and-account/preferences/dtos/preferences.dto.ts index 129e009..7307e6d 100644 --- a/src/identity-and-account/preferences/dtos/preferences.dto.ts +++ b/src/identity-and-account/preferences/dtos/preferences.dto.ts @@ -1,10 +1,10 @@ import { IsInt } from "class-validator"; export class PreferencesDto { - notifications: number; - dark_mode: number; - lang_switch: number; - lefty_mode: number; - employee_list_display: number; - approval_display: number; + notifications: boolean; + dark_mode: boolean; + lang_switch: boolean; + lefty_mode: boolean; + employee_list_display: boolean; + approval_display: boolean; } \ No newline at end of file From 7c9f3cda6513d76ea78800138011ac4d1bc93dda Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 26 Nov 2025 16:32:56 -0500 Subject: [PATCH 9/9] feat(preferences): small update on prefrences table --- prisma/schema.prisma | 12 ++++---- .../preferences/dtos/preferences.dto.ts | 12 ++++---- .../services/preferences.service.ts | 30 +++++++++---------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6aac4ef..c9670e3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -347,12 +347,12 @@ model Preferences { user Users @relation("UserPreferences", fields: [user_id], references: [id]) user_id String @unique @db.Uuid - notifications Boolean @default(false) - dark_mode Boolean @default(false) - lang_switch Boolean @default(false) - lefty_mode Boolean @default(false) - employee_list_display Boolean @default(false) - approval_display Boolean @default(false) + notifications Int @default(0) + is_dark_mode Boolean @default(false) + display_language String @default("fr-FR") //'fr-FR' | 'en-CA'; + is_lefty_mode Boolean @default(false) + is_employee_list_grid Boolean @default(true) + is_timesheet_approval_grid Boolean @default(true) @@map("preferences") } diff --git a/src/identity-and-account/preferences/dtos/preferences.dto.ts b/src/identity-and-account/preferences/dtos/preferences.dto.ts index 7307e6d..4eb8b79 100644 --- a/src/identity-and-account/preferences/dtos/preferences.dto.ts +++ b/src/identity-and-account/preferences/dtos/preferences.dto.ts @@ -1,10 +1,10 @@ import { IsInt } from "class-validator"; export class PreferencesDto { - notifications: boolean; - dark_mode: boolean; - lang_switch: boolean; - lefty_mode: boolean; - employee_list_display: boolean; - approval_display: boolean; + notifications: number; + is_dark_mode: boolean | null; + display_language: string | 'fr-FR' | 'en-CA'; + is_lefty_mode: boolean; + is_employee_list_grid: boolean; + is_timesheet_approval_grid: boolean; } \ No newline at end of file diff --git a/src/identity-and-account/preferences/services/preferences.service.ts b/src/identity-and-account/preferences/services/preferences.service.ts index c3e59a0..674a706 100644 --- a/src/identity-and-account/preferences/services/preferences.service.ts +++ b/src/identity-and-account/preferences/services/preferences.service.ts @@ -20,23 +20,23 @@ export class PreferencesService { const user_preferences = await this.prisma.preferences.findUnique({ where: { user_id: user_id.data }, select: { - dark_mode: true, - lang_switch: true, - lefty_mode: true, notifications: true, - employee_list_display: true, - approval_display: true, + is_dark_mode: true, + display_language: true, + is_lefty_mode: true, + is_employee_list_grid: true, + is_timesheet_approval_grid: true, }, }); if (!user_preferences) return { success: false, error: 'PREFERENCES_NOT_FOUND' }; const preferences: PreferencesDto = { - dark_mode: user_preferences.dark_mode, - lang_switch: user_preferences.lang_switch, - lefty_mode: user_preferences.lefty_mode, + is_dark_mode: user_preferences.is_dark_mode, + display_language: user_preferences.display_language, + is_lefty_mode: user_preferences.is_lefty_mode, notifications: user_preferences.notifications, - employee_list_display: user_preferences.employee_list_display, - approval_display: user_preferences.approval_display, + is_employee_list_grid: user_preferences.is_employee_list_grid, + is_timesheet_approval_grid: user_preferences.is_timesheet_approval_grid, }; return { success: true, data: preferences }; } @@ -50,11 +50,11 @@ export class PreferencesService { where: { user_id: user_id.data }, data: { notifications: dto.notifications, - dark_mode: dto.dark_mode, - lang_switch: dto.lang_switch, - lefty_mode: dto.lefty_mode, - employee_list_display: dto.employee_list_display, - approval_display: dto.approval_display, + is_dark_mode: dto.is_dark_mode ?? undefined, + display_language: dto.display_language, + is_lefty_mode: dto.is_lefty_mode, + is_employee_list_grid: dto.is_employee_list_grid, + is_timesheet_approval_grid: dto.is_timesheet_approval_grid, }, include: { user: true }, })