feat(schedule_presets): module schedule_presets setup. Ajustments to seeders to match new realities
This commit is contained in:
parent
9b169d43c8
commit
83792e596a
|
|
@ -1072,6 +1072,68 @@
|
||||||
"pay-periods"
|
"pay-periods"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/schedule-presets/{email}": {
|
||||||
|
"put": {
|
||||||
|
"operationId": "SchedulePresetsController_upsert",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "email",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "action",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SchedulePresetsDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"SchedulePresets"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"operationId": "SchedulePresetsController_findListByEmail",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "email",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"SchedulePresets"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
|
|
@ -1584,6 +1646,10 @@
|
||||||
"label",
|
"label",
|
||||||
"employees_overview"
|
"employees_overview"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"SchedulePresetsDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Weekday" AS ENUM ('SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "preferences" ADD COLUMN "id" SERIAL NOT NULL,
|
||||||
|
ADD CONSTRAINT "preferences_pkey" PRIMARY KEY ("id");
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "schedule_presets" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"employee_id" INTEGER NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"is_default" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
|
||||||
|
CONSTRAINT "schedule_presets_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "schedule_preset_shifts" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"preset_id" INTEGER NOT NULL,
|
||||||
|
"bank_code_id" INTEGER NOT NULL,
|
||||||
|
"sort_order" INTEGER NOT NULL,
|
||||||
|
"start_time" TIME(0) NOT NULL,
|
||||||
|
"end_time" TIME(0) NOT NULL,
|
||||||
|
"is_remote" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"week_day" "Weekday" NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "schedule_preset_shifts_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "schedule_presets_employee_id_name_key" ON "schedule_presets"("employee_id", "name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "schedule_preset_shifts_preset_id_week_day_idx" ON "schedule_preset_shifts"("preset_id", "week_day");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "schedule_preset_shifts_preset_id_week_day_sort_order_key" ON "schedule_preset_shifts"("preset_id", "week_day", "sort_order");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "schedule_presets" ADD CONSTRAINT "schedule_presets_employee_id_fkey" FOREIGN KEY ("employee_id") REFERENCES "employees"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "schedule_preset_shifts" ADD CONSTRAINT "schedule_preset_shifts_preset_id_fkey" FOREIGN KEY ("preset_id") REFERENCES "schedule_presets"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "schedule_preset_shifts" ADD CONSTRAINT "schedule_preset_shifts_bank_code_id_fkey" FOREIGN KEY ("bank_code_id") REFERENCES "bank_codes"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
@ -3,41 +3,46 @@ import { PrismaClient, Roles } from '@prisma/client';
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
// base sans underscore, en string
|
// base sans underscore, en string
|
||||||
const BASE_PHONE = "1100000000";
|
const BASE_PHONE = '1100000000';
|
||||||
|
|
||||||
function emailFor(i: number) {
|
function emailFor(i: number) {
|
||||||
return `user${i + 1}@example.test`;
|
return `user${i + 1}@example.test`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const usersData: {
|
type UserSeed = {
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone_number: string;
|
phone_number: string;
|
||||||
residence?: string | null;
|
residence?: string | null;
|
||||||
role: Roles;
|
role: Roles;
|
||||||
}[] = [];
|
};
|
||||||
|
|
||||||
const firstNames = ['Alex','Sam','Chris','Jordan','Taylor','Morgan','Jamie','Robin','Avery','Casey'];
|
const usersData: UserSeed[] = [];
|
||||||
const lastNames = ['Smith','Johnson','Williams','Brown','Jones','Miller','Davis','Wilson','Taylor','Clark'];
|
|
||||||
|
|
||||||
const pick = <T>(arr: T[]) => arr[Math.floor(Math.random() * arr.length)];
|
const firstNames = ['Alex', 'Sam', 'Chris', 'Jordan', 'Taylor', 'Morgan', 'Jamie', 'Robin', 'Avery', 'Casey'];
|
||||||
|
const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller', 'Davis', 'Wilson', 'Taylor', 'Clark'];
|
||||||
|
|
||||||
// 40 employees, avec une distribution initiale
|
const pick = <T,>(arr: T[]) => arr[Math.floor(Math.random() * arr.length)];
|
||||||
const rolesForEmployees: Roles[] = [
|
|
||||||
Roles.ADMIN,
|
/**
|
||||||
...Array(4).fill(Roles.SUPERVISOR), // 4 superviseurs
|
* Objectif total: 50 users
|
||||||
Roles.HR,
|
* - 39 employés génériques (dont ADMIN=1, SUPERVISOR=3, HR=1, ACCOUNTING=1, EMPLOYEE=33)
|
||||||
Roles.ACCOUNTING,
|
* - +1 superviseur spécial "User Test" (=> 40 employés)
|
||||||
...Array(33).fill(Roles.EMPLOYEE),
|
* - 10 customers
|
||||||
|
*/
|
||||||
|
const rolesForEmployees39: Roles[] = [
|
||||||
|
Roles.ADMIN, // 1
|
||||||
|
...Array(3).fill(Roles.SUPERVISOR), // 3 supervisors (le 4e sera "User Test")
|
||||||
|
Roles.HR, // 1
|
||||||
|
Roles.ACCOUNTING, // 1
|
||||||
|
...Array(33).fill(Roles.EMPLOYEE), // 33
|
||||||
|
// total = 39
|
||||||
];
|
];
|
||||||
|
|
||||||
// --- Normalisation : forcer user5@example.test à SUPERVISOR ---
|
// --- 39 employés génériques: user1..user39@example.test
|
||||||
// user5 => index 4 (i = 4)
|
for (let i = 0; i < 39; i++) {
|
||||||
rolesForEmployees[4] = Roles.SUPERVISOR;
|
|
||||||
|
|
||||||
for (let i = 0; i < 40; i++) {
|
|
||||||
const fn = pick(firstNames);
|
const fn = pick(firstNames);
|
||||||
const ln = pick(lastNames);
|
const ln = pick(lastNames);
|
||||||
usersData.push({
|
usersData.push({
|
||||||
|
|
@ -46,12 +51,12 @@ async function main() {
|
||||||
email: emailFor(i),
|
email: emailFor(i),
|
||||||
phone_number: BASE_PHONE + i.toString(),
|
phone_number: BASE_PHONE + i.toString(),
|
||||||
residence: Math.random() < 0.5 ? 'QC' : 'ON',
|
residence: Math.random() < 0.5 ? 'QC' : 'ON',
|
||||||
role: rolesForEmployees[i],
|
role: rolesForEmployees39[i],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10 customers
|
// --- 10 customers: user40..user49@example.test
|
||||||
for (let i = 40; i < 50; i++) {
|
for (let i = 39; i < 49; i++) {
|
||||||
const fn = pick(firstNames);
|
const fn = pick(firstNames);
|
||||||
const ln = pick(lastNames);
|
const ln = pick(lastNames);
|
||||||
usersData.push({
|
usersData.push({
|
||||||
|
|
@ -64,29 +69,132 @@ async function main() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1) Insert (sans doublons)
|
// 1) Insert des 49 génériques (skipDuplicates pour rejouer le seed sans erreurs)
|
||||||
await prisma.users.createMany({ data: usersData, skipDuplicates: true });
|
await prisma.users.createMany({ data: usersData, skipDuplicates: true });
|
||||||
|
|
||||||
// 2) Validation/Correction post-insert :
|
// 2) Upsert du superviseur spécial "User Test"
|
||||||
// - garantir que user5@example.test est SUPERVISOR
|
const specialEmail = 'user@targointernet.com';
|
||||||
// - si jamais le projet avait un user avec la typo, on tente aussi de le corriger (fallback)
|
const specialUser = await prisma.users.upsert({
|
||||||
const targetEmails = ['user5@example.test', 'user5@examplte.tset'];
|
where: { email: specialEmail },
|
||||||
for (const email of targetEmails) {
|
update: {
|
||||||
try {
|
first_name: 'User',
|
||||||
await prisma.users.update({
|
last_name: 'Test',
|
||||||
where: { email },
|
role: Roles.SUPERVISOR,
|
||||||
data: { role: Roles.SUPERVISOR },
|
residence: 'QC',
|
||||||
|
phone_number: BASE_PHONE + '999',
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
first_name: 'User',
|
||||||
|
last_name: 'Test',
|
||||||
|
email: specialEmail,
|
||||||
|
role: Roles.SUPERVISOR,
|
||||||
|
residence: 'QC',
|
||||||
|
phone_number: BASE_PHONE + '999',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3) Créer/mettre à jour les entrées Employees pour tous les rôles employés
|
||||||
|
const employeeUsers = await prisma.users.findMany({
|
||||||
|
where: { role: { in: [Roles.ADMIN, Roles.SUPERVISOR, Roles.HR, Roles.ACCOUNTING, Roles.EMPLOYEE] } },
|
||||||
|
orderBy: { email: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstWorkDay = new Date('2025-01-06'); // à adapter à ton contexte
|
||||||
|
|
||||||
|
for (let i = 0; i < employeeUsers.length; i++) {
|
||||||
|
const u = employeeUsers[i];
|
||||||
|
await prisma.employees.upsert({
|
||||||
|
where: { user_id: u.id },
|
||||||
|
update: {
|
||||||
|
is_supervisor: u.role === Roles.SUPERVISOR,
|
||||||
|
job_title: u.role,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
user_id: u.id,
|
||||||
|
is_supervisor: u.role === Roles.SUPERVISOR,
|
||||||
|
external_payroll_id: 1000 + i, // à adapter
|
||||||
|
company_code: 1, // à adapter
|
||||||
|
first_work_day: firstWorkDay,
|
||||||
|
job_title: u.role,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Répartition des 33 EMPLOYEE sur 4 superviseurs: 8/8/8/9 (9 pour User Test)
|
||||||
|
const supervisors = await prisma.employees.findMany({
|
||||||
|
where: { is_supervisor: true, user: { role: Roles.SUPERVISOR } },
|
||||||
|
include: { user: true },
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const userTestSupervisor = supervisors.find((s) => s.user.email === specialEmail);
|
||||||
|
if (!userTestSupervisor) {
|
||||||
|
throw new Error('Employee(User Test) introuvable — vérifie le upsert Users/Employees.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const plainEmployees = await prisma.employees.findMany({
|
||||||
|
where: { is_supervisor: false, user: { role: Roles.EMPLOYEE } },
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Si la configuration est bien 4 superviseurs + 33 employés, on force 8/8/8/9 avec 9 pour User Test.
|
||||||
|
if (supervisors.length === 4 && plainEmployees.length === 33) {
|
||||||
|
const others = supervisors.filter((s) => s.id !== userTestSupervisor.id);
|
||||||
|
// ordre: autres (3) puis User Test en dernier (reçoit 9)
|
||||||
|
const ordered = [...others, userTestSupervisor];
|
||||||
|
|
||||||
|
const chunks = [
|
||||||
|
plainEmployees.slice(0, 8), // -> sup 0
|
||||||
|
plainEmployees.slice(8, 16), // -> sup 1
|
||||||
|
plainEmployees.slice(16, 24), // -> sup 2
|
||||||
|
plainEmployees.slice(24, 33), // -> sup 3 (User Test) = 9
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let b = 0; b < chunks.length; b++) {
|
||||||
|
const sup = ordered[b];
|
||||||
|
for (const emp of chunks[b]) {
|
||||||
|
await prisma.employees.update({
|
||||||
|
where: { id: emp.id },
|
||||||
|
data: { supervisor_id: sup.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// fallback: distribution round-robin si la config diffère
|
||||||
|
console.warn(
|
||||||
|
`Répartition fallback (round-robin). Supervisors=${supervisors.length}, Employees=${plainEmployees.length}`
|
||||||
|
);
|
||||||
|
const others = supervisors.filter((s) => s.id !== userTestSupervisor.id);
|
||||||
|
const ordered = [...others, userTestSupervisor];
|
||||||
|
for (let i = 0; i < plainEmployees.length; i++) {
|
||||||
|
const sup = ordered[i % ordered.length];
|
||||||
|
await prisma.employees.update({
|
||||||
|
where: { id: plainEmployees[i].id },
|
||||||
|
data: { supervisor_id: sup.id },
|
||||||
});
|
});
|
||||||
console.log(`✓ Validation: ${email} est SUPERVISOR`);
|
|
||||||
break; // on s'arrête dès qu'on a corrigé l'un des deux
|
|
||||||
} catch {
|
|
||||||
// ignore si non trouvé, on tente l'autre
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Petite vérif : compter les superviseurs pour sanity check
|
// 5) Sanity checks
|
||||||
|
const totalUsers = await prisma.users.count();
|
||||||
const supCount = await prisma.users.count({ where: { role: Roles.SUPERVISOR } });
|
const supCount = await prisma.users.count({ where: { role: Roles.SUPERVISOR } });
|
||||||
console.log(`✓ Users: 50 rows (40 employees, 10 customers) — SUPERVISORS: ${supCount}`);
|
const empCount = await prisma.users.count({ where: { role: Roles.EMPLOYEE } });
|
||||||
|
|
||||||
|
const countForUserTest = await prisma.employees.count({
|
||||||
|
where: { supervisor_id: userTestSupervisor.id, is_supervisor: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✓ Users total: ${totalUsers} (attendu 50)`);
|
||||||
|
console.log(`✓ Supervisors: ${supCount} (attendu 4)`);
|
||||||
|
console.log(`✓ Employees : ${empCount} (attendu 33)`);
|
||||||
|
console.log(`✓ Employés sous User Test: ${countForUserTest} (attendu 9)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().finally(() => prisma.$disconnect());
|
main()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ function centsToAmountString(cents: number): string {
|
||||||
const c = abs % 100;
|
const c = abs % 100;
|
||||||
return `${sign}${dollars}.${c.toString().padStart(2, '0')}`;
|
return `${sign}${dollars}.${c.toString().padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tire un multiple de STEP_CENTS entre minCents et maxCents (inclus)
|
// Tire un multiple de STEP_CENTS entre minCents et maxCents (inclus)
|
||||||
function rndQuantizedCents(minCents: number, maxCents: number, step = STEP_CENTS): number {
|
function rndQuantizedCents(minCents: number, maxCents: number, step = STEP_CENTS): number {
|
||||||
const qmin = Math.ceil(minCents / step);
|
const qmin = Math.ceil(minCents / step);
|
||||||
|
|
@ -65,7 +66,7 @@ async function getOrCreateTimesheet(employee_id: number, start_date: Date) {
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Codes d'EXPENSES (exemples)
|
// Codes d'EXPENSES (exemples)
|
||||||
const BANKS = ['G517', 'G503', 'G502', 'G202', 'G234'] as const;
|
const BANKS = ['G517', 'G503', 'G502', 'G202'] as const;
|
||||||
|
|
||||||
// Précharger les bank codes
|
// Précharger les bank codes
|
||||||
const bcRows = await prisma.bankCodes.findMany({
|
const bcRows = await prisma.bankCodes.findMany({
|
||||||
|
|
@ -119,19 +120,16 @@ async function main() {
|
||||||
// Montants (cents) quantisés à 25¢ => aucun flottant binaire plus tard
|
// Montants (cents) quantisés à 25¢ => aucun flottant binaire plus tard
|
||||||
let amount: string;
|
let amount: string;
|
||||||
switch (code) {
|
switch (code) {
|
||||||
case 'G503': // petites fournitures
|
case 'G503': // kilométrage
|
||||||
amount = rndAmount(1000, 7500); // 10.00 à 75.00
|
amount = rndAmount(1000, 7500); // 10.00 à 75.00
|
||||||
break;
|
break;
|
||||||
case 'G502': // repas
|
case 'G502': // per_diem
|
||||||
amount = rndAmount(1500, 3000); // 15.00 à 30.00
|
amount = rndAmount(1500, 3000); // 15.00 à 30.00
|
||||||
break;
|
break;
|
||||||
case 'G202': // essence
|
case 'G202': // allowance /prime de garde
|
||||||
amount = rndAmount(2000, 15000); // 20.00 à 150.00
|
amount = rndAmount(2000, 15000); // 20.00 à 150.00
|
||||||
break;
|
break;
|
||||||
case 'G234': // hébergement
|
case 'G517': // expenses
|
||||||
amount = rndAmount(6000, 25000); // 60.00 à 250.00
|
|
||||||
break;
|
|
||||||
case 'G517': // péages / divers
|
|
||||||
default:
|
default:
|
||||||
amount = rndAmount(500, 5000); // 5.00 à 50.00
|
amount = rndAmount(500, 5000); // 5.00 à 50.00
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ model Users {
|
||||||
employees_archive EmployeesArchive[] @relation("UsersToEmployeesToArchive")
|
employees_archive EmployeesArchive[] @relation("UsersToEmployeesToArchive")
|
||||||
customer_archive CustomersArchive[] @relation("UserToCustomersToArchive")
|
customer_archive CustomersArchive[] @relation("UserToCustomersToArchive")
|
||||||
preferences Preferences? @relation("UserPreferences")
|
preferences Preferences? @relation("UserPreferences")
|
||||||
|
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,6 +37,9 @@ model Employees {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
user Users @relation("UserEmployee", fields: [user_id], references: [id])
|
user Users @relation("UserEmployee", fields: [user_id], references: [id])
|
||||||
user_id String @unique @db.Uuid
|
user_id String @unique @db.Uuid
|
||||||
|
supervisor Employees? @relation("EmployeeSupervisor", fields: [supervisor_id], references: [id])
|
||||||
|
supervisor_id Int?
|
||||||
|
|
||||||
external_payroll_id Int
|
external_payroll_id Int
|
||||||
company_code Int
|
company_code Int
|
||||||
first_work_day DateTime @db.Date
|
first_work_day DateTime @db.Date
|
||||||
|
|
@ -43,14 +47,13 @@ model Employees {
|
||||||
job_title String?
|
job_title String?
|
||||||
is_supervisor Boolean @default(false)
|
is_supervisor Boolean @default(false)
|
||||||
|
|
||||||
supervisor Employees? @relation("EmployeeSupervisor", fields: [supervisor_id], references: [id])
|
|
||||||
supervisor_id Int?
|
|
||||||
crew Employees[] @relation("EmployeeSupervisor")
|
|
||||||
|
|
||||||
|
crew Employees[] @relation("EmployeeSupervisor")
|
||||||
archive EmployeesArchive[] @relation("EmployeeToArchive")
|
archive EmployeesArchive[] @relation("EmployeeToArchive")
|
||||||
timesheet Timesheets[] @relation("TimesheetEmployee")
|
timesheet Timesheets[] @relation("TimesheetEmployee")
|
||||||
leave_request LeaveRequests[] @relation("LeaveRequestEmployee")
|
leave_request LeaveRequests[] @relation("LeaveRequestEmployee")
|
||||||
supervisor_archive EmployeesArchive[] @relation("EmployeeSupervisorToArchive")
|
supervisor_archive EmployeesArchive[] @relation("EmployeeSupervisorToArchive")
|
||||||
|
schedule_presets SchedulePresets[] @relation("SchedulePreset")
|
||||||
|
|
||||||
@@map("employees")
|
@@map("employees")
|
||||||
}
|
}
|
||||||
|
|
@ -59,10 +62,12 @@ model EmployeesArchive {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
employee Employees @relation("EmployeeToArchive", fields: [employee_id], references: [id])
|
employee Employees @relation("EmployeeToArchive", fields: [employee_id], references: [id])
|
||||||
employee_id Int
|
employee_id Int
|
||||||
archived_at DateTime @default(now())
|
|
||||||
|
|
||||||
user_id String @db.Uuid
|
user_id String @db.Uuid
|
||||||
user Users @relation("UsersToEmployeesToArchive", fields: [user_id], references: [id])
|
user Users @relation("UsersToEmployeesToArchive", fields: [user_id], references: [id])
|
||||||
|
supervisor Employees? @relation("EmployeeSupervisorToArchive", fields: [supervisor_id], references: [id])
|
||||||
|
supervisor_id Int?
|
||||||
|
|
||||||
|
archived_at DateTime @default(now())
|
||||||
first_name String
|
first_name String
|
||||||
last_name String
|
last_name String
|
||||||
job_title String?
|
job_title String?
|
||||||
|
|
@ -71,8 +76,6 @@ model EmployeesArchive {
|
||||||
company_code Int
|
company_code Int
|
||||||
first_work_day DateTime @db.Date
|
first_work_day DateTime @db.Date
|
||||||
last_work_day DateTime @db.Date
|
last_work_day DateTime @db.Date
|
||||||
supervisor_id Int?
|
|
||||||
supervisor Employees? @relation("EmployeeSupervisorToArchive", fields: [supervisor_id], references: [id])
|
|
||||||
|
|
||||||
@@map("employees_archive")
|
@@map("employees_archive")
|
||||||
}
|
}
|
||||||
|
|
@ -92,10 +95,10 @@ model CustomersArchive {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
customer Customers @relation("CustomerToArchive", fields: [customer_id], references: [id])
|
customer Customers @relation("CustomerToArchive", fields: [customer_id], references: [id])
|
||||||
customer_id Int
|
customer_id Int
|
||||||
archived_at DateTime @default(now())
|
|
||||||
user_id String @db.Uuid
|
|
||||||
user Users @relation("UserToCustomersToArchive", fields: [user_id], references: [id])
|
user Users @relation("UserToCustomersToArchive", fields: [user_id], references: [id])
|
||||||
|
user_id String @db.Uuid
|
||||||
|
|
||||||
|
archived_at DateTime @default(now())
|
||||||
invoice_id Int? @unique
|
invoice_id Int? @unique
|
||||||
|
|
||||||
@@map("customers_archive")
|
@@map("customers_archive")
|
||||||
|
|
@ -107,12 +110,13 @@ model LeaveRequests {
|
||||||
employee_id Int
|
employee_id Int
|
||||||
bank_code BankCodes @relation("LeaveRequestBankCodes", fields: [bank_code_id], references: [id])
|
bank_code BankCodes @relation("LeaveRequestBankCodes", fields: [bank_code_id], references: [id])
|
||||||
bank_code_id Int
|
bank_code_id Int
|
||||||
leave_type LeaveTypes
|
|
||||||
date DateTime @db.Date
|
|
||||||
payable_hours Decimal? @db.Decimal(5,2)
|
|
||||||
requested_hours Decimal? @db.Decimal(5,2)
|
|
||||||
comment String
|
comment String
|
||||||
|
date DateTime @db.Date
|
||||||
|
payable_hours Decimal? @db.Decimal(5, 2)
|
||||||
|
requested_hours Decimal? @db.Decimal(5, 2)
|
||||||
approval_status LeaveApprovalStatus @default(PENDING)
|
approval_status LeaveApprovalStatus @default(PENDING)
|
||||||
|
leave_type LeaveTypes
|
||||||
|
|
||||||
archive LeaveRequestsArchive[] @relation("LeaveRequestToArchive")
|
archive LeaveRequestsArchive[] @relation("LeaveRequestToArchive")
|
||||||
|
|
||||||
|
|
@ -125,13 +129,14 @@ model LeaveRequestsArchive {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
leave_request LeaveRequests @relation("LeaveRequestToArchive", fields: [leave_request_id], references: [id])
|
leave_request LeaveRequests @relation("LeaveRequestToArchive", fields: [leave_request_id], references: [id])
|
||||||
leave_request_id Int
|
leave_request_id Int
|
||||||
|
|
||||||
archived_at DateTime @default(now())
|
archived_at DateTime @default(now())
|
||||||
employee_id Int
|
employee_id Int
|
||||||
leave_type LeaveTypes
|
|
||||||
date DateTime @db.Date
|
date DateTime @db.Date
|
||||||
payable_hours Decimal? @db.Decimal(5,2)
|
payable_hours Decimal? @db.Decimal(5, 2)
|
||||||
requested_hours Decimal? @db.Decimal(5,2)
|
requested_hours Decimal? @db.Decimal(5, 2)
|
||||||
comment String
|
comment String
|
||||||
|
leave_type LeaveTypes
|
||||||
approval_status LeaveApprovalStatus
|
approval_status LeaveApprovalStatus
|
||||||
|
|
||||||
@@unique([leave_request_id])
|
@@unique([leave_request_id])
|
||||||
|
|
@ -143,10 +148,10 @@ model LeaveRequestsArchive {
|
||||||
view PayPeriods {
|
view PayPeriods {
|
||||||
pay_year Int
|
pay_year Int
|
||||||
pay_period_no Int
|
pay_period_no Int
|
||||||
|
label String
|
||||||
payday DateTime @db.Date
|
payday DateTime @db.Date
|
||||||
period_start DateTime @db.Date
|
period_start DateTime @db.Date
|
||||||
period_end DateTime @db.Date
|
period_end DateTime @db.Date
|
||||||
label String
|
|
||||||
|
|
||||||
@@map("pay_period")
|
@@map("pay_period")
|
||||||
}
|
}
|
||||||
|
|
@ -155,6 +160,7 @@ model Timesheets {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
employee Employees @relation("TimesheetEmployee", fields: [employee_id], references: [id])
|
employee Employees @relation("TimesheetEmployee", fields: [employee_id], references: [id])
|
||||||
employee_id Int
|
employee_id Int
|
||||||
|
|
||||||
start_date DateTime @db.Date
|
start_date DateTime @db.Date
|
||||||
is_approved Boolean @default(false)
|
is_approved Boolean @default(false)
|
||||||
|
|
||||||
|
|
@ -170,25 +176,71 @@ model TimesheetsArchive {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
timesheet Timesheets @relation("TimesheetsToArchive", fields: [timesheet_id], references: [id])
|
timesheet Timesheets @relation("TimesheetsToArchive", fields: [timesheet_id], references: [id])
|
||||||
timesheet_id Int
|
timesheet_id Int
|
||||||
archive_at DateTime @default(now())
|
|
||||||
employee_id Int
|
employee_id Int
|
||||||
is_approved Boolean
|
is_approved Boolean
|
||||||
|
archive_at DateTime @default(now())
|
||||||
|
|
||||||
@@map("timesheets_archive")
|
@@map("timesheets_archive")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
model SchedulePresets {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
employee Employees @relation("SchedulePreset", fields: [employee_id], references: [id])
|
||||||
|
employee_id Int
|
||||||
|
|
||||||
|
name String
|
||||||
|
is_default Boolean @default(false)
|
||||||
|
|
||||||
|
shifts SchedulePresetShifts[] @relation("SchedulePresetShiftsSchedulePreset")
|
||||||
|
|
||||||
|
@@unique([employee_id, name], name: "unique_preset_name_per_employee")
|
||||||
|
@@map("schedule_presets")
|
||||||
|
}
|
||||||
|
|
||||||
|
model SchedulePresetShifts {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
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_id Int
|
||||||
|
|
||||||
|
sort_order Int
|
||||||
|
start_time DateTime @db.Time(0)
|
||||||
|
end_time DateTime @db.Time(0)
|
||||||
|
is_remote Boolean @default(false)
|
||||||
|
week_day Weekday
|
||||||
|
|
||||||
|
@@unique([preset_id, week_day, sort_order], name: "unique_preset_shift_per_day_order")
|
||||||
|
@@index([preset_id, week_day])
|
||||||
|
@@map("schedule_preset_shifts")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
model Shifts {
|
model Shifts {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
timesheet Timesheets @relation("ShiftTimesheet", fields: [timesheet_id], references: [id])
|
timesheet Timesheets @relation("ShiftTimesheet", fields: [timesheet_id], references: [id])
|
||||||
timesheet_id Int
|
timesheet_id Int
|
||||||
bank_code BankCodes @relation("ShiftBankCodes", fields: [bank_code_id], references: [id])
|
bank_code BankCodes @relation("ShiftBankCodes", fields: [bank_code_id], references: [id])
|
||||||
bank_code_id Int
|
bank_code_id Int
|
||||||
comment String?
|
|
||||||
date DateTime @db.Date
|
date DateTime @db.Date
|
||||||
start_time DateTime @db.Time(0)
|
start_time DateTime @db.Time(0)
|
||||||
end_time DateTime @db.Time(0)
|
end_time DateTime @db.Time(0)
|
||||||
is_approved Boolean @default(false)
|
is_approved Boolean @default(false)
|
||||||
is_remote Boolean @default(false)
|
is_remote Boolean @default(false)
|
||||||
|
comment String?
|
||||||
|
|
||||||
archive ShiftsArchive[] @relation("ShiftsToArchive")
|
archive ShiftsArchive[] @relation("ShiftsToArchive")
|
||||||
|
|
||||||
|
|
@ -199,13 +251,14 @@ model ShiftsArchive {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
shift Shifts @relation("ShiftsToArchive", fields: [shift_id], references: [id])
|
shift Shifts @relation("ShiftsToArchive", fields: [shift_id], references: [id])
|
||||||
shift_id Int
|
shift_id Int
|
||||||
archive_at DateTime @default(now())
|
|
||||||
timesheet_id Int
|
|
||||||
bank_code_id Int
|
|
||||||
comment String?
|
|
||||||
date DateTime @db.Date
|
date DateTime @db.Date
|
||||||
start_time DateTime @db.Time(0)
|
start_time DateTime @db.Time(0)
|
||||||
end_time DateTime @db.Time(0)
|
end_time DateTime @db.Time(0)
|
||||||
|
timesheet_id Int
|
||||||
|
bank_code_id Int
|
||||||
|
comment String?
|
||||||
|
archive_at DateTime @default(now())
|
||||||
|
|
||||||
@@map("shifts_archive")
|
@@map("shifts_archive")
|
||||||
}
|
}
|
||||||
|
|
@ -220,6 +273,7 @@ model BankCodes {
|
||||||
shifts Shifts[] @relation("ShiftBankCodes")
|
shifts Shifts[] @relation("ShiftBankCodes")
|
||||||
expenses Expenses[] @relation("ExpenseBankCodes")
|
expenses Expenses[] @relation("ExpenseBankCodes")
|
||||||
leaveRequests LeaveRequests[] @relation("LeaveRequestBankCodes")
|
leaveRequests LeaveRequests[] @relation("LeaveRequestBankCodes")
|
||||||
|
SchedulePresetShifts SchedulePresetShifts[] @relation("SchedulePresetShiftsBankCodes")
|
||||||
|
|
||||||
@@map("bank_codes")
|
@@map("bank_codes")
|
||||||
}
|
}
|
||||||
|
|
@ -230,14 +284,15 @@ model Expenses {
|
||||||
timesheet_id Int
|
timesheet_id Int
|
||||||
bank_code BankCodes @relation("ExpenseBankCodes", fields: [bank_code_id], references: [id])
|
bank_code BankCodes @relation("ExpenseBankCodes", fields: [bank_code_id], references: [id])
|
||||||
bank_code_id Int
|
bank_code_id Int
|
||||||
|
attachment_record Attachments? @relation("ExpenseAttachment", fields: [attachment], references: [id], onDelete: SetNull)
|
||||||
|
attachment Int?
|
||||||
|
|
||||||
date DateTime @db.Date
|
date DateTime @db.Date
|
||||||
amount Decimal @db.Money
|
amount Decimal @db.Money
|
||||||
mileage Decimal?
|
mileage Decimal?
|
||||||
attachment Int?
|
|
||||||
attachment_record Attachments? @relation("ExpenseAttachment", fields: [attachment], references: [id], onDelete: SetNull)
|
|
||||||
comment String
|
comment String
|
||||||
is_approved Boolean @default(false)
|
|
||||||
supervisor_comment String?
|
supervisor_comment String?
|
||||||
|
is_approved Boolean @default(false)
|
||||||
|
|
||||||
archive ExpensesArchive[] @relation("ExpensesToArchive")
|
archive ExpensesArchive[] @relation("ExpensesToArchive")
|
||||||
|
|
||||||
|
|
@ -248,14 +303,15 @@ model ExpensesArchive {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
expense Expenses @relation("ExpensesToArchive", fields: [expense_id], references: [id])
|
expense Expenses @relation("ExpensesToArchive", fields: [expense_id], references: [id])
|
||||||
expense_id Int
|
expense_id Int
|
||||||
|
attachment_record Attachments? @relation("ExpenseArchiveAttachment", fields: [attachment], references: [id], onDelete: SetNull)
|
||||||
|
attachment Int?
|
||||||
|
|
||||||
timesheet_id Int
|
timesheet_id Int
|
||||||
archived_at DateTime @default(now())
|
archived_at DateTime @default(now())
|
||||||
bank_code_id Int
|
bank_code_id Int
|
||||||
date DateTime @db.Date
|
date DateTime @db.Date
|
||||||
amount Decimal? @db.Money
|
amount Decimal? @db.Money
|
||||||
mileage Decimal?
|
mileage Decimal?
|
||||||
attachment Int?
|
|
||||||
attachment_record Attachments? @relation("ExpenseArchiveAttachment", fields: [attachment], references: [id], onDelete: SetNull)
|
|
||||||
comment String?
|
comment String?
|
||||||
is_approved Boolean
|
is_approved Boolean
|
||||||
supervisor_comment String?
|
supervisor_comment String?
|
||||||
|
|
@ -296,8 +352,9 @@ model Blobs {
|
||||||
|
|
||||||
model Attachments {
|
model Attachments {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
sha256 String @db.Char(64)
|
|
||||||
blob Blobs @relation(fields: [sha256], references: [sha256], onUpdate: Cascade)
|
blob Blobs @relation(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
|
owner_type String //EXPENSES ou éventuellement autre chose comme scan ONU ou photos d'employés, etc
|
||||||
owner_id String //expense_id, employee_id, etc
|
owner_id String //expense_id, employee_id, etc
|
||||||
original_name String
|
original_name String
|
||||||
|
|
@ -318,6 +375,7 @@ model Preferences {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
user Users @relation("UserPreferences", fields: [user_id], references: [id])
|
user Users @relation("UserPreferences", fields: [user_id], references: [id])
|
||||||
user_id String @unique @db.Uuid
|
user_id String @unique @db.Uuid
|
||||||
|
|
||||||
notifications Boolean @default(false)
|
notifications Boolean @default(false)
|
||||||
dark_mode Boolean @default(false)
|
dark_mode Boolean @default(false)
|
||||||
lang_switch Boolean @default(false)
|
lang_switch Boolean @default(false)
|
||||||
|
|
@ -326,7 +384,6 @@ model Preferences {
|
||||||
@@map("preferences")
|
@@map("preferences")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum AttachmentStatus {
|
enum AttachmentStatus {
|
||||||
ACTIVE
|
ACTIVE
|
||||||
DELETED
|
DELETED
|
||||||
|
|
@ -373,3 +430,13 @@ enum LeaveApprovalStatus {
|
||||||
|
|
||||||
@@map("leave_approval_status")
|
@@map("leave_approval_status")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Weekday {
|
||||||
|
SUN
|
||||||
|
MON
|
||||||
|
TUE
|
||||||
|
WED
|
||||||
|
THU
|
||||||
|
FRI
|
||||||
|
SAT
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { ConfigModule } from '@nestjs/config';
|
||||||
import { APP_FILTER, APP_PIPE } from '@nestjs/core';
|
import { APP_FILTER, APP_PIPE } from '@nestjs/core';
|
||||||
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||||
import { ValidationError } from 'class-validator';
|
import { ValidationError } from 'class-validator';
|
||||||
|
import { SchedulePresetsModule } from './modules/schedule-presets/schedule-presets.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -43,7 +44,8 @@ import { ValidationError } from 'class-validator';
|
||||||
OauthSessionsModule,
|
OauthSessionsModule,
|
||||||
PayperiodsModule,
|
PayperiodsModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(), //cronjobs
|
||||||
|
SchedulePresetsModule,
|
||||||
ShiftsModule,
|
ShiftsModule,
|
||||||
TimesheetsModule,
|
TimesheetsModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Body, Controller, Get, NotFoundException, Param, Put, Query } from "@nestjs/common";
|
||||||
|
import { SchedulePresetsDto } from "../dtos/create-schedule-presets.dto";
|
||||||
|
import { SchedulePresetsCommandService } from "../services/schedule-presets-command.service";
|
||||||
|
import { UpsertAction } from "src/modules/shared/types/upsert-actions.types";
|
||||||
|
import { SchedulePresetsQueryService } from "../services/schedule-presets-query.service";
|
||||||
|
|
||||||
|
@Controller('schedule-presets')
|
||||||
|
export class SchedulePresetsController {
|
||||||
|
constructor(
|
||||||
|
private readonly commandService: SchedulePresetsCommandService,
|
||||||
|
private readonly queryService: SchedulePresetsQueryService,
|
||||||
|
){}
|
||||||
|
|
||||||
|
@Put(':email')
|
||||||
|
async upsert(
|
||||||
|
@Param('email') email: string,
|
||||||
|
@Query('action') action: UpsertAction,
|
||||||
|
@Body() dto: SchedulePresetsDto,
|
||||||
|
) {
|
||||||
|
const actions: UpsertAction[] = ['create','update','delete'];
|
||||||
|
if(!actions) throw new NotFoundException(`No action found for ${actions}`)
|
||||||
|
return this.commandService.upsertSchedulePreset(email, action, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':email')
|
||||||
|
async findListByEmail(
|
||||||
|
@Param('email') email: string,
|
||||||
|
) {
|
||||||
|
return this.queryService.findSchedulePresetsByEmail(email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { IsBoolean, IsEnum, IsInt, IsOptional, IsString, Matches, Min } from "class-validator";
|
||||||
|
import { Weekday } from "@prisma/client";
|
||||||
|
|
||||||
|
export class SchedulePresetShiftsDto {
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { ArrayMinSize, IsArray, IsBoolean, IsEmail, IsOptional, IsString } from "class-validator";
|
||||||
|
import { SchedulePresetShiftsDto } from "./create-schedule-preset-shifts.dto";
|
||||||
|
|
||||||
|
export class SchedulePresetsDto {
|
||||||
|
@IsString()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
is_default: boolean;
|
||||||
|
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMinSize(1)
|
||||||
|
preset_shifts: SchedulePresetShiftsDto[];
|
||||||
|
}
|
||||||
23
src/modules/schedule-presets/schedule-presets.module.ts
Normal file
23
src/modules/schedule-presets/schedule-presets.module.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { SchedulePresetsCommandService } from "./services/schedule-presets-command.service";
|
||||||
|
import { SchedulePresetsQueryService } from "./services/schedule-presets-query.service";
|
||||||
|
import { SchedulePresetsController } from "./controller/schedule-presets.controller";
|
||||||
|
import { EmployeeIdEmailResolver } from "../shared/utils/resolve-email-id.utils";
|
||||||
|
import { BankCodesResolver } from "../shared/utils/resolve-bank-type-id.utils";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [],
|
||||||
|
controllers: [SchedulePresetsController],
|
||||||
|
providers: [
|
||||||
|
PrismaService,
|
||||||
|
SchedulePresetsCommandService,
|
||||||
|
SchedulePresetsQueryService,
|
||||||
|
EmployeeIdEmailResolver,
|
||||||
|
BankCodesResolver,
|
||||||
|
],
|
||||||
|
exports:[
|
||||||
|
SchedulePresetsCommandService,
|
||||||
|
SchedulePresetsQueryService
|
||||||
|
],
|
||||||
|
}) export class SchedulePresetsModule {}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
import { EmployeeIdEmailResolver } from "src/modules/shared/utils/resolve-email-id.utils";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { ApplyResult } from "../types/schedule-presets.types";
|
||||||
|
|
||||||
|
// @Injectable()
|
||||||
|
// export class SchedulePresetsApplyService {
|
||||||
|
// constructor(
|
||||||
|
// private readonly prisma: PrismaService,
|
||||||
|
// private readonly emailResolver: EmployeeIdEmailResolver,
|
||||||
|
// ) {}
|
||||||
|
|
||||||
|
// async applyToTimesheet(
|
||||||
|
// email: string,
|
||||||
|
// preset_name: string,
|
||||||
|
// start_date_iso: string,
|
||||||
|
// ): Promise<ApplyResult> {
|
||||||
|
// if(!preset_name?.trim()) throw new BadRequestException('A preset_name is required');
|
||||||
|
// if(!DATE_ISO_FORMAT.test(start_date_iso)) throw new BadRequestException('start_date must be of format :YYYY-MM-DD');
|
||||||
|
|
||||||
|
// const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
|
// if(!employee_id) throw new NotFoundException(`Employee with email: ${email} not found`);
|
||||||
|
|
||||||
|
// const preset = await this.prisma.schedulePresets.findFirst({
|
||||||
|
// where: { employee_id, name: preset_name },
|
||||||
|
// include: {
|
||||||
|
// shifts: {
|
||||||
|
// orderBy: [{ week_day: 'asc'}, { sort_order: 'asc'}],
|
||||||
|
// select: {
|
||||||
|
// week_day: true,
|
||||||
|
// sort_order: true,
|
||||||
|
// start_time: true,
|
||||||
|
// end_time: true,
|
||||||
|
// is_remote: true,
|
||||||
|
// bank_code_id: true,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// if(!preset) throw new NotFoundException(`Preset ${preset} not found`);
|
||||||
|
|
||||||
|
// const start_date = new Date(`${start_date_iso}T00:00:00.000Z`)
|
||||||
|
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
@ -0,0 +1,238 @@
|
||||||
|
import { BadRequestException, ConflictException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils";
|
||||||
|
import { EmployeeIdEmailResolver } from "src/modules/shared/utils/resolve-email-id.utils";
|
||||||
|
import { UpsertAction } from "src/modules/shared/types/upsert-actions.types";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { SchedulePresetsDto } from "../dtos/create-schedule-presets.dto";
|
||||||
|
import { Prisma, Weekday } from "@prisma/client";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SchedulePresetsCommandService {
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly emailResolver: EmployeeIdEmailResolver,
|
||||||
|
private readonly typeResolver : BankCodesResolver,
|
||||||
|
){}
|
||||||
|
|
||||||
|
//_________________________________________________________________
|
||||||
|
// MASTER CRUD FUNCTION
|
||||||
|
//_________________________________________________________________
|
||||||
|
async upsertSchedulePreset(
|
||||||
|
email: string,
|
||||||
|
action: UpsertAction,
|
||||||
|
dto: SchedulePresetsDto,
|
||||||
|
): Promise<{
|
||||||
|
action: UpsertAction;
|
||||||
|
preset_id?: number;
|
||||||
|
total_items?: number;
|
||||||
|
}>{
|
||||||
|
if(!dto.name?.trim()) throw new BadRequestException(`A Name is required`);
|
||||||
|
|
||||||
|
//resolve employee_id using email
|
||||||
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
|
if(!employee_id) throw new NotFoundException(`employee with email: ${email} not found`);
|
||||||
|
|
||||||
|
//DELETE
|
||||||
|
if(action === 'delete') {
|
||||||
|
return this.deletePreset(employee_id, dto.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!Array.isArray(dto.preset_shifts) || dto.preset_shifts.length === 0) {
|
||||||
|
throw new BadRequestException(`Empty array, no detected shifts`);
|
||||||
|
}
|
||||||
|
const shifts_data = await this.resolveAndBuildPresetShifts(dto);
|
||||||
|
|
||||||
|
//CREATE AND UPDATE
|
||||||
|
if(action === 'create') {
|
||||||
|
return this.createPreset(employee_id, dto, shifts_data);
|
||||||
|
} else if (action === 'update') {
|
||||||
|
return this.updatePreset(employee_id, dto, shifts_data);
|
||||||
|
}
|
||||||
|
throw new BadRequestException(`Unknown action: ${ action }`);
|
||||||
|
}
|
||||||
|
|
||||||
|
//_________________________________________________________________
|
||||||
|
// CREATE
|
||||||
|
//_________________________________________________________________
|
||||||
|
private async createPreset(
|
||||||
|
employee_id: number,
|
||||||
|
dto: SchedulePresetsDto,
|
||||||
|
shifts_data: Prisma.SchedulePresetShiftsCreateWithoutPresetInput[],
|
||||||
|
): Promise<{
|
||||||
|
action: UpsertAction;
|
||||||
|
preset_id: number;
|
||||||
|
total_items: number;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const result = await this.prisma.$transaction(async (tx)=> {
|
||||||
|
if(dto.is_default) {
|
||||||
|
await tx.schedulePresets.updateMany({
|
||||||
|
where: { employee_id, is_default: true },
|
||||||
|
data: { is_default: false },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const created = await tx.schedulePresets.create({
|
||||||
|
data: {
|
||||||
|
employee_id,
|
||||||
|
name: dto.name,
|
||||||
|
is_default: !!dto.is_default,
|
||||||
|
shifts: { create: shifts_data},
|
||||||
|
},
|
||||||
|
include: { shifts: true },
|
||||||
|
});
|
||||||
|
return created;
|
||||||
|
});
|
||||||
|
return { action: 'create', preset_id: result.id, total_items: result.shifts.length };
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if(error instanceof Prisma.PrismaClientKnownRequestError){
|
||||||
|
if(error?.code === 'P2002') {
|
||||||
|
throw new ConflictException(`The name ${dto.name} is already used for another schedule preset`);
|
||||||
|
}
|
||||||
|
if (error.code === 'P2003' || error.code === 'P2011') {
|
||||||
|
throw new ConflictException('Invalid constraint on preset shifts');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//_________________________________________________________________
|
||||||
|
// UPDATE
|
||||||
|
//_________________________________________________________________
|
||||||
|
private async updatePreset(
|
||||||
|
employee_id: number,
|
||||||
|
dto: SchedulePresetsDto,
|
||||||
|
shifts_data: Prisma.SchedulePresetShiftsCreateWithoutPresetInput[],
|
||||||
|
): Promise<{
|
||||||
|
action: UpsertAction;
|
||||||
|
preset_id?: number;
|
||||||
|
total_items?: number;
|
||||||
|
}> {
|
||||||
|
const existing = await this.prisma.schedulePresets.findFirst({
|
||||||
|
where: { employee_id, name: dto.name },
|
||||||
|
select: { id:true, is_default: true },
|
||||||
|
});
|
||||||
|
if(!existing) throw new NotFoundException(`Preset "${dto.name}" not found`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.prisma.$transaction(async (tx) => {
|
||||||
|
if(typeof dto.is_default === 'boolean'){
|
||||||
|
if(dto.is_default) {
|
||||||
|
await tx.schedulePresets.updateMany({
|
||||||
|
where: { 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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } });
|
||||||
|
|
||||||
|
const create_many_data: Prisma.SchedulePresetShiftsCreateManyInput[] =
|
||||||
|
shifts_data.map((shift)=> {
|
||||||
|
if(!shift.bank_code || !('connect' in shift.bank_code) || typeof shift.bank_code.connect?.id !=='number'){
|
||||||
|
throw new NotFoundException(`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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await tx.schedulePresetShifts.createMany({data: create_many_data});
|
||||||
|
|
||||||
|
const count = await tx.schedulePresetShifts.count({ where: { preset_id: existing.id } });
|
||||||
|
return { id: existing.id, total: count };
|
||||||
|
});
|
||||||
|
return { action: 'update', preset_id: result.id, total_items: result.total };
|
||||||
|
} catch (error: unknown){
|
||||||
|
if(error instanceof Prisma.PrismaClientKnownRequestError){
|
||||||
|
if(error?.code === 'P2003' || error?.code === 'P2011') {
|
||||||
|
throw new ConflictException(`Invalid constraint on preset shifts`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//_________________________________________________________________
|
||||||
|
// DELETE
|
||||||
|
//_________________________________________________________________
|
||||||
|
private async deletePreset(
|
||||||
|
employee_id: number,
|
||||||
|
name: string,
|
||||||
|
): Promise<{
|
||||||
|
action: UpsertAction;
|
||||||
|
preset_id?: number;
|
||||||
|
total_items?: number;
|
||||||
|
}> {
|
||||||
|
const existing = await this.prisma.schedulePresets.findFirst({
|
||||||
|
where: { employee_id, name },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if(!existing) throw new NotFoundException(`Preset "${name}" not found`);
|
||||||
|
await this.prisma.$transaction(async (tx) => {
|
||||||
|
await tx.schedulePresetShifts.deleteMany({ where: { preset_id: existing.id } });
|
||||||
|
await tx.schedulePresets.delete({where: { id: existing.id } });
|
||||||
|
});
|
||||||
|
return { action: 'delete', preset_id: existing.id, total_items: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
//PRIVATE HELPER
|
||||||
|
//resolve bank_code_id using type and convert hours to TIME and valid shifts end/start
|
||||||
|
private async resolveAndBuildPresetShifts(
|
||||||
|
dto: SchedulePresetsDto
|
||||||
|
): Promise<Prisma.SchedulePresetShiftsCreateWithoutPresetInput[]>{
|
||||||
|
|
||||||
|
if(!dto.preset_shifts?.length) throw new NotFoundException(`Empty or preset shifts not found`);
|
||||||
|
|
||||||
|
const types = Array.from(new Set(dto.preset_shifts.map((shift)=> shift.type)));
|
||||||
|
const bank_code_set = new Map<string, number>();
|
||||||
|
|
||||||
|
for (const type of types) {
|
||||||
|
const { id } = await this.typeResolver.findByType(type);
|
||||||
|
bank_code_set.set(type, id)
|
||||||
|
}
|
||||||
|
const toTime = (hhmm: string) => new Date(`1970-01-01T${hhmm}:00.000Z`);
|
||||||
|
|
||||||
|
const pair_set = new Set<string>();
|
||||||
|
for (const shift of dto.preset_shifts) {
|
||||||
|
const key = `${shift.week_day}:${shift.sort_order}`;
|
||||||
|
if (pair_set.has(key)) {
|
||||||
|
throw new ConflictException(`Duplicate shift for day/order (${shift.week_day}, ${shift.sort_order})`);
|
||||||
|
}
|
||||||
|
pair_set.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const items: Prisma.SchedulePresetShiftsCreateWithoutPresetInput[] = dto.preset_shifts.map((shift)=> {
|
||||||
|
const bank_code_id = bank_code_set.get(shift.type);
|
||||||
|
if(!bank_code_id) throw new NotFoundException(`Bank code not found for type ${shift.type}`);
|
||||||
|
if (!shift.start_time || !shift.end_time) {
|
||||||
|
throw new BadRequestException(`start_time and end_time are required for (${shift.week_day}, ${shift.sort_order})`);
|
||||||
|
}
|
||||||
|
const start = toTime(shift.start_time);
|
||||||
|
const end = toTime(shift.end_time);
|
||||||
|
if(end.getTime() <= start.getTime()) {
|
||||||
|
throw new ConflictException(`end_time must be > start_time ( day: ${shift.week_day}, order: ${shift.sort_order})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
week_day: shift.week_day as Weekday,
|
||||||
|
sort_order: shift.sort_order,
|
||||||
|
bank_code: { connect: { id: bank_code_id} },
|
||||||
|
start_time: start,
|
||||||
|
end_time: end,
|
||||||
|
is_remote: !!shift.is_remote,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
import { EmployeeIdEmailResolver } from "src/modules/shared/utils/resolve-email-id.utils";
|
||||||
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { PresetResponse, ShiftResponse } from "../types/schedule-presets.types";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SchedulePresetsQueryService {
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly emailResolver: EmployeeIdEmailResolver,
|
||||||
|
){}
|
||||||
|
|
||||||
|
async findSchedulePresetsByEmail(email:string): Promise<PresetResponse[]> {
|
||||||
|
const employee_id = await this.emailResolver.findIdByEmail(email);
|
||||||
|
if(!employee_id) throw new NotFoundException(`Employee with email: ${email} not found`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const presets = await this.prisma.schedulePresets.findMany({
|
||||||
|
where: { employee_id },
|
||||||
|
orderBy: [{is_default: 'desc' }, { name: 'asc' }],
|
||||||
|
include: {
|
||||||
|
shifts: {
|
||||||
|
orderBy: [{week_day:'asc'}, { sort_order: 'asc'}],
|
||||||
|
include: { bank_code: { select: { type: true } } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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<ShiftResponse>((shift)=> ({
|
||||||
|
week_day: shift.week_day,
|
||||||
|
sort_order: shift.sort_order,
|
||||||
|
start_time: hhmm(shift.start_time),
|
||||||
|
end_time: hhmm(shift.end_time),
|
||||||
|
is_remote: shift.is_remote,
|
||||||
|
type: shift.bank_code?.type,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
return response;
|
||||||
|
} catch ( error: unknown) {
|
||||||
|
if(error instanceof Prisma.PrismaClientKnownRequestError) {}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
21
src/modules/schedule-presets/types/schedule-presets.types.ts
Normal file
21
src/modules/schedule-presets/types/schedule-presets.types.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
export type ShiftResponse = {
|
||||||
|
week_day: string;
|
||||||
|
sort_order: number;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
is_remote: boolean;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PresetResponse = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
is_default: boolean;
|
||||||
|
shifts: ShiftResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApplyResult = {
|
||||||
|
timesheet_id: number;
|
||||||
|
created: number;
|
||||||
|
skipped: number;
|
||||||
|
}
|
||||||
2
src/modules/shared/constants/regex.constant.ts
Normal file
2
src/modules/shared/constants/regex.constant.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
const HH_MM_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/;
|
||||||
|
const DATE_ISO_FORMAT = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
1
src/modules/shared/types/upsert-actions.types.ts
Normal file
1
src/modules/shared/types/upsert-actions.types.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export type UpsertAction = 'create' | 'update' | 'delete';
|
||||||
Loading…
Reference in New Issue
Block a user