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"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/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": {
|
||||
|
|
@ -1584,6 +1646,10 @@
|
|||
"label",
|
||||
"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();
|
||||
|
||||
// base sans underscore, en string
|
||||
const BASE_PHONE = "1100000000";
|
||||
const BASE_PHONE = '1100000000';
|
||||
|
||||
function emailFor(i: number) {
|
||||
return `user${i + 1}@example.test`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const usersData: {
|
||||
type UserSeed = {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
phone_number: string;
|
||||
residence?: string | null;
|
||||
role: Roles;
|
||||
}[] = [];
|
||||
};
|
||||
|
||||
const firstNames = ['Alex','Sam','Chris','Jordan','Taylor','Morgan','Jamie','Robin','Avery','Casey'];
|
||||
const lastNames = ['Smith','Johnson','Williams','Brown','Jones','Miller','Davis','Wilson','Taylor','Clark'];
|
||||
const usersData: UserSeed[] = [];
|
||||
|
||||
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 rolesForEmployees: Roles[] = [
|
||||
Roles.ADMIN,
|
||||
...Array(4).fill(Roles.SUPERVISOR), // 4 superviseurs
|
||||
Roles.HR,
|
||||
Roles.ACCOUNTING,
|
||||
...Array(33).fill(Roles.EMPLOYEE),
|
||||
const pick = <T,>(arr: T[]) => arr[Math.floor(Math.random() * arr.length)];
|
||||
|
||||
/**
|
||||
* Objectif total: 50 users
|
||||
* - 39 employés génériques (dont ADMIN=1, SUPERVISOR=3, HR=1, ACCOUNTING=1, EMPLOYEE=33)
|
||||
* - +1 superviseur spécial "User Test" (=> 40 employés)
|
||||
* - 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 ---
|
||||
// user5 => index 4 (i = 4)
|
||||
rolesForEmployees[4] = Roles.SUPERVISOR;
|
||||
|
||||
for (let i = 0; i < 40; i++) {
|
||||
// --- 39 employés génériques: user1..user39@example.test
|
||||
for (let i = 0; i < 39; i++) {
|
||||
const fn = pick(firstNames);
|
||||
const ln = pick(lastNames);
|
||||
usersData.push({
|
||||
|
|
@ -46,12 +51,12 @@ async function main() {
|
|||
email: emailFor(i),
|
||||
phone_number: BASE_PHONE + i.toString(),
|
||||
residence: Math.random() < 0.5 ? 'QC' : 'ON',
|
||||
role: rolesForEmployees[i],
|
||||
role: rolesForEmployees39[i],
|
||||
});
|
||||
}
|
||||
|
||||
// 10 customers
|
||||
for (let i = 40; i < 50; i++) {
|
||||
// --- 10 customers: user40..user49@example.test
|
||||
for (let i = 39; i < 49; i++) {
|
||||
const fn = pick(firstNames);
|
||||
const ln = pick(lastNames);
|
||||
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 });
|
||||
|
||||
// 2) Validation/Correction post-insert :
|
||||
// - garantir que user5@example.test est SUPERVISOR
|
||||
// - si jamais le projet avait un user avec la typo, on tente aussi de le corriger (fallback)
|
||||
const targetEmails = ['user5@example.test', 'user5@examplte.tset'];
|
||||
for (const email of targetEmails) {
|
||||
try {
|
||||
await prisma.users.update({
|
||||
where: { email },
|
||||
data: { role: Roles.SUPERVISOR },
|
||||
// 2) Upsert du superviseur spécial "User Test"
|
||||
const specialEmail = 'user@targointernet.com';
|
||||
const specialUser = await prisma.users.upsert({
|
||||
where: { email: specialEmail },
|
||||
update: {
|
||||
first_name: 'User',
|
||||
last_name: 'Test',
|
||||
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 } });
|
||||
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;
|
||||
return `${sign}${dollars}.${c.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// Tire un multiple de STEP_CENTS entre minCents et maxCents (inclus)
|
||||
function rndQuantizedCents(minCents: number, maxCents: number, step = STEP_CENTS): number {
|
||||
const qmin = Math.ceil(minCents / step);
|
||||
|
|
@ -65,7 +66,7 @@ async function getOrCreateTimesheet(employee_id: number, start_date: Date) {
|
|||
|
||||
async function main() {
|
||||
// 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
|
||||
const bcRows = await prisma.bankCodes.findMany({
|
||||
|
|
@ -119,19 +120,16 @@ async function main() {
|
|||
// Montants (cents) quantisés à 25¢ => aucun flottant binaire plus tard
|
||||
let amount: string;
|
||||
switch (code) {
|
||||
case 'G503': // petites fournitures
|
||||
case 'G503': // kilométrage
|
||||
amount = rndAmount(1000, 7500); // 10.00 à 75.00
|
||||
break;
|
||||
case 'G502': // repas
|
||||
case 'G502': // per_diem
|
||||
amount = rndAmount(1500, 3000); // 15.00 à 30.00
|
||||
break;
|
||||
case 'G202': // essence
|
||||
case 'G202': // allowance /prime de garde
|
||||
amount = rndAmount(2000, 15000); // 20.00 à 150.00
|
||||
break;
|
||||
case 'G234': // hébergement
|
||||
amount = rndAmount(6000, 25000); // 60.00 à 250.00
|
||||
break;
|
||||
case 'G517': // péages / divers
|
||||
case 'G517': // expenses
|
||||
default:
|
||||
amount = rndAmount(500, 5000); // 5.00 à 50.00
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -23,19 +23,23 @@ model Users {
|
|||
residence String?
|
||||
role Roles @default(GUEST)
|
||||
|
||||
employee Employees? @relation("UserEmployee")
|
||||
customer Customers? @relation("UserCustomer")
|
||||
oauth_sessions OAuthSessions[] @relation("UserOAuthSessions")
|
||||
employees_archive EmployeesArchive[] @relation("UsersToEmployeesToArchive")
|
||||
customer_archive CustomersArchive[] @relation("UserToCustomersToArchive")
|
||||
preferences Preferences? @relation("UserPreferences")
|
||||
employee Employees? @relation("UserEmployee")
|
||||
customer Customers? @relation("UserCustomer")
|
||||
oauth_sessions OAuthSessions[] @relation("UserOAuthSessions")
|
||||
employees_archive EmployeesArchive[] @relation("UsersToEmployeesToArchive")
|
||||
customer_archive CustomersArchive[] @relation("UserToCustomersToArchive")
|
||||
preferences Preferences? @relation("UserPreferences")
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model Employees {
|
||||
id Int @id @default(autoincrement())
|
||||
user Users @relation("UserEmployee", fields: [user_id], references: [id])
|
||||
user_id String @unique @db.Uuid
|
||||
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
|
||||
|
|
@ -43,26 +47,27 @@ model Employees {
|
|||
job_title String?
|
||||
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")
|
||||
timesheet Timesheets[] @relation("TimesheetEmployee")
|
||||
leave_request LeaveRequests[] @relation("LeaveRequestEmployee")
|
||||
supervisor_archive EmployeesArchive[] @relation("EmployeeSupervisorToArchive")
|
||||
schedule_presets SchedulePresets[] @relation("SchedulePreset")
|
||||
|
||||
@@map("employees")
|
||||
}
|
||||
|
||||
model EmployeesArchive {
|
||||
id Int @id @default(autoincrement())
|
||||
employee Employees @relation("EmployeeToArchive", fields: [employee_id], references: [id])
|
||||
employee_id Int
|
||||
archived_at DateTime @default(now())
|
||||
id Int @id @default(autoincrement())
|
||||
employee Employees @relation("EmployeeToArchive", fields: [employee_id], references: [id])
|
||||
employee_id Int
|
||||
user_id String @db.Uuid
|
||||
user Users @relation("UsersToEmployeesToArchive", fields: [user_id], references: [id])
|
||||
supervisor Employees? @relation("EmployeeSupervisorToArchive", fields: [supervisor_id], references: [id])
|
||||
supervisor_id Int?
|
||||
|
||||
user_id String @db.Uuid
|
||||
user Users @relation("UsersToEmployeesToArchive", fields: [user_id], references: [id])
|
||||
archived_at DateTime @default(now())
|
||||
first_name String
|
||||
last_name String
|
||||
job_title String?
|
||||
|
|
@ -71,8 +76,6 @@ model EmployeesArchive {
|
|||
company_code Int
|
||||
first_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")
|
||||
}
|
||||
|
|
@ -92,27 +95,28 @@ model CustomersArchive {
|
|||
id Int @id @default(autoincrement())
|
||||
customer Customers @relation("CustomerToArchive", fields: [customer_id], references: [id])
|
||||
customer_id Int
|
||||
archived_at DateTime @default(now())
|
||||
user_id String @db.Uuid
|
||||
user Users @relation("UserToCustomersToArchive", fields: [user_id], references: [id])
|
||||
user_id String @db.Uuid
|
||||
|
||||
invoice_id Int? @unique
|
||||
archived_at DateTime @default(now())
|
||||
invoice_id Int? @unique
|
||||
|
||||
@@map("customers_archive")
|
||||
}
|
||||
|
||||
model LeaveRequests {
|
||||
id Int @id @default(autoincrement())
|
||||
employee Employees @relation("LeaveRequestEmployee", fields: [employee_id], references: [id])
|
||||
employee_id Int
|
||||
bank_code BankCodes @relation("LeaveRequestBankCodes", fields: [bank_code_id], references: [id])
|
||||
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)
|
||||
id Int @id @default(autoincrement())
|
||||
employee Employees @relation("LeaveRequestEmployee", fields: [employee_id], references: [id])
|
||||
employee_id Int
|
||||
bank_code BankCodes @relation("LeaveRequestBankCodes", fields: [bank_code_id], references: [id])
|
||||
bank_code_id Int
|
||||
|
||||
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)
|
||||
leave_type LeaveTypes
|
||||
|
||||
archive LeaveRequestsArchive[] @relation("LeaveRequestToArchive")
|
||||
|
||||
|
|
@ -122,16 +126,17 @@ model LeaveRequests {
|
|||
}
|
||||
|
||||
model LeaveRequestsArchive {
|
||||
id Int @id @default(autoincrement())
|
||||
leave_request LeaveRequests @relation("LeaveRequestToArchive", fields: [leave_request_id], references: [id])
|
||||
id Int @id @default(autoincrement())
|
||||
leave_request LeaveRequests @relation("LeaveRequestToArchive", fields: [leave_request_id], references: [id])
|
||||
leave_request_id Int
|
||||
archived_at DateTime @default(now())
|
||||
|
||||
archived_at DateTime @default(now())
|
||||
employee_id Int
|
||||
leave_type LeaveTypes
|
||||
date DateTime @db.Date
|
||||
payable_hours Decimal? @db.Decimal(5,2)
|
||||
requested_hours Decimal? @db.Decimal(5,2)
|
||||
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])
|
||||
|
|
@ -141,12 +146,12 @@ model LeaveRequestsArchive {
|
|||
|
||||
//pay-period vue
|
||||
view PayPeriods {
|
||||
pay_year Int
|
||||
pay_period_no Int
|
||||
payday DateTime @db.Date
|
||||
period_start DateTime @db.Date
|
||||
period_end DateTime @db.Date
|
||||
label String
|
||||
pay_year Int
|
||||
pay_period_no Int
|
||||
label String
|
||||
payday DateTime @db.Date
|
||||
period_start DateTime @db.Date
|
||||
period_end DateTime @db.Date
|
||||
|
||||
@@map("pay_period")
|
||||
}
|
||||
|
|
@ -155,8 +160,9 @@ model Timesheets {
|
|||
id Int @id @default(autoincrement())
|
||||
employee Employees @relation("TimesheetEmployee", fields: [employee_id], references: [id])
|
||||
employee_id Int
|
||||
start_date DateTime @db.Date
|
||||
is_approved Boolean @default(false)
|
||||
|
||||
start_date DateTime @db.Date
|
||||
is_approved Boolean @default(false)
|
||||
|
||||
shift Shifts[] @relation("ShiftTimesheet")
|
||||
expense Expenses[] @relation("ExpensesTimesheet")
|
||||
|
|
@ -170,25 +176,71 @@ model TimesheetsArchive {
|
|||
id Int @id @default(autoincrement())
|
||||
timesheet Timesheets @relation("TimesheetsToArchive", fields: [timesheet_id], references: [id])
|
||||
timesheet_id Int
|
||||
archive_at DateTime @default(now())
|
||||
|
||||
employee_id Int
|
||||
is_approved Boolean
|
||||
archive_at DateTime @default(now())
|
||||
|
||||
@@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 {
|
||||
id Int @id @default(autoincrement())
|
||||
timesheet Timesheets @relation("ShiftTimesheet", fields: [timesheet_id], references: [id])
|
||||
timesheet_id Int
|
||||
bank_code BankCodes @relation("ShiftBankCodes", fields: [bank_code_id], references: [id])
|
||||
bank_code_id Int
|
||||
comment String?
|
||||
date DateTime @db.Date
|
||||
start_time DateTime @db.Time(0)
|
||||
end_time DateTime @db.Time(0)
|
||||
is_approved Boolean @default(false)
|
||||
is_remote Boolean @default(false)
|
||||
|
||||
date DateTime @db.Date
|
||||
start_time DateTime @db.Time(0)
|
||||
end_time DateTime @db.Time(0)
|
||||
is_approved Boolean @default(false)
|
||||
is_remote Boolean @default(false)
|
||||
comment String?
|
||||
|
||||
archive ShiftsArchive[] @relation("ShiftsToArchive")
|
||||
|
||||
|
|
@ -196,16 +248,17 @@ model Shifts {
|
|||
}
|
||||
|
||||
model ShiftsArchive {
|
||||
id Int @id @default(autoincrement())
|
||||
shift Shifts @relation("ShiftsToArchive", fields: [shift_id], references: [id])
|
||||
shift_id Int
|
||||
archive_at DateTime @default(now())
|
||||
timesheet_id Int
|
||||
bank_code_id Int
|
||||
comment String?
|
||||
id Int @id @default(autoincrement())
|
||||
shift Shifts @relation("ShiftsToArchive", fields: [shift_id], references: [id])
|
||||
shift_id Int
|
||||
|
||||
date DateTime @db.Date
|
||||
start_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")
|
||||
}
|
||||
|
|
@ -217,27 +270,29 @@ model BankCodes {
|
|||
modifier Float
|
||||
bank_code String
|
||||
|
||||
shifts Shifts[] @relation("ShiftBankCodes")
|
||||
expenses Expenses[] @relation("ExpenseBankCodes")
|
||||
leaveRequests LeaveRequests[] @relation("LeaveRequestBankCodes")
|
||||
shifts Shifts[] @relation("ShiftBankCodes")
|
||||
expenses Expenses[] @relation("ExpenseBankCodes")
|
||||
leaveRequests LeaveRequests[] @relation("LeaveRequestBankCodes")
|
||||
SchedulePresetShifts SchedulePresetShifts[] @relation("SchedulePresetShiftsBankCodes")
|
||||
|
||||
@@map("bank_codes")
|
||||
}
|
||||
|
||||
model Expenses {
|
||||
id Int @id @default(autoincrement())
|
||||
timesheet Timesheets @relation("ExpensesTimesheet", fields: [timesheet_id], references: [id])
|
||||
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 BankCodes @relation("ExpenseBankCodes", fields: [bank_code_id], references: [id])
|
||||
bank_code_id Int
|
||||
date DateTime @db.Date
|
||||
amount Decimal @db.Money
|
||||
mileage Decimal?
|
||||
attachment Int?
|
||||
attachment_record Attachments? @relation("ExpenseAttachment", fields: [attachment], references: [id], onDelete: SetNull)
|
||||
attachment Int?
|
||||
|
||||
date DateTime @db.Date
|
||||
amount Decimal @db.Money
|
||||
mileage Decimal?
|
||||
comment String
|
||||
is_approved Boolean @default(false)
|
||||
supervisor_comment String?
|
||||
is_approved Boolean @default(false)
|
||||
|
||||
archive ExpensesArchive[] @relation("ExpensesToArchive")
|
||||
|
||||
|
|
@ -245,17 +300,18 @@ model Expenses {
|
|||
}
|
||||
|
||||
model ExpensesArchive {
|
||||
id Int @id @default(autoincrement())
|
||||
expense Expenses @relation("ExpensesToArchive", fields: [expense_id], references: [id])
|
||||
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.Money
|
||||
mileage Decimal?
|
||||
attachment Int?
|
||||
attachment_record Attachments? @relation("ExpenseArchiveAttachment", fields: [attachment], references: [id], onDelete: SetNull)
|
||||
comment String?
|
||||
is_approved Boolean
|
||||
supervisor_comment String?
|
||||
|
|
@ -282,7 +338,7 @@ model OAuthSessions {
|
|||
}
|
||||
|
||||
model Blobs {
|
||||
sha256 String @id @db.Char(64)
|
||||
sha256 String @id @db.Char(64)
|
||||
size Int
|
||||
mime String
|
||||
storage_path String
|
||||
|
|
@ -295,19 +351,20 @@ model Blobs {
|
|||
}
|
||||
|
||||
model Attachments {
|
||||
id Int @id @default(autoincrement())
|
||||
sha256 String @db.Char(64)
|
||||
blob Blobs @relation(fields: [sha256], references: [sha256], onUpdate: Cascade)
|
||||
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
|
||||
id Int @id @default(autoincrement())
|
||||
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_id String //expense_id, employee_id, etc
|
||||
original_name String
|
||||
status AttachmentStatus @default(ACTIVE)
|
||||
status AttachmentStatus @default(ACTIVE)
|
||||
retention_policy RetentionPolicy
|
||||
created_by String
|
||||
created_at DateTime @default(now())
|
||||
created_at DateTime @default(now())
|
||||
|
||||
expenses Expenses[] @relation("ExpenseAttachment")
|
||||
expenses_archive ExpensesArchive[] @relation("ExpenseArchiveAttachment")
|
||||
expenses Expenses[] @relation("ExpenseAttachment")
|
||||
expenses_archive ExpensesArchive[] @relation("ExpenseArchiveAttachment")
|
||||
|
||||
@@index([owner_type, owner_id, created_at])
|
||||
@@index([sha256])
|
||||
|
|
@ -315,9 +372,10 @@ model Attachments {
|
|||
}
|
||||
|
||||
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 Boolean @default(false)
|
||||
dark_mode Boolean @default(false)
|
||||
lang_switch Boolean @default(false)
|
||||
|
|
@ -326,7 +384,6 @@ model Preferences {
|
|||
@@map("preferences")
|
||||
}
|
||||
|
||||
|
||||
enum AttachmentStatus {
|
||||
ACTIVE
|
||||
DELETED
|
||||
|
|
@ -373,3 +430,13 @@ enum LeaveApprovalStatus {
|
|||
|
||||
@@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 { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||
import { ValidationError } from 'class-validator';
|
||||
import { SchedulePresetsModule } from './modules/schedule-presets/schedule-presets.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -43,7 +44,8 @@ import { ValidationError } from 'class-validator';
|
|||
OauthSessionsModule,
|
||||
PayperiodsModule,
|
||||
PrismaModule,
|
||||
ScheduleModule.forRoot(),
|
||||
ScheduleModule.forRoot(), //cronjobs
|
||||
SchedulePresetsModule,
|
||||
ShiftsModule,
|
||||
TimesheetsModule,
|
||||
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