Merge branch 'main' of git.targo.ca:Targo/targo_backend into auth-session-fullstack
did something it didn't like, not sure whadid something it didn't like, not sure whatt
This commit is contained in:
commit
406233b2a3
File diff suppressed because it is too large
Load Diff
78
package-lock.json
generated
78
package-lock.json
generated
|
|
@ -51,7 +51,7 @@
|
|||
"globals": "^16.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prisma": "^6.14.0",
|
||||
"prisma": "^6.16.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
|
|
@ -3148,9 +3148,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@prisma/config": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.14.0.tgz",
|
||||
"integrity": "sha512-IwC7o5KNNGhmblLs23swnfBjADkacBb7wvyDXUWLwuvUQciKJZqyecU0jw0d7JRkswrj+XTL8fdr0y2/VerKQQ==",
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.3.tgz",
|
||||
"integrity": "sha512-VlsLnG4oOuKGGMToEeVaRhoTBZu5H3q51jTQXb/diRags3WV0+BQK5MolJTtP6G7COlzoXmWeS11rNBtvg+qFQ==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"c12": "3.1.0",
|
||||
|
|
@ -3160,48 +3160,48 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.14.0.tgz",
|
||||
"integrity": "sha512-j4Lf+y+5QIJgQD4sJWSbkOD7geKx9CakaLp/TyTy/UDu9Wo0awvWCBH/BAxTHUaCpIl9USA5VS/KJhDqKJSwug==",
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.3.tgz",
|
||||
"integrity": "sha512-89DdqWtdKd7qoc9/qJCKLTazj3W3zPEiz0hc7HfZdpjzm21c7orOUB5oHWJsG+4KbV4cWU5pefq3CuDVYF9vgA==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.14.0.tgz",
|
||||
"integrity": "sha512-LhJjqsALFEcoAtF07nSaOkVguaxw/ZsgfROIYZ8bAZDobe7y8Wy+PkYQaPOK1iLSsFgV2MhCO/eNrI1gdSOj6w==",
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.3.tgz",
|
||||
"integrity": "sha512-b+Rl4nzQDcoqe6RIpSHv8f5lLnwdDGvXhHjGDiokObguAAv/O1KaX1Oc69mBW/GFWKQpCkOraobLjU6s1h8HGg==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.14.0",
|
||||
"@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
|
||||
"@prisma/fetch-engine": "6.14.0",
|
||||
"@prisma/get-platform": "6.14.0"
|
||||
"@prisma/debug": "6.16.3",
|
||||
"@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
||||
"@prisma/fetch-engine": "6.16.3",
|
||||
"@prisma/get-platform": "6.16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49.tgz",
|
||||
"integrity": "sha512-EgN9ODJpiX45yvwcngoStp3uQPJ3l+AEVoQ6dMMO2QvmwIlnxfApzKmJQExzdo7/hqQANrz5txHJdGYHzOnGHA==",
|
||||
"version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a.tgz",
|
||||
"integrity": "sha512-fftRmosBex48Ph1v2ll1FrPpirwtPZpNkE5CDCY1Lw2SD2ctyrLlVlHiuxDAAlALwWBOkPbAll4+EaqdGuMhJw==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.14.0.tgz",
|
||||
"integrity": "sha512-MPzYPOKMENYOaY3AcAbaKrfvXVlvTc6iHmTXsp9RiwCX+bPyfDMqMFVUSVXPYrXnrvEzhGHfyiFy0PRLHPysNg==",
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.3.tgz",
|
||||
"integrity": "sha512-bUoRIkVaI+CCaVGrSfcKev0/Mk4ateubqWqGZvQ9uCqFv2ENwWIR3OeNuGin96nZn5+SkebcD7RGgKr/+mJelw==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.14.0",
|
||||
"@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
|
||||
"@prisma/get-platform": "6.14.0"
|
||||
"@prisma/debug": "6.16.3",
|
||||
"@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
||||
"@prisma/get-platform": "6.16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.14.0.tgz",
|
||||
"integrity": "sha512-7VjuxKNwjnBhKfqPpMeWiHEa2sVjYzmHdl1slW6STuUCe9QnOY0OY1ljGSvz6wpG4U8DfbDqkG1yofd/1GINww==",
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.3.tgz",
|
||||
"integrity": "sha512-X1LxiFXinJ4iQehrodGp0f66Dv6cDL0GbRlcCoLtSu6f4Wi+hgo7eND/afIs5029GQLgNWKZ46vn8hjyXTsHLA==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "6.14.0"
|
||||
"@prisma/debug": "6.16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@scarf/scarf": {
|
||||
|
|
@ -9450,15 +9450,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/nypm": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz",
|
||||
"integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==",
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
|
||||
"integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"citty": "^0.1.6",
|
||||
"consola": "^3.4.2",
|
||||
"pathe": "^2.0.3",
|
||||
"pkg-types": "^2.2.0",
|
||||
"pkg-types": "^2.3.0",
|
||||
"tinyexec": "^1.0.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -9967,9 +9967,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"confbox": "^0.2.2",
|
||||
|
|
@ -10049,14 +10049,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.14.0.tgz",
|
||||
"integrity": "sha512-QEuCwxu+Uq9BffFw7in8In+WfbSUN0ewnaSUKloLkbJd42w6EyFckux4M0f7VwwHlM3A8ssaz4OyniCXlsn0WA==",
|
||||
"version": "6.16.3",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.3.tgz",
|
||||
"integrity": "sha512-4tJq3KB9WRshH5+QmzOLV54YMkNlKOtLKaSdvraI5kC/axF47HuOw6zDM8xrxJ6s9o2WodY654On4XKkrobQdQ==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/config": "6.14.0",
|
||||
"@prisma/engines": "6.14.0"
|
||||
"@prisma/config": "6.16.3",
|
||||
"@prisma/engines": "6.16.3"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@
|
|||
"globals": "^16.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prisma": "^6.14.0",
|
||||
"prisma": "^6.16.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `attachement` on the `expenses` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `attachement` on the `expenses_archive` table. All the data in the column will be lost.
|
||||
- Made the column `comment` on table `expenses` required. This step will fail if there are existing NULL values in that column.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."expenses" DROP COLUMN "attachement",
|
||||
ADD COLUMN "attachment" INTEGER,
|
||||
ADD COLUMN "mileage" DECIMAL(65,30),
|
||||
ALTER COLUMN "comment" SET NOT NULL;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."expenses_archive" DROP COLUMN "attachement",
|
||||
ADD COLUMN "attachment" INTEGER,
|
||||
ADD COLUMN "mileage" DECIMAL(65,30),
|
||||
ALTER COLUMN "amount" DROP NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "public"."expenses" ADD CONSTRAINT "expenses_attachment_fkey" FOREIGN KEY ("attachment") REFERENCES "public"."attachments"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "public"."expenses_archive" ADD CONSTRAINT "expenses_archive_attachment_fkey" FOREIGN KEY ("attachment") REFERENCES "public"."attachments"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `end_date_time` on the `leave_requests` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `start_date_time` on the `leave_requests` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `end_date_time` on the `leave_requests_archive` table. All the data in the column will be lost.
|
||||
- You are about to drop the column `start_date_time` on the `leave_requests_archive` table. All the data in the column will be lost.
|
||||
- A unique constraint covering the columns `[employee_id,leave_type,date]` on the table `leave_requests` will be added. If there are existing duplicate values, this will fail.
|
||||
- A unique constraint covering the columns `[leave_request_id]` on the table `leave_requests_archive` will be added. If there are existing duplicate values, this will fail.
|
||||
- Added the required column `date` to the `leave_requests` table without a default value. This is not possible if the table is not empty.
|
||||
- Added the required column `date` to the `leave_requests_archive` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterEnum
|
||||
ALTER TYPE "leave_types" ADD VALUE 'HOLIDAY';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "leave_requests" DROP COLUMN "end_date_time",
|
||||
DROP COLUMN "start_date_time",
|
||||
ADD COLUMN "date" DATE NOT NULL,
|
||||
ADD COLUMN "payable_hours" DECIMAL(5,2),
|
||||
ADD COLUMN "requested_hours" DECIMAL(5,2);
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "leave_requests_archive" DROP COLUMN "end_date_time",
|
||||
DROP COLUMN "start_date_time",
|
||||
ADD COLUMN "date" DATE NOT NULL,
|
||||
ADD COLUMN "payable_hours" DECIMAL(5,2),
|
||||
ADD COLUMN "requested_hours" DECIMAL(5,2);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "preferences" (
|
||||
"user_id" UUID NOT NULL,
|
||||
"notifications" BOOLEAN NOT NULL DEFAULT false,
|
||||
"dark_mode" BOOLEAN NOT NULL DEFAULT false,
|
||||
"lang_switch" BOOLEAN NOT NULL DEFAULT false,
|
||||
"lefty_mode" BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "preferences_user_id_key" ON "preferences"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "leave_requests_employee_id_date_idx" ON "leave_requests"("employee_id", "date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "leave_requests_employee_id_leave_type_date_key" ON "leave_requests"("employee_id", "leave_type", "date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "leave_requests_archive_employee_id_date_idx" ON "leave_requests_archive"("employee_id", "date");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "leave_requests_archive_leave_request_id_key" ON "leave_requests_archive"("leave_request_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "preferences" ADD CONSTRAINT "preferences_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import { PrismaClient, Prisma, LeaveTypes, LeaveApprovalStatus } from '@prisma/client';
|
||||
|
||||
if (process.env.SKIP_LEAVE_REQUESTS === 'true') {
|
||||
console.log("⏭ Seed leave-requests ignoré (SKIP_LEAVE_REQUESTS=true)");
|
||||
console.log('?? Seed leave-requests ignoré (SKIP_LEAVE_REQUESTS=true)');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
function dateOn(y: number, m: number, d: number) {
|
||||
// stocke une date (pour @db.Date) à minuit UTC
|
||||
// stocke une date (@db.Date) à minuit UTC
|
||||
return new Date(Date.UTC(y, m - 1, d, 0, 0, 0));
|
||||
}
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ async function main() {
|
|||
const employees = await prisma.employees.findMany({ select: { id: true } });
|
||||
const bankCodes = await prisma.bankCodes.findMany({
|
||||
where: { categorie: 'LEAVE' },
|
||||
select: { id: true },
|
||||
select: { id: true, type: true },
|
||||
});
|
||||
|
||||
if (!employees.length || !bankCodes.length) {
|
||||
|
|
@ -44,30 +44,31 @@ async function main() {
|
|||
LeaveApprovalStatus.ESCALATED,
|
||||
];
|
||||
|
||||
const futureMonths = [8, 9, 10, 11, 12]; // Août→Déc (1-based)
|
||||
const futureMonths = [8, 9, 10, 11, 12]; // Août ? Déc. (1-based)
|
||||
|
||||
// ✅ typer rows pour éviter never[]
|
||||
const rows: Prisma.LeaveRequestsCreateManyInput[] = [];
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const emp = employees[i % employees.length];
|
||||
const m = futureMonths[i % futureMonths.length];
|
||||
const start = dateOn(year, m, 5 + i); // 5..14
|
||||
if (start <= today) continue; // garantir "futur"
|
||||
const date = dateOn(year, m, 5 + i); // 5..14
|
||||
if (date <= today) continue; // garantir « futur »
|
||||
|
||||
const end = Math.random() < 0.5 ? null : dateOn(year, m, 6 + i);
|
||||
const type = types[i % types.length];
|
||||
const status = statuses[i % statuses.length];
|
||||
const bc = bankCodes[i % bankCodes.length];
|
||||
const requestedHours = 4 + (i % 5); // 4 ? 8 h
|
||||
const payableHours = status === LeaveApprovalStatus.APPROVED ? Math.min(requestedHours, 8) : null;
|
||||
|
||||
rows.push({
|
||||
employee_id: emp.id,
|
||||
bank_code_id: bc.id,
|
||||
leave_type: type,
|
||||
start_date_time: start,
|
||||
end_date_time: end, // ok: Date | null
|
||||
comment: `Future leave #${i + 1}`,
|
||||
date,
|
||||
comment: `Future leave #${i + 1} (${bc.type})`,
|
||||
approval_status: status,
|
||||
requested_hours: requestedHours,
|
||||
payable_hours: payableHours,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +76,7 @@ async function main() {
|
|||
await prisma.leaveRequests.createMany({ data: rows });
|
||||
}
|
||||
|
||||
console.log(`✓ LeaveRequests (future): ${rows.length} rows`);
|
||||
console.log(`? LeaveRequests (future): ${rows.length} rows`);
|
||||
}
|
||||
|
||||
main().finally(() => prisma.$disconnect());
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { PrismaClient, LeaveApprovalStatus, LeaveRequests } from '@prisma/client';
|
||||
import { PrismaClient, LeaveApprovalStatus, LeaveTypes } from '@prisma/client';
|
||||
|
||||
if (process.env.SKIP_LEAVE_REQUESTS === 'true') {
|
||||
console.log("⏭ Seed leave-requests ignoré (SKIP_LEAVE_REQUESTS=true)");
|
||||
console.log('?? Seed leave-requests ignoré (SKIP_LEAVE_REQUESTS=true)');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
|
@ -15,65 +15,73 @@ function daysAgo(n: number) {
|
|||
}
|
||||
|
||||
async function main() {
|
||||
// 1) Récupère tous les employés
|
||||
const employees = await prisma.employees.findMany({ select: { id: true } });
|
||||
if (!employees.length) {
|
||||
throw new Error('Aucun employé trouvé. Exécute le seed employees avant celui-ci.');
|
||||
throw new Error('Aucun employé trouvé. Exécute le seed employees avant celui-ci.');
|
||||
}
|
||||
|
||||
// 2) Va chercher les bank codes dont le type est SICK, VACATION ou HOLIDAY
|
||||
const leaveCodes = await prisma.bankCodes.findMany({
|
||||
where: { type: { in: ['SICK', 'VACATION'] } },
|
||||
select: { id: true, type: true, bank_code: true },
|
||||
where: { type: { in: ['SICK', 'VACATION', 'HOLIDAY'] } },
|
||||
select: { id: true, type: true },
|
||||
});
|
||||
if (!leaveCodes.length) {
|
||||
throw new Error("Aucun bank code trouvé avec type in ('SICK','VACATION','HOLIDAY'). Vérifie ta table bank_codes.");
|
||||
throw new Error("Aucun bank code trouvé avec type in ('SICK','VACATION','HOLIDAY'). Vérifie ta table bank_codes.");
|
||||
}
|
||||
|
||||
const statuses = Object.values(LeaveApprovalStatus);
|
||||
const created: LeaveRequests[] = [];
|
||||
const created = [] as Array<{ id: number; employee_id: number; leave_type: LeaveTypes; date: Date; comment: string; approval_status: LeaveApprovalStatus; requested_hours: number; payable_hours: number | null }>;
|
||||
|
||||
// 3) Crée quelques leave requests
|
||||
const COUNT = 12;
|
||||
for (let i = 0; i < COUNT; i++) {
|
||||
const emp = employees[i % employees.length];
|
||||
const leaveCode = leaveCodes[Math.floor(Math.random() * leaveCodes.length)];
|
||||
|
||||
const start = daysAgo(120 - i * 3);
|
||||
const end = Math.random() < 0.6 ? daysAgo(119 - i * 3) : null;
|
||||
const date = daysAgo(120 - i * 3);
|
||||
const status = statuses[(i + 2) % statuses.length];
|
||||
const requestedHours = 4 + (i % 5); // 4 ? 8 h
|
||||
const payableHours = status === LeaveApprovalStatus.APPROVED ? Math.min(requestedHours, 8) : null;
|
||||
|
||||
const lr = await prisma.leaveRequests.create({
|
||||
data: {
|
||||
employee_id: emp.id,
|
||||
bank_code_id: leaveCode.id,
|
||||
// on stocke le "type" tel qu’il est défini dans bank_codes
|
||||
leave_type: leaveCode.type as any,
|
||||
start_date_time: start,
|
||||
end_date_time: end,
|
||||
leave_type: leaveCode.type as LeaveTypes,
|
||||
date,
|
||||
comment: `Past leave #${i + 1} (${leaveCode.type})`,
|
||||
approval_status: statuses[(i + 2) % statuses.length],
|
||||
approval_status: status,
|
||||
requested_hours: requestedHours,
|
||||
payable_hours: payableHours,
|
||||
},
|
||||
});
|
||||
|
||||
created.push(lr);
|
||||
created.push({
|
||||
id: lr.id,
|
||||
employee_id: lr.employee_id,
|
||||
leave_type: lr.leave_type,
|
||||
date: lr.date,
|
||||
comment: lr.comment,
|
||||
approval_status: lr.approval_status,
|
||||
requested_hours: requestedHours,
|
||||
payable_hours: payableHours,
|
||||
});
|
||||
}
|
||||
|
||||
// 4) Archive
|
||||
for (const lr of created) {
|
||||
await prisma.leaveRequestsArchive.create({
|
||||
data: {
|
||||
leave_request_id: lr.id,
|
||||
employee_id: lr.employee_id,
|
||||
leave_type: lr.leave_type,
|
||||
start_date_time: lr.start_date_time,
|
||||
end_date_time: lr.end_date_time,
|
||||
date: lr.date,
|
||||
comment: lr.comment,
|
||||
approval_status: lr.approval_status,
|
||||
requested_hours: lr.requested_hours,
|
||||
payable_hours: lr.payable_hours,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✓ LeaveRequestsArchive: ${created.length} rows`);
|
||||
console.log(`? LeaveRequestsArchive: ${created.length} rows`);
|
||||
}
|
||||
|
||||
main().finally(() => prisma.$disconnect());
|
||||
|
|
@ -143,7 +143,7 @@ async function main() {
|
|||
bank_code_id,
|
||||
date,
|
||||
amount, // string "xx.yy" (2 décimales exactes)
|
||||
attachement: null,
|
||||
attachment: null,
|
||||
comment: `Expense ${code} ${amount}$ (emp ${e.id})`,
|
||||
is_approved: Math.random() < 0.65,
|
||||
supervisor_comment: Math.random() < 0.25 ? 'OK' : null,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ async function main() {
|
|||
bank_code_id: bc.id,
|
||||
date: daysAgo(60 + i),
|
||||
amount: (20 + i * 3.5).toFixed(2), // ok: Decimal accepte string
|
||||
attachement: null,
|
||||
attachment: null,
|
||||
comment: `Old expense #${i + 1}`,
|
||||
is_approved: true,
|
||||
supervisor_comment: null,
|
||||
|
|
@ -50,7 +50,7 @@ async function main() {
|
|||
bank_code_id: e.bank_code_id,
|
||||
date: e.date,
|
||||
amount: e.amount,
|
||||
attachement: e.attachement,
|
||||
attachment: e.attachment,
|
||||
comment: e.comment,
|
||||
is_approved: e.is_approved,
|
||||
supervisor_comment: e.supervisor_comment,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ model Users {
|
|||
oauth_sessions OAuthSessions[] @relation("UserOAuthSessions")
|
||||
employees_archive EmployeesArchive[] @relation("UsersToEmployeesToArchive")
|
||||
customer_archive CustomersArchive[] @relation("UserToCustomersToArchive")
|
||||
|
||||
preferences Preferences? @relation("UserPreferences")
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
|
|
@ -105,16 +105,19 @@ 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 BankCodes @relation("LeaveRequestBankCodes", fields: [bank_code_id], references: [id])
|
||||
bank_code_id Int
|
||||
leave_type LeaveTypes
|
||||
start_date_time DateTime @db.Date
|
||||
end_date_time DateTime? @db.Date
|
||||
date DateTime @db.Date
|
||||
payable_hours Decimal? @db.Decimal(5,2)
|
||||
requested_hours Decimal? @db.Decimal(5,2)
|
||||
comment String
|
||||
approval_status LeaveApprovalStatus @default(PENDING)
|
||||
|
||||
archive LeaveRequestsArchive[] @relation("LeaveRequestToArchive")
|
||||
|
||||
@@unique([employee_id, leave_type, date], name: "leave_per_employee_date")
|
||||
@@index([employee_id, date])
|
||||
@@map("leave_requests")
|
||||
}
|
||||
|
||||
|
|
@ -125,11 +128,14 @@ model LeaveRequestsArchive {
|
|||
archived_at DateTime @default(now())
|
||||
employee_id Int
|
||||
leave_type LeaveTypes
|
||||
start_date_time DateTime @db.Date
|
||||
end_date_time DateTime? @db.Date
|
||||
date DateTime @db.Date
|
||||
payable_hours Decimal? @db.Decimal(5,2)
|
||||
requested_hours Decimal? @db.Decimal(5,2)
|
||||
comment String
|
||||
approval_status LeaveApprovalStatus
|
||||
|
||||
@@unique([leave_request_id])
|
||||
@@index([employee_id, date])
|
||||
@@map("leave_requests_archive")
|
||||
}
|
||||
|
||||
|
|
@ -226,8 +232,10 @@ model Expenses {
|
|||
bank_code_id Int
|
||||
date DateTime @db.Date
|
||||
amount Decimal @db.Money
|
||||
attachement String?
|
||||
comment String?
|
||||
mileage Decimal?
|
||||
attachment Int?
|
||||
attachment_record Attachments? @relation("ExpenseAttachment", fields: [attachment], references: [id], onDelete: SetNull)
|
||||
comment String
|
||||
is_approved Boolean @default(false)
|
||||
supervisor_comment String?
|
||||
|
||||
|
|
@ -244,8 +252,10 @@ model ExpensesArchive {
|
|||
archived_at DateTime @default(now())
|
||||
bank_code_id Int
|
||||
date DateTime @db.Date
|
||||
amount Decimal @db.Money
|
||||
attachement String?
|
||||
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?
|
||||
|
|
@ -296,11 +306,27 @@ model Attachments {
|
|||
created_by String
|
||||
created_at DateTime @default(now())
|
||||
|
||||
expenses Expenses[] @relation("ExpenseAttachment")
|
||||
expenses_archive ExpensesArchive[] @relation("ExpenseArchiveAttachment")
|
||||
|
||||
@@index([owner_type, owner_id, created_at])
|
||||
@@index([sha256])
|
||||
@@map("attachments")
|
||||
}
|
||||
|
||||
model Preferences {
|
||||
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)
|
||||
lefty_mode Boolean @default(false)
|
||||
|
||||
@@map("preferences")
|
||||
}
|
||||
|
||||
|
||||
enum AttachmentStatus {
|
||||
ACTIVE
|
||||
DELETED
|
||||
|
|
@ -333,6 +359,7 @@ enum LeaveTypes {
|
|||
PARENTAL // maternite/paternite/adoption
|
||||
LEGAL // obligations legales comme devoir de juree
|
||||
WEDDING // mariage
|
||||
HOLIDAY // férier
|
||||
|
||||
@@map("leave_types")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
import { BadRequestException, Module, ValidationPipe } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { ArchivalModule } from './modules/archival/archival.module';
|
||||
import { AuthenticationModule } from './modules/authentication/auth.module';
|
||||
import { BankCodesModule } from './modules/bank-codes/bank-codes.module';
|
||||
import { BankCodesModule } from './modules/bank-codes/bank-codes.module';
|
||||
import { BusinessLogicsModule } from './modules/business-logics/business-logics.module';
|
||||
// import { CsvExportModule } from './modules/exports/csv-exports.module';
|
||||
import { CustomersModule } from './modules/customers/customers.module';
|
||||
import { EmployeesModule } from './modules/employees/employees.module';
|
||||
import { ExpensesModule } from './modules/expenses/expenses.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { CustomersModule } from './modules/customers/customers.module';
|
||||
import { EmployeesModule } from './modules/employees/employees.module';
|
||||
import { ExpensesModule } from './modules/expenses/expenses.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
import { HealthController } from './health/health.controller';
|
||||
import { LeaveRequestsModule } from './modules/leave-requests/leave-requests.module';
|
||||
import { NotificationsModule } from './modules/notifications/notifications.module';
|
||||
import { OauthSessionsModule } from './modules/oauth-sessions/oauth-sessions.module';
|
||||
import { OvertimeService } from './modules/business-logics/services/overtime.service';
|
||||
import { OvertimeService } from './modules/business-logics/services/overtime.service';
|
||||
import { PayperiodsModule } from './modules/pay-periods/pay-periods.module';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { ShiftsModule } from './modules/shifts/shifts.module';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { ShiftsModule } from './modules/shifts/shifts.module';
|
||||
import { TimesheetsModule } from './modules/timesheets/timesheets.module';
|
||||
import { UsersModule } from './modules/users-management/users.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { UsersModule } from './modules/users-management/users.module';
|
||||
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 { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||
import { ValidationError } from 'class-validator';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { EmployeesModule } from "../employees/employees.module";
|
|||
LeaveRequestsArchiveController,
|
||||
ShiftsArchiveController,
|
||||
TimesheetsArchiveController,
|
||||
]
|
||||
],
|
||||
})
|
||||
|
||||
export class ArchivalModule {}
|
||||
|
|
@ -1,33 +1,7 @@
|
|||
import { Get, Param, ParseIntPipe, NotFoundException, Controller, UseGuards } from "@nestjs/common";
|
||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { LeaveRequestsArchive, Roles as RoleEnum } from "@prisma/client";
|
||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||
import { LeaveRequestViewDto } from "src/modules/leave-requests/dtos/leave-request.view.dto";
|
||||
import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-requests.service";
|
||||
import { Controller } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('LeaveRequests Archives')
|
||||
// @UseGuards()
|
||||
@Controller('archives/leaveRequests')
|
||||
export class LeaveRequestsArchiveController {
|
||||
constructor(private readonly leaveRequestsService: LeaveRequestsService) {}
|
||||
|
||||
@Get()
|
||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'List of archived leaveRequests'})
|
||||
@ApiResponse({ status: 200, description: 'List of archived leaveRequests', isArray: true })
|
||||
async findAllArchived(): Promise<LeaveRequestsArchive[]> {
|
||||
return this.leaveRequestsService.findAllArchived();
|
||||
}
|
||||
|
||||
@Get()
|
||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Fetch leaveRequest in archives with its Id'})
|
||||
@ApiResponse({ status: 200, description: 'Archived leaveRequest found'})
|
||||
async findOneArchived(@Param('id', ParseIntPipe) id: number ): Promise<LeaveRequestViewDto> {
|
||||
try{
|
||||
return await this.leaveRequestsService.findOneArchived(id);
|
||||
}catch {
|
||||
throw new NotFoundException(`Archived leaveRequest #${id} not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
export class LeaveRequestsArchiveController {}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Cron } from "@nestjs/schedule";
|
||||
import { ExpensesQueryService } from "src/modules/expenses/services/expenses-query.service";
|
||||
import { LeaveRequestsService } from "src/modules/leave-requests/services/leave-requests.service";
|
||||
import { ShiftsQueryService } from "src/modules/shifts/services/shifts-query.service";
|
||||
import { TimesheetsQueryService } from "src/modules/timesheets/services/timesheets-query.service";
|
||||
|
||||
|
|
@ -13,7 +12,6 @@ export class ArchivalService {
|
|||
private readonly timesheetsService: TimesheetsQueryService,
|
||||
private readonly expensesService: ExpensesQueryService,
|
||||
private readonly shiftsService: ShiftsQueryService,
|
||||
private readonly leaveRequestsService: LeaveRequestsService,
|
||||
) {}
|
||||
|
||||
@Cron('0 0 3 * * 1', {timeZone:'America/Toronto'}) // chaque premier lundi du mois à 03h00
|
||||
|
|
@ -31,7 +29,7 @@ export class ArchivalService {
|
|||
await this.timesheetsService.archiveOld();
|
||||
await this.expensesService.archiveOld();
|
||||
await this.shiftsService.archiveOld();
|
||||
await this.leaveRequestsService.archiveExpired();
|
||||
// await this.leaveRequestsService.archiveExpired();
|
||||
this.logger.log('archivation process done');
|
||||
} catch (err) {
|
||||
this.logger.error('an error occured during archivation process ', err);
|
||||
|
|
|
|||
|
|
@ -7,40 +7,43 @@ import { ApiBadRequestResponse, ApiNotFoundResponse, ApiOperation, ApiResponse }
|
|||
@Controller('bank-codes')
|
||||
export class BankCodesControllers {
|
||||
constructor(private readonly bankCodesService: BankCodesService) {}
|
||||
//_____________________________________________________________________________________________
|
||||
// Deprecated or unused methods
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new bank code' })
|
||||
@ApiResponse({ status: 201, description: 'Bank code successfully created.' })
|
||||
@ApiBadRequestResponse({ description: 'Invalid input data.' })
|
||||
create(@Body() dto: CreateBankCodeDto) {
|
||||
return this.bankCodesService.create(dto);
|
||||
}
|
||||
// @Post()
|
||||
// @ApiOperation({ summary: 'Create a new bank code' })
|
||||
// @ApiResponse({ status: 201, description: 'Bank code successfully created.' })
|
||||
// @ApiBadRequestResponse({ description: 'Invalid input data.' })
|
||||
// create(@Body() dto: CreateBankCodeDto) {
|
||||
// return this.bankCodesService.create(dto);
|
||||
// }
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Retrieve all bank codes' })
|
||||
@ApiResponse({ status: 200, description: 'List of bank codes.' })
|
||||
findAll() {
|
||||
return this.bankCodesService.findAll();
|
||||
}
|
||||
// @Get()
|
||||
// @ApiOperation({ summary: 'Retrieve all bank codes' })
|
||||
// @ApiResponse({ status: 200, description: 'List of bank codes.' })
|
||||
// findAll() {
|
||||
// return this.bankCodesService.findAll();
|
||||
// }
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Retrieve a bank code by its ID' })
|
||||
@ApiNotFoundResponse({ description: 'Bank code not found.' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number){
|
||||
return this.bankCodesService.findOne(id);
|
||||
}
|
||||
// @Get(':id')
|
||||
// @ApiOperation({ summary: 'Retrieve a bank code by its ID' })
|
||||
// @ApiNotFoundResponse({ description: 'Bank code not found.' })
|
||||
// findOne(@Param('id', ParseIntPipe) id: number){
|
||||
// return this.bankCodesService.findOne(id);
|
||||
// }
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: 'Update an existing bank code' })
|
||||
@ApiNotFoundResponse({ description: 'Bank code not found.' })
|
||||
update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateBankCodeDto) {
|
||||
return this.bankCodesService.update(id, dto)
|
||||
}
|
||||
// @Patch(':id')
|
||||
// @ApiOperation({ summary: 'Update an existing bank code' })
|
||||
// @ApiNotFoundResponse({ description: 'Bank code not found.' })
|
||||
// update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateBankCodeDto) {
|
||||
// return this.bankCodesService.update(id, dto)
|
||||
// }
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete a bank code' })
|
||||
@ApiNotFoundResponse({ description: 'Bank code not found.' })
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.bankCodesService.remove(id);
|
||||
}
|
||||
// @Delete(':id')
|
||||
// @ApiOperation({ summary: 'Delete a bank code' })
|
||||
// @ApiNotFoundResponse({ description: 'Bank code not found.' })
|
||||
// remove(@Param('id', ParseIntPipe) id: number) {
|
||||
// return this.bankCodesService.remove(id);
|
||||
// }
|
||||
}
|
||||
|
|
@ -1,6 +1,14 @@
|
|||
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
import { PrismaService } from "../../../prisma/prisma.service";
|
||||
import { computeHours, getWeekStart } from "src/common/utils/date-utils";
|
||||
import { PrismaService } from "../../../prisma/prisma.service";
|
||||
|
||||
const WEEK_IN_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
/*
|
||||
le calcul est 1/20 des 4 dernières semaines, précédent la semaine incluant le férier.
|
||||
Un maximum de 08h00 est allouable pour le férier
|
||||
Un maximum de 40hrs par semaine est retenue pour faire le calcul.
|
||||
*/
|
||||
|
||||
@Injectable()
|
||||
export class HolidayService {
|
||||
|
|
@ -22,36 +30,49 @@ export class HolidayService {
|
|||
|
||||
private async computeHoursPrevious4WeeksByEmail(email: string, holiday_date: Date): Promise<number> {
|
||||
const employee_id = await this.resolveEmployeeByEmail(email);
|
||||
return this.computeHoursPrevious4Weeks(employee_id, holiday_date)
|
||||
return this.computeHoursPrevious4Weeks(employee_id, holiday_date);
|
||||
}
|
||||
|
||||
private async computeHoursPrevious4Weeks(employee_id: number, holiday_date: Date): Promise<number> {
|
||||
//sets the end of the window to 1ms before the week with the holiday
|
||||
const holiday_week_start = getWeekStart(holiday_date);
|
||||
const window_start = new Date(holiday_week_start.getTime() - 4 * WEEK_IN_MS);
|
||||
const window_end = new Date(holiday_week_start.getTime() - 1);
|
||||
//sets the start of the window to 28 days ( 4 completed weeks ) before the week with the holiday
|
||||
const window_start = new Date(window_end.getTime() - 28 * 24 * 60 * 60000 + 1 )
|
||||
|
||||
const valid_codes = ['G1', 'G45', 'G56', 'G104', 'G105', 'G700'];
|
||||
//fetches all shift of the employee in said window ( 4 previous completed weeks )
|
||||
const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G700'];
|
||||
const shifts = await this.prisma.shifts.findMany({
|
||||
where: { timesheet: { employee_id: employee_id } ,
|
||||
date: { gte: window_start, lte: window_end },
|
||||
bank_code: { bank_code: { in: valid_codes } },
|
||||
where: {
|
||||
timesheet: { employee_id: employee_id },
|
||||
date: { gte: window_start, lte: window_end },
|
||||
bank_code: { bank_code: { in: valid_codes } },
|
||||
},
|
||||
select: { date: true, start_time: true, end_time: true },
|
||||
});
|
||||
|
||||
const total_hours = shifts.map(s => computeHours(s.start_time, s.end_time)).reduce((sum, h)=> sum + h, 0);
|
||||
const daily_hours = total_hours / 20;
|
||||
const hours_by_week = new Map<number, number>();
|
||||
for(const shift of shifts) {
|
||||
const hours = computeHours(shift.start_time, shift.end_time);
|
||||
if(hours <= 0) continue;
|
||||
const shift_week_start = getWeekStart(shift.date);
|
||||
const key = shift_week_start.getTime();
|
||||
hours_by_week.set(key, (hours_by_week.get(key) ?? 0) + hours);
|
||||
}
|
||||
|
||||
return daily_hours;
|
||||
let capped_total = 0;
|
||||
for(let offset = 1; offset <= 4; offset++) {
|
||||
const week_start = new Date(holiday_week_start.getTime() - offset * WEEK_IN_MS);
|
||||
const key = week_start.getTime();
|
||||
const weekly_hours = hours_by_week.get(key) ?? 0;
|
||||
capped_total += Math.min(weekly_hours, 40);
|
||||
}
|
||||
|
||||
const average_daily_hours = capped_total / 20;
|
||||
return average_daily_hours;
|
||||
}
|
||||
|
||||
async calculateHolidayPay( email: string, holiday_date: Date, modifier: number): Promise<number> {
|
||||
const hours = await this.computeHoursPrevious4WeeksByEmail(email, holiday_date);
|
||||
const daily_rate = Math.min(hours, 8);
|
||||
this.logger.debug(`Holiday pay calculation: hours= ${hours.toFixed(2)}`);
|
||||
const average_daily_hours = await this.computeHoursPrevious4WeeksByEmail(email, holiday_date);
|
||||
const daily_rate = Math.min(average_daily_hours, 8);
|
||||
this.logger.debug(`Holiday pay calculation: cappedHoursPerDay= ${average_daily_hours.toFixed(2)}, appliedDailyRate= ${daily_rate.toFixed(2)}`);
|
||||
return daily_rate * modifier;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { getYearStart, roundToQuarterHour } from "src/common/utils/date-utils";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from "../../../prisma/prisma.service";
|
||||
import { getYearStart, roundToQuarterHour } from "src/common/utils/date-utils";
|
||||
|
||||
@Injectable()
|
||||
export class SickLeaveService {
|
||||
|
|
@ -9,28 +9,38 @@ export class SickLeaveService {
|
|||
private readonly logger = new Logger(SickLeaveService.name);
|
||||
|
||||
//switch employeeId for email
|
||||
async calculateSickLeavePay(employee_id: number, reference_date: Date, days_requested: number, modifier: number):
|
||||
Promise<number> {
|
||||
async calculateSickLeavePay(
|
||||
employee_id: number,
|
||||
reference_date: Date,
|
||||
days_requested: number,
|
||||
hours_per_day: number,
|
||||
modifier: number,
|
||||
): Promise<number> {
|
||||
if (days_requested <= 0 || hours_per_day <= 0 || modifier <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
//sets the year to jan 1st to dec 31st
|
||||
const period_start = getYearStart(reference_date);
|
||||
const period_end = reference_date;
|
||||
const period_end = reference_date;
|
||||
|
||||
//fetches all shifts of a selected employee
|
||||
const shifts = await this.prisma.shifts.findMany({
|
||||
where: {
|
||||
timesheet: { employee_id: employee_id },
|
||||
date: { gte: period_start, lte: period_end},
|
||||
date: { gte: period_start, lte: period_end },
|
||||
},
|
||||
select: { date: true },
|
||||
});
|
||||
|
||||
//count the amount of worked days
|
||||
const worked_dates = new Set(
|
||||
shifts.map(shift => shift.date.toISOString().slice(0,10))
|
||||
shifts.map((shift) => shift.date.toISOString().slice(0, 10)),
|
||||
);
|
||||
const days_worked = worked_dates.size;
|
||||
this.logger.debug(`Sick leave: days worked= ${days_worked} in ${period_start.toDateString()}
|
||||
-> ${period_end.toDateString()}`);
|
||||
this.logger.debug(
|
||||
`Sick leave: days worked= ${days_worked} in ${period_start.toDateString()} -> ${period_end.toDateString()}`,
|
||||
);
|
||||
|
||||
//less than 30 worked days returns 0
|
||||
if (days_worked < 30) {
|
||||
|
|
@ -45,22 +55,31 @@ export class SickLeaveService {
|
|||
const threshold_date = new Date(ordered_dates[29]); // index 29 is the 30th day
|
||||
|
||||
//calculate each completed month, starting the 1st of the next month
|
||||
const first_bonus_date = new Date(threshold_date.getFullYear(), threshold_date.getMonth() +1, 1);
|
||||
let months = (period_end.getFullYear() - first_bonus_date.getFullYear()) * 12 +
|
||||
(period_end.getMonth() - first_bonus_date.getMonth()) + 1;
|
||||
if(months < 0) months = 0;
|
||||
const first_bonus_date = new Date(
|
||||
threshold_date.getFullYear(),
|
||||
threshold_date.getMonth() + 1,
|
||||
1,
|
||||
);
|
||||
let months =
|
||||
(period_end.getFullYear() - first_bonus_date.getFullYear()) * 12 +
|
||||
(period_end.getMonth() - first_bonus_date.getMonth()) +
|
||||
1;
|
||||
if (months < 0) months = 0;
|
||||
acquired_days += months;
|
||||
|
||||
//cap of 10 days
|
||||
if (acquired_days > 10) acquired_days = 10;
|
||||
|
||||
this.logger.debug(`Sick leave: threshold Date = ${threshold_date.toDateString()}
|
||||
, bonusMonths = ${months}, acquired Days = ${acquired_days}`);
|
||||
this.logger.debug(
|
||||
`Sick leave: threshold Date = ${threshold_date.toDateString()}, bonusMonths = ${months}, acquired Days = ${acquired_days}`,
|
||||
);
|
||||
|
||||
const payable_days = Math.min(acquired_days, days_requested);
|
||||
const raw_hours = payable_days * 8 * modifier;
|
||||
const rounded = roundToQuarterHour(raw_hours)
|
||||
this.logger.debug(`Sick leave pay: days= ${payable_days}, modifier= ${modifier}, hours= ${rounded}`);
|
||||
const raw_hours = payable_days * hours_per_day * modifier;
|
||||
const rounded = roundToQuarterHour(raw_hours);
|
||||
this.logger.debug(
|
||||
`Sick leave pay: days= ${payable_days}, hoursPerDay= ${hours_per_day}, modifier= ${modifier}, hours= ${rounded}`,
|
||||
);
|
||||
return rounded;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,16 +6,8 @@ export class VacationService {
|
|||
constructor(private readonly prisma: PrismaService) {}
|
||||
private readonly logger = new Logger(VacationService.name);
|
||||
|
||||
/**
|
||||
* Calculate the ammount allowed for vacation days.
|
||||
*
|
||||
* @param employee_id employee ID
|
||||
* @param startDate first day of vacation
|
||||
* @param daysRequested number of days requested
|
||||
* @param modifier Coefficient of hours(1)
|
||||
* @returns amount of payable hours
|
||||
*/
|
||||
//switch employeeId for email
|
||||
|
||||
//switch employeeId for email
|
||||
async calculateVacationPay(employee_id: number, start_date: Date, days_requested: number, modifier: number): Promise<number> {
|
||||
//fetch hiring date
|
||||
const employee = await this.prisma.employees.findUnique({
|
||||
|
|
@ -56,7 +48,7 @@ export class VacationService {
|
|||
const segment_end = boundaries[i+1];
|
||||
|
||||
//number of days in said segment
|
||||
const days_in_segment = Math.round((segment_end.getTime() - segment_start.getTime())/ ms_per_day);
|
||||
const days_in_segment = Math.round((segment_end.getTime() - segment_start.getTime())/ ms_per_day);
|
||||
const years_since_hire = (segment_start.getFullYear() - hire_date.getFullYear()) -
|
||||
(segment_start < new Date(segment_start.getFullYear(), hire_date.getMonth()) ? 1 : 0);
|
||||
let alloc_days: number;
|
||||
|
|
|
|||
|
|
@ -14,51 +14,55 @@ import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagg
|
|||
export class CustomersController {
|
||||
constructor(private readonly customersService: CustomersService) {}
|
||||
|
||||
@Post()
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Create customer' })
|
||||
@ApiResponse({ status: 201, description: 'Customer created', type: CreateCustomerDto })
|
||||
@ApiResponse({ status: 400, description: 'Invalid task or invalid data' })
|
||||
create(@Body() dto: CreateCustomerDto): Promise<Customers> {
|
||||
return this.customersService.create(dto);
|
||||
}
|
||||
//_____________________________________________________________________________________________
|
||||
// Deprecated or unused methods
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
@Get()
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Find all customers' })
|
||||
@ApiResponse({ status: 201, description: 'List of customers found', type: CreateCustomerDto, isArray: true })
|
||||
@ApiResponse({ status: 400, description: 'List of customers not found' })
|
||||
findAll(): Promise<Customers[]> {
|
||||
return this.customersService.findAll();
|
||||
}
|
||||
// @Post()
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Create customer' })
|
||||
// @ApiResponse({ status: 201, description: 'Customer created', type: CreateCustomerDto })
|
||||
// @ApiResponse({ status: 400, description: 'Invalid task or invalid data' })
|
||||
// create(@Body() dto: CreateCustomerDto): Promise<Customers> {
|
||||
// return this.customersService.create(dto);
|
||||
// }
|
||||
|
||||
@Get(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Find customer' })
|
||||
@ApiResponse({ status: 201, description: 'Customer found', type: CreateCustomerDto })
|
||||
@ApiResponse({ status: 400, description: 'Customer not found' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number): Promise<Customers> {
|
||||
return this.customersService.findOne(id);
|
||||
}
|
||||
// @Get()
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Find all customers' })
|
||||
// @ApiResponse({ status: 201, description: 'List of customers found', type: CreateCustomerDto, isArray: true })
|
||||
// @ApiResponse({ status: 400, description: 'List of customers not found' })
|
||||
// findAll(): Promise<Customers[]> {
|
||||
// return this.customersService.findAll();
|
||||
// }
|
||||
|
||||
@Patch(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE,RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Update customer' })
|
||||
@ApiResponse({ status: 201, description: 'Customer updated', type: CreateCustomerDto })
|
||||
@ApiResponse({ status: 400, description: 'Customer not found' })
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() dto: UpdateCustomerDto,
|
||||
): Promise<Customers> {
|
||||
return this.customersService.update(id, dto);
|
||||
}
|
||||
// @Get(':id')
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Find customer' })
|
||||
// @ApiResponse({ status: 201, description: 'Customer found', type: CreateCustomerDto })
|
||||
// @ApiResponse({ status: 400, description: 'Customer not found' })
|
||||
// findOne(@Param('id', ParseIntPipe) id: number): Promise<Customers> {
|
||||
// return this.customersService.findOne(id);
|
||||
// }
|
||||
|
||||
@Delete(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Delete customer' })
|
||||
@ApiResponse({ status: 201, description: 'Customer deleted', type: CreateCustomerDto })
|
||||
@ApiResponse({ status: 400, description: 'Customer not found' })
|
||||
remove(@Param('id', ParseIntPipe) id: number): Promise<Customers>{
|
||||
return this.customersService.remove(id);
|
||||
}
|
||||
// @Patch(':id')
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE,RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Update customer' })
|
||||
// @ApiResponse({ status: 201, description: 'Customer updated', type: CreateCustomerDto })
|
||||
// @ApiResponse({ status: 400, description: 'Customer not found' })
|
||||
// update(
|
||||
// @Param('id', ParseIntPipe) id: number,
|
||||
// @Body() dto: UpdateCustomerDto,
|
||||
// ): Promise<Customers> {
|
||||
// return this.customersService.update(id, dto);
|
||||
// }
|
||||
|
||||
// @Delete(':id')
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Delete customer' })
|
||||
// @ApiResponse({ status: 201, description: 'Customer deleted', type: CreateCustomerDto })
|
||||
// @ApiResponse({ status: 400, description: 'Customer not found' })
|
||||
// remove(@Param('id', ParseIntPipe) id: number): Promise<Customers>{
|
||||
// return this.customersService.remove(id);
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,15 +24,6 @@ export class EmployeesController {
|
|||
return this.employeesService.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING)
|
||||
@ApiOperation({summary: 'Find all employees' })
|
||||
@ApiResponse({ status: 200, description: 'List of employees found', type: CreateEmployeeDto, isArray: true })
|
||||
@ApiResponse({ status: 400, description: 'List of employees not found' })
|
||||
findAll(): Promise<Employees[]> {
|
||||
return this.employeesService.findAll();
|
||||
}
|
||||
|
||||
@Get('employee-list')
|
||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING)
|
||||
@ApiOperation({summary: 'Find all employees with scoped info' })
|
||||
|
|
@ -42,34 +33,6 @@ export class EmployeesController {
|
|||
return this.employeesService.findListEmployees();
|
||||
}
|
||||
|
||||
@Get(':email')
|
||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING )
|
||||
@ApiOperation({summary: 'Find employee' })
|
||||
@ApiResponse({ status: 200, description: 'Employee found', type: CreateEmployeeDto })
|
||||
@ApiResponse({ status: 400, description: 'Employee not found' })
|
||||
findOne(@Param('email', ParseIntPipe) email: string): Promise<Employees> {
|
||||
return this.employeesService.findOne(email);
|
||||
}
|
||||
|
||||
@Get('profile/:email')
|
||||
@ApiOperation({summary: 'Find employee profile' })
|
||||
@ApiParam({ name: 'email', type: String, description: 'Identifier of the employee' })
|
||||
@ApiResponse({ status: 200, description: 'Employee profile found', type: EmployeeProfileItemDto })
|
||||
@ApiResponse({ status: 400, description: 'Employee profile not found' })
|
||||
findOneProfile(@Param('email') email: string): Promise<EmployeeProfileItemDto> {
|
||||
return this.employeesService.findOneProfile(email);
|
||||
}
|
||||
|
||||
@Delete(':email')
|
||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR )
|
||||
@ApiOperation({summary: 'Delete employee' })
|
||||
@ApiParam({ name: 'email', type: Number, description: 'Email of the employee to delete' })
|
||||
@ApiResponse({ status: 204, description: 'Employee deleted' })
|
||||
@ApiResponse({ status: 404, description: 'Employee not found' })
|
||||
remove(@Param('email', ParseIntPipe) email: string): Promise<Employees> {
|
||||
return this.employeesService.remove(email);
|
||||
}
|
||||
|
||||
@Patch(':email')
|
||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiBearerAuth('access-token')
|
||||
|
|
@ -88,4 +51,47 @@ export class EmployeesController {
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//_____________________________________________________________________________________________
|
||||
// Deprecated or unused methods
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
// @Get()
|
||||
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING)
|
||||
// @ApiOperation({summary: 'Find all employees' })
|
||||
// @ApiResponse({ status: 200, description: 'List of employees found', type: CreateEmployeeDto, isArray: true })
|
||||
// @ApiResponse({ status: 400, description: 'List of employees not found' })
|
||||
// findAll(): Promise<Employees[]> {
|
||||
// return this.employeesService.findAll();
|
||||
// }
|
||||
|
||||
|
||||
// @Get(':email')
|
||||
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR,RoleEnum.ACCOUNTING )
|
||||
// @ApiOperation({summary: 'Find employee' })
|
||||
// @ApiResponse({ status: 200, description: 'Employee found', type: CreateEmployeeDto })
|
||||
// @ApiResponse({ status: 400, description: 'Employee not found' })
|
||||
// findOne(@Param('email', ParseIntPipe) email: string): Promise<Employees> {
|
||||
// return this.employeesService.findOne(email);
|
||||
// }
|
||||
|
||||
// @Get('profile/:email')
|
||||
// @ApiOperation({summary: 'Find employee profile' })
|
||||
// @ApiParam({ name: 'email', type: String, description: 'Identifier of the employee' })
|
||||
// @ApiResponse({ status: 200, description: 'Employee profile found', type: EmployeeProfileItemDto })
|
||||
// @ApiResponse({ status: 400, description: 'Employee profile not found' })
|
||||
// findOneProfile(@Param('email') email: string): Promise<EmployeeProfileItemDto> {
|
||||
// return this.employeesService.findOneProfile(email);
|
||||
// }
|
||||
|
||||
// @Delete(':email')
|
||||
// //@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR )
|
||||
// @ApiOperation({summary: 'Delete employee' })
|
||||
// @ApiParam({ name: 'email', type: Number, description: 'Email of the employee to delete' })
|
||||
// @ApiResponse({ status: 204, description: 'Employee deleted' })
|
||||
// @ApiResponse({ status: 404, description: 'Employee not found' })
|
||||
// remove(@Param('email', ParseIntPipe) email: string): Promise<Employees> {
|
||||
// return this.employeesService.remove(email);
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
|
||||
import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Put, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
|
||||
import { ExpensesQueryService } from "../services/expenses-query.service";
|
||||
import { CreateExpenseDto } from "../dtos/create-expense.dto";
|
||||
import { Expenses } from "@prisma/client";
|
||||
|
|
@ -8,6 +8,8 @@ import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagg
|
|||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||
import { ExpensesCommandService } from "../services/expenses-command.service";
|
||||
import { SearchExpensesDto } from "../dtos/search-expense.dto";
|
||||
import { UpsertExpenseDto } from "../dtos/upsert-expense.dto";
|
||||
import { UpsertExpenseResult } from "../types and interfaces/expenses.types.interfaces";
|
||||
|
||||
@ApiTags('Expenses')
|
||||
@ApiBearerAuth('access-token')
|
||||
|
|
@ -15,60 +17,73 @@ import { SearchExpensesDto } from "../dtos/search-expense.dto";
|
|||
@Controller('Expenses')
|
||||
export class ExpensesController {
|
||||
constructor(
|
||||
private readonly expensesService: ExpensesQueryService,
|
||||
private readonly expensesApprovalService: ExpensesCommandService,
|
||||
private readonly query: ExpensesQueryService,
|
||||
private readonly command: ExpensesCommandService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Create expense' })
|
||||
@ApiResponse({ status: 201, description: 'Expense created',type: CreateExpenseDto })
|
||||
@ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
||||
create(@Body() dto: CreateExpenseDto): Promise<Expenses> {
|
||||
return this.expensesService.create(dto);
|
||||
@Put('upsert/:email/:date')
|
||||
async upsert_by_date(
|
||||
@Param('email') email: string,
|
||||
@Param('date') date: string,
|
||||
@Body() dto: UpsertExpenseDto,
|
||||
): Promise<UpsertExpenseResult> {
|
||||
return this.command.upsertExpensesByDate(email, date, dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Find all expenses' })
|
||||
@ApiResponse({ status: 201, description: 'List of expenses found',type: CreateExpenseDto, isArray: true })
|
||||
@ApiResponse({ status: 400, description: 'List of expenses not found' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
findAll(@Query() filters: SearchExpensesDto): Promise<Expenses[]> {
|
||||
return this.expensesService.findAll(filters);
|
||||
}
|
||||
//_____________________________________________________________________________________________
|
||||
// Deprecated or unused methods
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
@Get(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Find expense' })
|
||||
@ApiResponse({ status: 201, description: 'Expense found',type: CreateExpenseDto })
|
||||
@ApiResponse({ status: 400, description: 'Expense not found' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number): Promise <Expenses> {
|
||||
return this.expensesService.findOne(id);
|
||||
}
|
||||
// @Post()
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Create expense' })
|
||||
// @ApiResponse({ status: 201, description: 'Expense created',type: CreateExpenseDto })
|
||||
// @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
||||
// create(@Body() dto: CreateExpenseDto): Promise<Expenses> {
|
||||
// return this.query.create(dto);
|
||||
// }
|
||||
|
||||
@Patch(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Expense shift' })
|
||||
@ApiResponse({ status: 201, description: 'Expense updated',type: CreateExpenseDto })
|
||||
@ApiResponse({ status: 400, description: 'Expense not found' })
|
||||
update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateExpenseDto) {
|
||||
return this.expensesService.update(id,dto);
|
||||
}
|
||||
// @Get()
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Find all expenses' })
|
||||
// @ApiResponse({ status: 201, description: 'List of expenses found',type: CreateExpenseDto, isArray: true })
|
||||
// @ApiResponse({ status: 400, description: 'List of expenses not found' })
|
||||
// @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
// findAll(@Query() filters: SearchExpensesDto): Promise<Expenses[]> {
|
||||
// return this.query.findAll(filters);
|
||||
// }
|
||||
|
||||
@Delete(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Delete expense' })
|
||||
@ApiResponse({ status: 201, description: 'Expense deleted',type: CreateExpenseDto })
|
||||
@ApiResponse({ status: 400, description: 'Expense not found' })
|
||||
remove(@Param('id', ParseIntPipe) id: number): Promise<Expenses> {
|
||||
return this.expensesService.remove(id);
|
||||
}
|
||||
// @Get(':id')
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Find expense' })
|
||||
// @ApiResponse({ status: 201, description: 'Expense found',type: CreateExpenseDto })
|
||||
// @ApiResponse({ status: 400, description: 'Expense not found' })
|
||||
// findOne(@Param('id', ParseIntPipe) id: number): Promise <Expenses> {
|
||||
// return this.query.findOne(id);
|
||||
// }
|
||||
|
||||
@Patch('approval/:id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) {
|
||||
return this.expensesApprovalService.updateApproval(id, isApproved);
|
||||
}
|
||||
// @Patch(':id')
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Expense shift' })
|
||||
// @ApiResponse({ status: 201, description: 'Expense updated',type: CreateExpenseDto })
|
||||
// @ApiResponse({ status: 400, description: 'Expense not found' })
|
||||
// update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateExpenseDto) {
|
||||
// return this.query.update(id,dto);
|
||||
// }
|
||||
|
||||
// @Delete(':id')
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Delete expense' })
|
||||
// @ApiResponse({ status: 201, description: 'Expense deleted',type: CreateExpenseDto })
|
||||
// @ApiResponse({ status: 400, description: 'Expense not found' })
|
||||
// remove(@Param('id', ParseIntPipe) id: number): Promise<Expenses> {
|
||||
// return this.query.remove(id);
|
||||
// }
|
||||
|
||||
// @Patch('approval/:id')
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) {
|
||||
// return this.command.updateApproval(id, isApproved);
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ export class CreateExpenseDto {
|
|||
description:'explain`s why the expense was made'
|
||||
})
|
||||
@IsString()
|
||||
comment?: string;
|
||||
comment: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'DENIED, APPROUVED, PENDING, etc...',
|
||||
|
|
|
|||
59
src/modules/expenses/dtos/upsert-expense.dto.ts
Normal file
59
src/modules/expenses/dtos/upsert-expense.dto.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { Transform, Type } from "class-transformer";
|
||||
import {
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
MaxLength,
|
||||
Min,
|
||||
ValidateIf,
|
||||
ValidateNested
|
||||
} from "class-validator";
|
||||
|
||||
export class ExpensePayloadDto {
|
||||
@IsString()
|
||||
type!: string;
|
||||
|
||||
@ValidateIf(o => (o.type ?? '').toUpperCase() !== 'MILEAGE')
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
amount?: number;
|
||||
|
||||
@ValidateIf(o => (o.type ?? '').toUpperCase() === 'MILEAGE')
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
mileage?: number;
|
||||
|
||||
@IsString()
|
||||
@MaxLength(280)
|
||||
@Transform(({ value }) => (typeof value === 'string' ? value.trim() : value))
|
||||
comment!: string;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => {
|
||||
if (value === null || value === undefined || value === '') return undefined;
|
||||
if (typeof value === 'number') return value.toString();
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length ? trimmed : undefined;
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
@IsString()
|
||||
@Matches(/^\d+$/)
|
||||
@MaxLength(255)
|
||||
attachment?: string;
|
||||
}
|
||||
|
||||
|
||||
export class UpsertExpenseDto {
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(()=> ExpensePayloadDto)
|
||||
old_expense?: ExpensePayloadDto;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(()=> ExpensePayloadDto)
|
||||
new_expense?: ExpensePayloadDto;
|
||||
}
|
||||
|
|
@ -3,12 +3,26 @@ import { Module } from "@nestjs/common";
|
|||
import { ExpensesQueryService } from "./services/expenses-query.service";
|
||||
import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module";
|
||||
import { ExpensesCommandService } from "./services/expenses-command.service";
|
||||
import { BankCodesRepo } from "./repos/bank-codes.repo";
|
||||
import { TimesheetsRepo } from "./repos/timesheets.repo";
|
||||
import { EmployeesRepo } from "./repos/employee.repo";
|
||||
|
||||
@Module({
|
||||
imports: [BusinessLogicsModule],
|
||||
controllers: [ExpensesController],
|
||||
providers: [ExpensesQueryService, ExpensesCommandService],
|
||||
exports: [ ExpensesQueryService ],
|
||||
providers: [
|
||||
ExpensesQueryService,
|
||||
ExpensesCommandService,
|
||||
BankCodesRepo,
|
||||
TimesheetsRepo,
|
||||
EmployeesRepo,
|
||||
],
|
||||
exports: [
|
||||
ExpensesQueryService,
|
||||
BankCodesRepo,
|
||||
TimesheetsRepo,
|
||||
EmployeesRepo,
|
||||
],
|
||||
})
|
||||
|
||||
export class ExpensesModule {}
|
||||
34
src/modules/expenses/repos/bank-codes.repo.ts
Normal file
34
src/modules/expenses/repos/bank-codes.repo.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
|
||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||
|
||||
@Injectable()
|
||||
export class BankCodesRepo {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
//find id and modifier by type
|
||||
readonly findByType = async ( type: string, client?: Tx
|
||||
): Promise<{id:number; modifier: number }> => {
|
||||
const db = client ?? this.prisma;
|
||||
const bank = await db.bankCodes.findFirst({
|
||||
where: {
|
||||
type,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
modifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
if(!bank) {
|
||||
throw new NotFoundException(`Unknown bank code type: ${type}`);
|
||||
}
|
||||
return {
|
||||
id: bank.id,
|
||||
modifier: bank.modifier,
|
||||
};
|
||||
};
|
||||
}
|
||||
32
src/modules/expenses/repos/employee.repo.ts
Normal file
32
src/modules/expenses/repos/employee.repo.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
|
||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||
|
||||
@Injectable()
|
||||
export class EmployeesRepo {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
// find employee id by email
|
||||
readonly findIdByEmail = async ( email: string, client?: Tx
|
||||
): Promise<number> => {
|
||||
const db = client ?? this.prisma;
|
||||
const employee = await db.employees.findFirst({
|
||||
where: {
|
||||
user: {
|
||||
email,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if(!employee) {
|
||||
throw new NotFoundException(`Employee with email: ${email} not found`);
|
||||
}
|
||||
return employee.id;
|
||||
}
|
||||
}
|
||||
42
src/modules/expenses/repos/timesheets.repo.ts
Normal file
42
src/modules/expenses/repos/timesheets.repo.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { weekStartMondayUTC } from "src/modules/shifts/helpers/shifts-date-time-helpers";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
|
||||
type Tx = Prisma.TransactionClient | PrismaClient;
|
||||
|
||||
@Injectable()
|
||||
export class TimesheetsRepo {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
//find an existing timesheet linked to the employee
|
||||
readonly ensureForDate = async (employee_id: number, date: Date, client?: Tx,
|
||||
): Promise<{id: number; start_date: Date }> => {
|
||||
const db = client ?? this.prisma;
|
||||
const startOfWeek = weekStartMondayUTC(date);
|
||||
const existing = await db.timesheets.findFirst({
|
||||
where: {
|
||||
employee_id: employee_id,
|
||||
start_date: startOfWeek,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
start_date: true,
|
||||
},
|
||||
});
|
||||
if(existing) return existing;
|
||||
|
||||
const created = await db.timesheets.create({
|
||||
data: {
|
||||
employee_id: employee_id,
|
||||
start_date: startOfWeek,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
start_date: true,
|
||||
},
|
||||
});
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,32 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { Expenses, Prisma } from "@prisma/client";
|
||||
import { Decimal } from "@prisma/client/runtime/library";
|
||||
import { transcode } from "buffer";
|
||||
import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { Expenses, Prisma } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { UpsertExpenseDto } from "../dtos/upsert-expense.dto";
|
||||
import { BankCodesRepo } from "../repos/bank-codes.repo";
|
||||
import { TimesheetsRepo } from "../repos/timesheets.repo";
|
||||
import { EmployeesRepo } from "../repos/employee.repo";
|
||||
import { toDateOnlyUTC } from "src/modules/shifts/helpers/shifts-date-time-helpers";
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException
|
||||
} from "@nestjs/common";
|
||||
import { ExpenseResponse, UpsertAction } from "../types and interfaces/expenses.types.interfaces";
|
||||
import {
|
||||
assertAndTrimComment,
|
||||
computeMileageAmount,
|
||||
mapDbExpenseToDayResponse,
|
||||
normalizeType as normalizeTypeUtil
|
||||
} from "../utils/expenses.utils";
|
||||
|
||||
@Injectable()
|
||||
export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||
constructor(prisma: PrismaService) { super(prisma); }
|
||||
constructor(
|
||||
prisma: PrismaService,
|
||||
private readonly bankCodesRepo: BankCodesRepo,
|
||||
private readonly timesheetsRepo: TimesheetsRepo,
|
||||
private readonly employeesRepo: EmployeesRepo,
|
||||
) { super(prisma); }
|
||||
|
||||
protected get delegate() {
|
||||
return this.prisma.expenses;
|
||||
|
|
@ -22,4 +41,273 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
|||
this.updateApprovalWithTransaction(transaction, id, isApproved),
|
||||
);
|
||||
}
|
||||
|
||||
//-------------------- Master CRUD function --------------------
|
||||
readonly upsertExpensesByDate = async (email: string, date: string, dto: UpsertExpenseDto,
|
||||
): Promise<{ action:UpsertAction; day: ExpenseResponse[] }> => {
|
||||
|
||||
//validates if there is an existing expense, at least 1 old or new
|
||||
const { old_expense, new_expense } = dto ?? {};
|
||||
if(!old_expense && !new_expense) {
|
||||
throw new BadRequestException('At least one expense must be provided');
|
||||
}
|
||||
|
||||
//validate date format
|
||||
const date_only = toDateOnlyUTC(date);
|
||||
if(Number.isNaN(date_only.getTime())) {
|
||||
throw new BadRequestException('Invalid date format (expected: YYYY-MM-DD)');
|
||||
}
|
||||
|
||||
//resolve employee_id by email
|
||||
const employee_id = await this.resolveEmployeeIdByEmail(email);
|
||||
|
||||
//make sure a timesheet existes
|
||||
const timesheet_id = await this.ensureTimesheetForDate(employee_id, date_only);
|
||||
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const loadDay = async (): Promise<ExpenseResponse[]> => {
|
||||
const rows = await tx.expenses.findMany({
|
||||
where: {
|
||||
timesheet_id: timesheet_id,
|
||||
date: date_only,
|
||||
},
|
||||
include: {
|
||||
bank_code: {
|
||||
select: {
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ date: 'asc' }, { id: 'asc' }],
|
||||
});
|
||||
|
||||
return rows.map((r) =>
|
||||
this.mapDbToDayResponse({
|
||||
date: r.date,
|
||||
amount: r.amount ?? 0,
|
||||
mileage: r.mileage ?? 0,
|
||||
comment: r.comment,
|
||||
is_approved: r.is_approved,
|
||||
bank_code: r.bank_code,
|
||||
}));
|
||||
};
|
||||
|
||||
const normalizePayload = async (payload: {
|
||||
type: string;
|
||||
amount?: number;
|
||||
mileage?: number;
|
||||
comment: string;
|
||||
attachment?: string | number;
|
||||
}): Promise<{
|
||||
type: string;
|
||||
bank_code_id: number;
|
||||
amount: Prisma.Decimal;
|
||||
mileage: number | null;
|
||||
comment: string;
|
||||
attachment: number | null;
|
||||
}> => {
|
||||
const type = this.normalizeType(payload.type);
|
||||
const comment = this.assertAndTrimComment(payload.comment);
|
||||
const attachment = this.parseAttachmentId(payload.attachment);
|
||||
|
||||
const { id: bank_code_id, modifier } = await this.lookupBankCodeOrThrow(tx, type);
|
||||
let amount = this.computeAmountDecimal(type, payload, modifier);
|
||||
let mileage: number | null = null;
|
||||
|
||||
if (type === 'MILEAGE') {
|
||||
mileage = Number(payload.mileage ?? 0);
|
||||
if (!(mileage > 0)) {
|
||||
throw new BadRequestException('Mileage required and must be > 0 for type MILEAGE');
|
||||
}
|
||||
|
||||
const amountNumber = computeMileageAmount(mileage, modifier);
|
||||
amount = new Prisma.Decimal(amountNumber);
|
||||
|
||||
} else {
|
||||
if (!(typeof payload.amount === 'number' && payload.amount >= 0)) {
|
||||
throw new BadRequestException('Amount required for non-MILEAGE expense');
|
||||
}
|
||||
amount = new Prisma.Decimal(payload.amount);
|
||||
}
|
||||
|
||||
if (attachment !== null) {
|
||||
const attachmentRow = await tx.attachments.findUnique({
|
||||
where: { id: attachment },
|
||||
select: { status: true },
|
||||
});
|
||||
if (!attachmentRow || attachmentRow.status !== 'ACTIVE') {
|
||||
throw new BadRequestException('Attachment not found or inactive');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
bank_code_id,
|
||||
amount,
|
||||
mileage,
|
||||
comment,
|
||||
attachment
|
||||
};
|
||||
};
|
||||
|
||||
const findExactOld = async (norm: {
|
||||
bank_code_id: number;
|
||||
amount: Prisma.Decimal;
|
||||
mileage: number | null;
|
||||
comment: string;
|
||||
attachment: number | null;
|
||||
}) => {
|
||||
return tx.expenses.findFirst({
|
||||
where: {
|
||||
timesheet_id: timesheet_id,
|
||||
date: date_only,
|
||||
bank_code_id: norm.bank_code_id,
|
||||
amount: norm.amount,
|
||||
comment: norm.comment,
|
||||
attachment: norm.attachment,
|
||||
...(norm.mileage !== null ? { mileage: norm.mileage } : { mileage: null }),
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
};
|
||||
|
||||
let action : UpsertAction;
|
||||
//-------------------- DELETE --------------------
|
||||
if(old_expense && !new_expense) {
|
||||
const oldNorm = await normalizePayload(old_expense);
|
||||
const existing = await findExactOld(oldNorm);
|
||||
if(!existing) {
|
||||
throw new NotFoundException({
|
||||
error_code: 'EXPENSE_STALE',
|
||||
message: 'The expense was modified or deleted by someone else',
|
||||
});
|
||||
}
|
||||
await tx.expenses.delete({where: { id: existing.id } });
|
||||
action = 'delete';
|
||||
}
|
||||
//-------------------- CREATE --------------------
|
||||
else if (!old_expense && new_expense) {
|
||||
const new_exp = await normalizePayload(new_expense);
|
||||
await tx.expenses.create({
|
||||
data: {
|
||||
timesheet_id: timesheet_id,
|
||||
date: date_only,
|
||||
bank_code_id: new_exp.bank_code_id,
|
||||
amount: new_exp.amount,
|
||||
mileage: new_exp.mileage,
|
||||
comment: new_exp.comment,
|
||||
attachment: new_exp.attachment,
|
||||
is_approved: false,
|
||||
},
|
||||
});
|
||||
action = 'create';
|
||||
}
|
||||
//-------------------- UPDATE --------------------
|
||||
else if(old_expense && new_expense) {
|
||||
const oldNorm = await normalizePayload(old_expense);
|
||||
const existing = await findExactOld(oldNorm);
|
||||
if(!existing) {
|
||||
throw new NotFoundException({
|
||||
error_code: 'EXPENSE_STALE',
|
||||
message: 'The expense was modified or deleted by someone else',
|
||||
});
|
||||
}
|
||||
|
||||
const new_exp = await normalizePayload(new_expense);
|
||||
await tx.expenses.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
bank_code_id: new_exp.bank_code_id,
|
||||
amount: new_exp.amount,
|
||||
mileage: new_exp.mileage,
|
||||
comment: new_exp.comment,
|
||||
attachment: new_exp.attachment,
|
||||
},
|
||||
});
|
||||
action = 'update';
|
||||
}
|
||||
else {
|
||||
throw new BadRequestException('Invalid upsert combination');
|
||||
}
|
||||
|
||||
const day = await loadDay();
|
||||
|
||||
return { action, day };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//-------------------- helpers --------------------
|
||||
private readonly normalizeType = (type: string): string =>
|
||||
normalizeTypeUtil(type);
|
||||
|
||||
private readonly assertAndTrimComment = (comment: string): string =>
|
||||
assertAndTrimComment(comment);
|
||||
|
||||
private readonly parseAttachmentId = (value: unknown): number | null => {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
if (!Number.isInteger(value) || value <= 0) {
|
||||
throw new BadRequestException('Invalid attachment id');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed.length) return null;
|
||||
if (!/^\d+$/.test(trimmed)) throw new BadRequestException('Invalid attachment id');
|
||||
|
||||
const parsed = Number(trimmed);
|
||||
if (parsed <= 0) throw new BadRequestException('Invalid attachment id');
|
||||
|
||||
return parsed;
|
||||
}
|
||||
throw new BadRequestException('Invalid attachment id');
|
||||
};
|
||||
|
||||
|
||||
private readonly resolveEmployeeIdByEmail = async (email: string): Promise<number> =>
|
||||
this.employeesRepo.findIdByEmail(email);
|
||||
|
||||
private readonly ensureTimesheetForDate = async ( employee_id: number, date: Date
|
||||
): Promise<number> => {
|
||||
const { id } = await this.timesheetsRepo.ensureForDate(employee_id, date);
|
||||
return id;
|
||||
};
|
||||
|
||||
private readonly lookupBankCodeOrThrow = async ( transaction: Prisma.TransactionClient, type: string
|
||||
): Promise<{id: number; modifier: number}> =>
|
||||
this.bankCodesRepo.findByType(type, transaction);
|
||||
|
||||
private readonly computeAmountDecimal = (
|
||||
type: string,
|
||||
payload: {
|
||||
amount?: number;
|
||||
mileage?: number;
|
||||
},
|
||||
modifier: number,
|
||||
): Prisma.Decimal => {
|
||||
if(type === 'MILEAGE') {
|
||||
const km = payload.mileage ?? 0;
|
||||
const amountNumber = computeMileageAmount(km, modifier);
|
||||
return new Prisma.Decimal(amountNumber);
|
||||
}
|
||||
return new Prisma.Decimal(payload.amount!);
|
||||
};
|
||||
|
||||
private readonly mapDbToDayResponse = (row: {
|
||||
date: Date;
|
||||
amount: Prisma.Decimal | number | string;
|
||||
mileage: Prisma.Decimal | number | string;
|
||||
comment: string;
|
||||
is_approved: boolean;
|
||||
bank_code: { type: string } | null;
|
||||
}): ExpenseResponse => mapDbExpenseToDayResponse(row);
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -121,7 +121,7 @@ export class ExpensesQueryService {
|
|||
bank_code_id: exp.bank_code_id,
|
||||
date: exp.date,
|
||||
amount: exp.amount,
|
||||
attachement: exp.attachement,
|
||||
attachment: exp.attachment,
|
||||
comment: exp.comment,
|
||||
is_approved: exp.is_approved,
|
||||
supervisor_comment: exp.supervisor_comment,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
export type UpsertAction = 'create' | 'update' | 'delete';
|
||||
|
||||
export interface ExpenseResponse {
|
||||
date: string;
|
||||
type: string;
|
||||
amount: number;
|
||||
comment: string;
|
||||
is_approved: boolean;
|
||||
};
|
||||
|
||||
export type UpsertExpenseResult = {
|
||||
action: UpsertAction;
|
||||
day: ExpenseResponse[]
|
||||
};
|
||||
69
src/modules/expenses/utils/expenses.utils.ts
Normal file
69
src/modules/expenses/utils/expenses.utils.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { BadRequestException } from "@nestjs/common";
|
||||
import { ExpenseResponse } from "../types and interfaces/expenses.types.interfaces";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
//uppercase and trim for validation
|
||||
export function normalizeType(type: string): string {
|
||||
return (type ?? '').trim().toUpperCase();
|
||||
};
|
||||
|
||||
//required comment after trim
|
||||
export function assertAndTrimComment(comment: string): string {
|
||||
const cmt = (comment ?? '').trim();
|
||||
if(cmt.length === 0) {
|
||||
throw new BadRequestException('A comment is required');
|
||||
}
|
||||
return cmt;
|
||||
};
|
||||
|
||||
//rounding $ to 2 decimals
|
||||
export function roundMoney2(num: number): number {
|
||||
return Math.round((num + Number.EPSILON) * 100)/ 100;
|
||||
};
|
||||
|
||||
export function computeMileageAmount(km: number, modifier: number): number {
|
||||
if(km < 0) throw new BadRequestException('mileage must be positive');
|
||||
if(modifier < 0) throw new BadRequestException('modifier must be positive');
|
||||
return roundMoney2(km * modifier);
|
||||
};
|
||||
|
||||
//compat. types with Prisma.Decimal. work around Prisma import in utils.
|
||||
export type DecimalLike =
|
||||
| number
|
||||
| string
|
||||
| { toNumber?: () => number }
|
||||
| { toString?: () => string };
|
||||
|
||||
|
||||
//safe conversion to number
|
||||
export function toNumberSafe(value: DecimalLike): number {
|
||||
if(typeof value === 'number') return value;
|
||||
if(value && typeof (value as any).toNumber === 'function') return (value as any).toNumber();
|
||||
return Number(
|
||||
typeof (value as any)?.toString === 'function'
|
||||
? (value as any).toString()
|
||||
: value,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
//map of a row for DayExpenseResponse
|
||||
export function mapDbExpenseToDayResponse(row: {
|
||||
date: Date;
|
||||
amount: Prisma.Decimal | number | string | null;
|
||||
mileage?: Prisma.Decimal | number | string | null;
|
||||
comment: string;
|
||||
is_approved: boolean;
|
||||
bank_code?: { type?: string | null } | null;
|
||||
}): ExpenseResponse {
|
||||
const yyyyMmDd = row.date.toISOString().slice(0,10);
|
||||
const toNum = (value: any)=> (value == null ? 0 : Number(value));
|
||||
return {
|
||||
date: yyyyMmDd,
|
||||
type: normalizeType(row.bank_code?.type ?? 'UNKNOWN'),
|
||||
amount: toNum(row.amount),
|
||||
comment: row.comment,
|
||||
is_approved: row.is_approved,
|
||||
...(row.mileage !== null ? { mileage: toNum(row.mileage) }: {}),
|
||||
};
|
||||
}
|
||||
|
|
@ -1,76 +1,30 @@
|
|||
import { Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from "@nestjs/common";
|
||||
import { LeaveRequestsService } from "../services/leave-requests.service";
|
||||
import { CreateLeaveRequestsDto } from "../dtos/create-leave-request.dto";
|
||||
import { LeaveRequests } from "@prisma/client";
|
||||
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-request.dto";
|
||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||
import { LeaveApprovalStatus, Roles as RoleEnum } from '.prisma/client';
|
||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { SearchLeaveRequestsDto } from "../dtos/search-leave-request.dto";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request.view.dto";
|
||||
import { Body, Controller, Post } from "@nestjs/common";
|
||||
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||
import { LeaveRequestsService } from "../services/leave-request.service";
|
||||
import { UpsertLeaveRequestDto } from "../dtos/upsert-leave-request.dto";
|
||||
import { LeaveTypes } from "@prisma/client";
|
||||
|
||||
@ApiTags('Leave Requests')
|
||||
@ApiBearerAuth('access-token')
|
||||
// @UseGuards()
|
||||
@Controller('leave-requests')
|
||||
export class LeaveRequestController {
|
||||
constructor(private readonly leaveRequetsService: LeaveRequestsService){}
|
||||
constructor(private readonly leave_service: LeaveRequestsService){}
|
||||
|
||||
@Post()
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({summary: 'Create leave request' })
|
||||
@ApiResponse({ status: 201, description: 'Leave request created',type: CreateLeaveRequestsDto })
|
||||
@ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
||||
create(@Body() dto: CreateLeaveRequestsDto): Promise<LeaveRequestViewDto> {
|
||||
return this. leaveRequetsService.create(dto);
|
||||
}
|
||||
@Post('upsert')
|
||||
async upsertLeaveRequest(@Body() dto: UpsertLeaveRequestDto) {
|
||||
const { action, leave_requests } = await this.leave_service.handle(dto);
|
||||
return { action, leave_requests };
|
||||
}q
|
||||
|
||||
@Get()
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({summary: 'Find all leave request' })
|
||||
@ApiResponse({ status: 201, description: 'List of Leave requests found',type: LeaveRequestViewDto, isArray: true })
|
||||
@ApiResponse({ status: 400, description: 'List of leave request not found' })
|
||||
@UsePipes(new ValidationPipe({transform: true, whitelist: true}))
|
||||
findAll(@Query() filters: SearchLeaveRequestsDto): Promise<LeaveRequestViewDto[]> {
|
||||
return this.leaveRequetsService.findAll(filters);
|
||||
}
|
||||
//remove emp_id and use email
|
||||
@Get(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({summary: 'Find leave request' })
|
||||
@ApiResponse({ status: 201, description: 'Leave request found',type: LeaveRequestViewDto })
|
||||
@ApiResponse({ status: 400, description: 'Leave request not found' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number): Promise<LeaveRequestViewDto> {
|
||||
return this.leaveRequetsService.findOne(id);
|
||||
}
|
||||
//remove emp_id and use email
|
||||
@Patch(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({summary: 'Update leave request' })
|
||||
@ApiResponse({ status: 201, description: 'Leave request updated',type: LeaveRequestViewDto })
|
||||
@ApiResponse({ status: 400, description: 'Leave request not found' })
|
||||
update(@Param('id', ParseIntPipe) id: number,@Body() dto: UpdateLeaveRequestsDto): Promise<LeaveRequestViewDto> {
|
||||
return this.leaveRequetsService.update(id, dto);
|
||||
}
|
||||
//TODO:
|
||||
/*
|
||||
@Get('archive')
|
||||
findAllArchived(){...}
|
||||
|
||||
//remove emp_id and use email
|
||||
@Delete(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({summary: 'Delete leave request' })
|
||||
@ApiResponse({ status: 201, description: 'Leave request deleted',type: CreateLeaveRequestsDto })
|
||||
@ApiResponse({ status: 400, description: 'Leave request not found' })
|
||||
remove(@Param('id', ParseIntPipe) id: number): Promise<LeaveRequestViewDto> {
|
||||
return this.leaveRequetsService.remove(id);
|
||||
}
|
||||
@Get('archive/:id')
|
||||
findOneArchived(id){...}
|
||||
*/
|
||||
|
||||
//remove emp_id and use email
|
||||
@Patch('approval/:id')
|
||||
updateApproval( @Param('id', ParseIntPipe) id: number,
|
||||
@Body('is_approved', ParseBoolPipe) is_approved: boolean): Promise<LeaveRequestViewDto> {
|
||||
const approvalStatus = is_approved ?
|
||||
LeaveApprovalStatus.APPROVED : LeaveApprovalStatus.DENIED;
|
||||
return this.leaveRequetsService.update(id, { approval_status: approvalStatus });
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsDateString, IsEmail, IsEnum, IsInt, IsISO8601, IsNotEmpty, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class CreateLeaveRequestsDto {
|
||||
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 7,
|
||||
description: 'ID number of a leave-request code (link with bank-codes)',
|
||||
})
|
||||
@Type(()=> Number)
|
||||
@IsInt()
|
||||
bank_code_id: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Sick or Vacation or Unpaid or Bereavement or Parental or Legal',
|
||||
description: 'type of leave request for an accounting perception',
|
||||
})
|
||||
@IsEnum(LeaveTypes)
|
||||
leave_type: LeaveTypes;
|
||||
|
||||
@ApiProperty({
|
||||
example: '22/06/2463',
|
||||
description: 'Leave request`s start date',
|
||||
})
|
||||
@IsISO8601()
|
||||
start_date_time:string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '25/03/3019',
|
||||
description: 'Leave request`s end date',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsISO8601()
|
||||
end_date_time?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'My precious',
|
||||
description: 'Leave request`s comment',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
comment: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'True or False or Pending or Denied or Cancelled or Escalated',
|
||||
description: 'Leave request`s approval status',
|
||||
})
|
||||
@IsEnum(LeaveApprovalStatus)
|
||||
@IsOptional()
|
||||
approval_status?: LeaveApprovalStatus;
|
||||
}
|
||||
|
|
@ -1,13 +1,14 @@
|
|||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
|
||||
export class LeaveRequestViewDto {
|
||||
id!: number;
|
||||
id: number;
|
||||
leave_type!: LeaveTypes;
|
||||
start_date_time!: string;
|
||||
end_date_time!: string | null;
|
||||
comment!: string | null;
|
||||
date!: string;
|
||||
comment!: string;
|
||||
approval_status: LeaveApprovalStatus;
|
||||
email!: string;
|
||||
employee_full_name: string;
|
||||
days_requested?: number;
|
||||
employee_full_name!: string;
|
||||
payable_hours?: number;
|
||||
requested_hours?: number;
|
||||
action?: 'create' | 'update' | 'delete';
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsOptional, IsInt, IsEnum, IsDateString, IsEmail } from "class-validator";
|
||||
|
||||
export class SearchLeaveRequestsDto {
|
||||
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsOptional()
|
||||
@Type(()=> Number)
|
||||
@IsInt()
|
||||
bank_code_id?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(LeaveApprovalStatus)
|
||||
approval_status?: LeaveApprovalStatus
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
start_date?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
end_date?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(LeaveTypes)
|
||||
leave_type?: LeaveTypes;
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import { PartialType } from "@nestjs/swagger";
|
||||
import { CreateLeaveRequestsDto } from "./create-leave-request.dto";
|
||||
|
||||
export class UpdateLeaveRequestsDto extends PartialType(CreateLeaveRequestsDto){}
|
||||
51
src/modules/leave-requests/dtos/upsert-leave-request.dto.ts
Normal file
51
src/modules/leave-requests/dtos/upsert-leave-request.dto.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { IsEmail, IsArray, ArrayNotEmpty, ArrayUnique, IsISO8601, IsIn, IsOptional, IsString, IsNumber, Min, Max, IsEnum } from "class-validator";
|
||||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
import { LeaveRequestViewDto } from "./leave-request-view.dto";
|
||||
import { Type } from "class-transformer";
|
||||
|
||||
//sets wich function to call
|
||||
export const UPSERT_ACTIONS = ['create', 'update', 'delete'] as const;
|
||||
export type UpsertAction = (typeof UPSERT_ACTIONS)[number];
|
||||
|
||||
//sets wich types to use
|
||||
export const REQUEST_TYPES = Object.values(LeaveTypes) as readonly LeaveTypes[];
|
||||
export type RequestTypes = (typeof REQUEST_TYPES)[number];
|
||||
|
||||
//filter requests by type and action
|
||||
export interface UpsertResult {
|
||||
action: UpsertAction;
|
||||
leave_requests: LeaveRequestViewDto[];
|
||||
}
|
||||
|
||||
export class UpsertLeaveRequestDto {
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@ArrayUnique()
|
||||
@IsISO8601({}, { each: true })
|
||||
dates!: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(LeaveTypes)
|
||||
type!: string;
|
||||
|
||||
@IsIn(UPSERT_ACTIONS)
|
||||
action!: UpsertAction;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
comment?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber({ maxDecimalPlaces: 2 })
|
||||
@Min(0)
|
||||
@Max(24)
|
||||
requested_hours?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(LeaveApprovalStatus)
|
||||
approval_status?: LeaveApprovalStatus
|
||||
}
|
||||
|
|
@ -1,13 +1,28 @@
|
|||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { LeaveRequestController } from "./controllers/leave-requests.controller";
|
||||
import { LeaveRequestsService } from "./services/leave-requests.service";
|
||||
import { HolidayLeaveRequestsService } from "./services/holiday-leave-requests.service";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module";
|
||||
import { VacationLeaveRequestsService } from "./services/vacation-leave-requests.service";
|
||||
import { SickLeaveRequestsService } from "./services/sick-leave-requests.service";
|
||||
import { LeaveRequestsService } from "./services/leave-request.service";
|
||||
import { ShiftsModule } from "../shifts/shifts.module";
|
||||
import { LeaveRequestsUtils } from "./utils/leave-request.util";
|
||||
|
||||
@Module({
|
||||
imports: [BusinessLogicsModule],
|
||||
imports: [BusinessLogicsModule, ShiftsModule],
|
||||
controllers: [LeaveRequestController],
|
||||
providers: [LeaveRequestsService],
|
||||
exports: [LeaveRequestsService],
|
||||
providers: [
|
||||
VacationLeaveRequestsService,
|
||||
SickLeaveRequestsService,
|
||||
HolidayLeaveRequestsService,
|
||||
LeaveRequestsService,
|
||||
PrismaService,
|
||||
LeaveRequestsUtils,
|
||||
],
|
||||
exports: [
|
||||
LeaveRequestsService,
|
||||
],
|
||||
})
|
||||
|
||||
export class LeaveRequestsModule {}
|
||||
|
|
@ -1,14 +1,20 @@
|
|||
import { LeaveRequestViewDto } from "../dtos/leave-request.view.dto";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||
import { LeaveRequestArchiveRow } from "../utils/leave-requests-archive.select";
|
||||
|
||||
const toISO = (date: Date | null): string | null => (date ? date.toISOString().slice(0,10): null);
|
||||
const toNum = (value?: Prisma.Decimal | null) => value ? Number(value) : undefined;
|
||||
|
||||
export function mapArchiveRowToView(row: LeaveRequestArchiveRow, email: string, employee_full_name:string): LeaveRequestViewDto {
|
||||
const isoDate = row.date?.toISOString().slice(0, 10);
|
||||
if (!isoDate) {
|
||||
throw new Error(`Leave request #${row.id} has no date set.`);
|
||||
}
|
||||
return {
|
||||
id: row.id,
|
||||
leave_type: row.leave_type,
|
||||
start_date_time: toISO(row.start_date_time)!,
|
||||
end_date_time: toISO(row.end_date_time),
|
||||
date: isoDate,
|
||||
payable_hours: toNum(row.payable_hours),
|
||||
requested_hours: toNum(row.requested_hours),
|
||||
comment: row.comment,
|
||||
approval_status: row.approval_status,
|
||||
email,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,23 @@
|
|||
import { LeaveRequestViewDto } from "../dtos/leave-request.view.dto";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||
import { LeaveRequestRow } from "../utils/leave-requests.select";
|
||||
|
||||
function toISODateString(date:Date | null): string | null {
|
||||
return date ? date.toISOString().slice(0,10) : null;
|
||||
}
|
||||
const toNum = (value?: Prisma.Decimal | null) =>
|
||||
value !== null && value !== undefined ? Number(value) : undefined;
|
||||
|
||||
export function mapRowToView(row: LeaveRequestRow): LeaveRequestViewDto {
|
||||
const iso_date = row.date?.toISOString().slice(0, 10);
|
||||
if (!iso_date) throw new Error(`Leave request #${row.id} has no date set.`);
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
leave_type: row.leave_type,
|
||||
start_date_time: toISODateString(row.start_date_time)!,
|
||||
end_date_time: toISODateString(row.end_date_time),
|
||||
date: iso_date,
|
||||
payable_hours: toNum(row.payable_hours),
|
||||
requested_hours: toNum(row.requested_hours),
|
||||
comment: row.comment,
|
||||
approval_status: row.approval_status,
|
||||
email: row.employee.user.email,
|
||||
employee_full_name: `${row.employee.user.first_name} ${row.employee.user.last_name}`
|
||||
}
|
||||
employee_full_name: `${row.employee.user.first_name} ${row.employee.user.last_name}`,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import { UpsertLeaveRequestDto, UpsertResult } from '../dtos/upsert-leave-request.dto';
|
||||
import { LeaveRequestViewDto } from '../dtos/leave-request-view.dto';
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { LeaveApprovalStatus, LeaveTypes } from '@prisma/client';
|
||||
import { HolidayService } from 'src/modules/business-logics/services/holiday.service';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { mapRowToView } from '../mappers/leave-requests.mapper';
|
||||
import { leaveRequestsSelect } from '../utils/leave-requests.select';
|
||||
import { LeaveRequestsUtils, normalizeDates, toDateOnly } from '../utils/leave-request.util';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class HolidayLeaveRequestsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly holidayService: HolidayService,
|
||||
private readonly leaveUtils: LeaveRequestsUtils,
|
||||
) {}
|
||||
|
||||
async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
|
||||
const email = dto.email.trim();
|
||||
const employee_id = await this.leaveUtils.resolveEmployeeIdByEmail(email);
|
||||
const bank_code = await this.leaveUtils.resolveBankCodeByType(LeaveTypes.HOLIDAY);
|
||||
if(!bank_code) throw new NotFoundException(`bank_code not found`);
|
||||
const dates = normalizeDates(dto.dates);
|
||||
if (!dates.length) throw new BadRequestException('Dates array must not be empty');
|
||||
|
||||
const created: LeaveRequestViewDto[] = [];
|
||||
|
||||
for (const iso_date of dates) {
|
||||
const date = toDateOnly(iso_date);
|
||||
|
||||
const existing = await this.prisma.leaveRequests.findUnique({
|
||||
where: {
|
||||
leave_per_employee_date: {
|
||||
employee_id: employee_id,
|
||||
leave_type: LeaveTypes.HOLIDAY,
|
||||
date,
|
||||
},
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing) {
|
||||
throw new BadRequestException(`Holiday request already exists for ${iso_date}`);
|
||||
}
|
||||
|
||||
const payable = await this.holidayService.calculateHolidayPay(email, date, bank_code.modifier);
|
||||
const row = await this.prisma.leaveRequests.create({
|
||||
data: {
|
||||
employee_id: employee_id,
|
||||
bank_code_id: bank_code.id,
|
||||
leave_type: LeaveTypes.HOLIDAY,
|
||||
date,
|
||||
comment: dto.comment ?? '',
|
||||
requested_hours: dto.requested_hours ?? 8,
|
||||
payable_hours: payable,
|
||||
approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING,
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
|
||||
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
|
||||
if (row.approval_status === LeaveApprovalStatus.APPROVED) {
|
||||
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours,LeaveTypes.HOLIDAY, row.comment);
|
||||
}
|
||||
|
||||
created.push({ ...mapRowToView(row), action: 'create' });
|
||||
}
|
||||
|
||||
return { action: 'create', leave_requests: created };
|
||||
}
|
||||
}
|
||||
|
||||
243
src/modules/leave-requests/services/leave-request.service.ts
Normal file
243
src/modules/leave-requests/services/leave-request.service.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
import { roundToQuarterHour } from "src/common/utils/date-utils";
|
||||
import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||
import { mapRowToView } from "../mappers/leave-requests.mapper";
|
||||
import { leaveRequestsSelect } from "../utils/leave-requests.select";
|
||||
import { HolidayLeaveRequestsService } from "./holiday-leave-requests.service";
|
||||
import { SickLeaveRequestsService } from "./sick-leave-requests.service";
|
||||
import { VacationLeaveRequestsService } from "./vacation-leave-requests.service";
|
||||
import { HolidayService } from "src/modules/business-logics/services/holiday.service";
|
||||
import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service";
|
||||
import { VacationService } from "src/modules/business-logics/services/vacation.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { LeaveRequestsUtils, normalizeDates, toDateOnly, toISODateKey } from "../utils/leave-request.util";
|
||||
|
||||
@Injectable()
|
||||
export class LeaveRequestsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly holidayLeaveService: HolidayLeaveRequestsService,
|
||||
private readonly holidayService: HolidayService,
|
||||
private readonly sickLogic: SickLeaveService,
|
||||
private readonly sickLeaveService: SickLeaveRequestsService,
|
||||
private readonly vacationLeaveService: VacationLeaveRequestsService,
|
||||
private readonly vacationLogic: VacationService,
|
||||
private readonly leaveUtils: LeaveRequestsUtils,
|
||||
) {}
|
||||
|
||||
//handle distribution to the right service according to the selected type and action
|
||||
async handle(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
|
||||
switch (dto.type) {
|
||||
case LeaveTypes.HOLIDAY:
|
||||
if( dto.action === 'create'){
|
||||
return this.holidayLeaveService.create(dto);
|
||||
} else if (dto.action === 'update') {
|
||||
return this.update(dto, LeaveTypes.HOLIDAY);
|
||||
} else if (dto.action === 'delete'){
|
||||
return this.delete(dto, LeaveTypes.HOLIDAY);
|
||||
}
|
||||
case LeaveTypes.VACATION:
|
||||
if( dto.action === 'create'){
|
||||
return this.vacationLeaveService.create(dto);
|
||||
} else if (dto.action === 'update') {
|
||||
return this.update(dto, LeaveTypes.VACATION);
|
||||
} else if (dto.action === 'delete'){
|
||||
return this.delete(dto, LeaveTypes.VACATION);
|
||||
}
|
||||
case LeaveTypes.SICK:
|
||||
if( dto.action === 'create'){
|
||||
return this.sickLeaveService.create(dto);
|
||||
} else if (dto.action === 'update') {
|
||||
return this.update(dto, LeaveTypes.SICK);
|
||||
} else if (dto.action === 'delete'){
|
||||
return this.delete(dto, LeaveTypes.SICK);
|
||||
}
|
||||
default:
|
||||
throw new BadRequestException(`Unsupported leave type: ${dto.type} or action: ${dto.action}`);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise<UpsertResult> {
|
||||
const email = dto.email.trim();
|
||||
const dates = normalizeDates(dto.dates);
|
||||
const employee_id = await this.leaveUtils.resolveEmployeeIdByEmail(email);
|
||||
if (!dates.length) throw new BadRequestException("Dates array must not be empty");
|
||||
|
||||
const rows = await this.prisma.leaveRequests.findMany({
|
||||
where: {
|
||||
employee_id: employee_id,
|
||||
leave_type: type,
|
||||
date: { in: dates.map((d) => toDateOnly(d)) },
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
|
||||
if (rows.length !== dates.length) {
|
||||
const missing = dates.filter((isoDate) => !rows.some((row) => toISODateKey(row.date) === isoDate));
|
||||
throw new NotFoundException(`No Leave request found for: ${missing.join(", ")}`);
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.approval_status === LeaveApprovalStatus.APPROVED) {
|
||||
const iso = toISODateKey(row.date);
|
||||
await this.leaveUtils.removeShift(email, employee_id, iso, type);
|
||||
}
|
||||
}
|
||||
|
||||
await this.prisma.leaveRequests.deleteMany({
|
||||
where: { id: { in: rows.map((row) => row.id) } },
|
||||
});
|
||||
|
||||
const deleted = rows.map((row) => ({ ...mapRowToView(row), action: "delete" as const }));
|
||||
return { action: "delete", leave_requests: deleted };
|
||||
}
|
||||
|
||||
async update(dto: UpsertLeaveRequestDto, type: LeaveTypes): Promise<UpsertResult> {
|
||||
const email = dto.email.trim();
|
||||
const employee_id = await this.leaveUtils.resolveEmployeeIdByEmail(email);
|
||||
const bank_code = await this.leaveUtils.resolveBankCodeByType(type);
|
||||
if(!bank_code) throw new NotFoundException(`bank_code not found`);
|
||||
const modifier = Number(bank_code.modifier ?? 1);
|
||||
const dates = normalizeDates(dto.dates);
|
||||
if (!dates.length) {
|
||||
throw new BadRequestException("Dates array must not be empty");
|
||||
}
|
||||
|
||||
const entries = await Promise.all(
|
||||
dates.map(async (iso_date) => {
|
||||
const date = toDateOnly(iso_date);
|
||||
const existing = await this.prisma.leaveRequests.findUnique({
|
||||
where: {
|
||||
leave_per_employee_date: {
|
||||
employee_id: employee_id,
|
||||
leave_type: type,
|
||||
date,
|
||||
},
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
if (!existing) throw new NotFoundException(`No Leave request found for ${iso_date}`);
|
||||
return { iso_date, date, existing };
|
||||
}),
|
||||
);
|
||||
|
||||
const updated: LeaveRequestViewDto[] = [];
|
||||
|
||||
if (type === LeaveTypes.SICK) {
|
||||
const firstExisting = entries[0].existing;
|
||||
const fallbackRequested =
|
||||
firstExisting.requested_hours !== null && firstExisting.requested_hours !== undefined
|
||||
? Number(firstExisting.requested_hours)
|
||||
: 8;
|
||||
const requested_hours_per_day = dto.requested_hours ?? fallbackRequested;
|
||||
const reference_date = entries.reduce(
|
||||
(latest, entry) => (entry.date > latest ? entry.date : latest),
|
||||
entries[0].date,
|
||||
);
|
||||
const total_payable_hours = await this.sickLogic.calculateSickLeavePay(
|
||||
employee_id,
|
||||
reference_date,
|
||||
entries.length,
|
||||
requested_hours_per_day,
|
||||
modifier,
|
||||
);
|
||||
let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours));
|
||||
const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier);
|
||||
|
||||
for (const { iso_date, existing } of entries) {
|
||||
const previous_status = existing.approval_status;
|
||||
const payable = Math.min(remaining_payable_hours, daily_payable_cap);
|
||||
const payable_rounded = roundToQuarterHour(Math.max(0, payable));
|
||||
remaining_payable_hours = roundToQuarterHour(
|
||||
Math.max(0, remaining_payable_hours - payable_rounded),
|
||||
);
|
||||
|
||||
const row = await this.prisma.leaveRequests.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
comment: dto.comment ?? existing.comment,
|
||||
requested_hours: requested_hours_per_day,
|
||||
payable_hours: payable_rounded,
|
||||
bank_code_id: bank_code.id,
|
||||
approval_status: dto.approval_status ?? existing.approval_status,
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
|
||||
const was_approved = previous_status === LeaveApprovalStatus.APPROVED;
|
||||
const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED;
|
||||
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
|
||||
|
||||
if (!was_approved && is_approved) {
|
||||
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
|
||||
} else if (was_approved && !is_approved) {
|
||||
await this.leaveUtils.removeShift(email, employee_id, iso_date, type);
|
||||
} else if (was_approved && is_approved) {
|
||||
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
|
||||
}
|
||||
updated.push({ ...mapRowToView(row), action: "update" });
|
||||
}
|
||||
return { action: "update", leave_requests: updated };
|
||||
}
|
||||
|
||||
for (const { iso_date, date, existing } of entries) {
|
||||
const previous_status = existing.approval_status;
|
||||
const fallbackRequested =
|
||||
existing.requested_hours !== null && existing.requested_hours !== undefined
|
||||
? Number(existing.requested_hours)
|
||||
: 8;
|
||||
const requested_hours = dto.requested_hours ?? fallbackRequested;
|
||||
|
||||
let payable: number;
|
||||
switch (type) {
|
||||
case LeaveTypes.HOLIDAY:
|
||||
payable = await this.holidayService.calculateHolidayPay(email, date, modifier);
|
||||
break;
|
||||
case LeaveTypes.VACATION: {
|
||||
const days_requested = requested_hours / 8;
|
||||
payable = await this.vacationLogic.calculateVacationPay(
|
||||
employee_id,
|
||||
date,
|
||||
Math.max(0, days_requested),
|
||||
modifier,
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
payable = existing.payable_hours !== null && existing.payable_hours !== undefined
|
||||
? Number(existing.payable_hours)
|
||||
: requested_hours;
|
||||
}
|
||||
|
||||
const row = await this.prisma.leaveRequests.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
requested_hours,
|
||||
comment: dto.comment ?? existing.comment,
|
||||
payable_hours: payable,
|
||||
bank_code_id: bank_code.id,
|
||||
approval_status: dto.approval_status ?? existing.approval_status,
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
|
||||
const was_approved = previous_status === LeaveApprovalStatus.APPROVED;
|
||||
const is_approved = row.approval_status === LeaveApprovalStatus.APPROVED;
|
||||
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
|
||||
|
||||
if (!was_approved && is_approved) {
|
||||
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
|
||||
} else if (was_approved && !is_approved) {
|
||||
await this.leaveUtils.removeShift(email, employee_id, iso_date, type);
|
||||
} else if (was_approved && is_approved) {
|
||||
await this.leaveUtils.syncShift(email, employee_id, iso_date, hours, type, row.comment);
|
||||
}
|
||||
updated.push({ ...mapRowToView(row), action: "update" });
|
||||
}
|
||||
return { action: "update", leave_requests: updated };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { CreateLeaveRequestsDto } from "../dtos/create-leave-request.dto";
|
||||
import { LeaveRequests, LeaveRequestsArchive } from "@prisma/client";
|
||||
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-request.dto";
|
||||
import { HolidayService } from "src/modules/business-logics/services/holiday.service";
|
||||
import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service";
|
||||
import { VacationService } from "src/modules/business-logics/services/vacation.service";
|
||||
import { SearchLeaveRequestsDto } from "../dtos/search-leave-request.dto";
|
||||
import { buildPrismaWhere } from "src/common/shared/build-prisma-where";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request.view.dto";
|
||||
import { LeaveRequestRow, leaveRequestsSelect } from "../utils/leave-requests.select";
|
||||
import { mapRowToView } from "../mappers/leave-requests.mapper";
|
||||
import { LeaveRequestsArchiveController } from "src/modules/archival/controllers/leave-requests-archive.controller";
|
||||
import { LeaveRequestArchiveRow, leaveRequestsArchiveSelect } from "../utils/leave-requests-archive.select";
|
||||
import { mapArchiveRowToView } from "../mappers/leave-requests-archive.mapper";
|
||||
import { mapArchiveRowToViewWithDays, mapRowToViewWithDays } from "../utils/leave-request.transform";
|
||||
|
||||
@Injectable()
|
||||
export class LeaveRequestsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly holidayService: HolidayService,
|
||||
private readonly vacationService: VacationService,
|
||||
private readonly sickLeaveService: SickLeaveService
|
||||
) {}
|
||||
|
||||
//function to avoid using employee_id as identifier in the frontend.
|
||||
private async resolveEmployeeIdByEmail(email: string): Promise<number> {
|
||||
const employee = await this.prisma.employees.findFirst({
|
||||
where: { user: { email} },
|
||||
select: { id:true },
|
||||
});
|
||||
if(!employee) throw new NotFoundException(`Employee with email ${email} not found`);
|
||||
return employee.id;
|
||||
}
|
||||
|
||||
//create a leave-request without the use of employee_id
|
||||
async create(dto: CreateLeaveRequestsDto): Promise<LeaveRequestViewDto> {
|
||||
const employee_id = await this.resolveEmployeeIdByEmail(dto.email);
|
||||
const row: LeaveRequestRow = await this.prisma.leaveRequests.create({
|
||||
data: {
|
||||
employee_id,
|
||||
bank_code_id: dto.bank_code_id,
|
||||
leave_type: dto.leave_type,
|
||||
start_date_time: new Date(dto.start_date_time),
|
||||
end_date_time: dto.end_date_time ? new Date(dto.end_date_time) : null,
|
||||
comment: dto.comment,
|
||||
approval_status: dto.approval_status ?? undefined,
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
return mapRowToViewWithDays(row);
|
||||
}
|
||||
|
||||
//fetches all leave-requests using email
|
||||
async findAll(filters: SearchLeaveRequestsDto): Promise<LeaveRequestViewDto[]> {
|
||||
const {start_date, end_date,email, leave_type, approval_status, bank_code_id } = filters;
|
||||
const where: any = {};
|
||||
|
||||
if (start_date) where.start_date_time = { ...(where.start_date_time ?? {}), gte: new Date(start_date) };
|
||||
if (end_date) where.end_date_time = { ...(where.end_date_time ?? {}), lte: new Date(end_date) };
|
||||
if (email) where.employee = { user: { email } };
|
||||
if (leave_type) where.leave_type = leave_type;
|
||||
if (approval_status) where.approval_status = approval_status;
|
||||
if (typeof bank_code_id === 'number') where.bank_code_id = bank_code_id;
|
||||
|
||||
const rows= await this.prisma.leaveRequests.findMany({
|
||||
where,
|
||||
select: leaveRequestsSelect,
|
||||
orderBy: { start_date_time: 'desc' },
|
||||
});
|
||||
|
||||
return rows.map(mapRowToViewWithDays);
|
||||
}
|
||||
|
||||
//fetch 1 leave-request using email
|
||||
async findOne(id:number): Promise<LeaveRequestViewDto> {
|
||||
const row: LeaveRequestRow | null = await this.prisma.leaveRequests.findUnique({
|
||||
where: { id },
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
if(!row) throw new NotFoundException(`Leave Request #${id} not found`);
|
||||
return mapRowToViewWithDays(row);
|
||||
}
|
||||
|
||||
//updates 1 leave-request using email
|
||||
async update(id: number, dto: UpdateLeaveRequestsDto): Promise<LeaveRequestViewDto> {
|
||||
await this.findOne(id);
|
||||
const data: Record<string, any> = {};
|
||||
|
||||
if(dto.email !== undefined) data.employee_id = await this.resolveEmployeeIdByEmail(dto.email);
|
||||
if(dto.leave_type !== undefined) data.bank_code_id = dto.bank_code_id;
|
||||
if(dto.start_date_time !== undefined) data.start_date_time = new Date(dto.start_date_time);
|
||||
if(dto.end_date_time !== undefined) data.end_date_time = new Date(dto.end_date_time);
|
||||
if(dto.comment !== undefined) data.comment = dto.comment;
|
||||
if(dto.approval_status !== undefined) data.approval_status = dto.approval_status;
|
||||
|
||||
const row: LeaveRequestRow = await this.prisma.leaveRequests.update({
|
||||
where: { id },
|
||||
data,
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
return mapRowToViewWithDays(row);
|
||||
}
|
||||
|
||||
//removes 1 leave-request using email
|
||||
async remove(id:number): Promise<LeaveRequestViewDto> {
|
||||
await this.findOne(id);
|
||||
const row: LeaveRequestRow = await this.prisma.leaveRequests.delete({
|
||||
where: { id },
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
return mapRowToViewWithDays(row);
|
||||
}
|
||||
|
||||
//archivation functions ******************************************************
|
||||
|
||||
async archiveExpired(): Promise<void> {
|
||||
const now = new Date();
|
||||
|
||||
await this.prisma.$transaction(async transaction => {
|
||||
//fetches expired leave requests
|
||||
const expired = await transaction.leaveRequests.findMany({
|
||||
where: { end_date_time: { lt: now } },
|
||||
});
|
||||
if(expired.length === 0) {
|
||||
return;
|
||||
}
|
||||
//copy unto archive table
|
||||
await transaction.leaveRequestsArchive.createMany({
|
||||
data: expired.map(request => ({
|
||||
leave_request_id: request.id,
|
||||
employee_id: request.employee_id,
|
||||
leave_type: request.leave_type,
|
||||
start_date_time: request.start_date_time,
|
||||
end_date_time: request.end_date_time,
|
||||
comment: request.comment,
|
||||
approval_status: request.approval_status,
|
||||
})),
|
||||
});
|
||||
//delete from leave_requests table
|
||||
await transaction.leaveRequests.deleteMany({
|
||||
where: { id: { in: expired.map(request => request.id ) } },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//fetches all archived leave-requests
|
||||
async findAllArchived(): Promise<LeaveRequestsArchive[]> {
|
||||
return this.prisma.leaveRequestsArchive.findMany();
|
||||
}
|
||||
|
||||
//remove emp_id and use email
|
||||
//fetches an archived employee
|
||||
async findOneArchived(id: number): Promise<LeaveRequestViewDto> {
|
||||
const row: LeaveRequestArchiveRow | null = await this.prisma.leaveRequestsArchive.findUnique({
|
||||
where: { id },
|
||||
select: leaveRequestsArchiveSelect,
|
||||
});
|
||||
if(!row) throw new NotFoundException(`Archived Leave Request #${id} not found`);
|
||||
|
||||
const emp = await this.prisma.employees.findUnique({
|
||||
where: { id: row.employee_id },
|
||||
select: { user: {select: { email:true,
|
||||
first_name: true,
|
||||
last_name: true,
|
||||
}}},
|
||||
});
|
||||
const email = emp?.user.email ?? "";
|
||||
const full_name = emp ? `${emp.user.first_name} ${emp.user.last_name}` : "";
|
||||
|
||||
return mapArchiveRowToViewWithDays(row, email, full_name);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
import { leaveRequestsSelect } from "../utils/leave-requests.select";
|
||||
import { mapRowToView } from "../mappers/leave-requests.mapper";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { SickLeaveService } from "src/modules/business-logics/services/sick-leave.service";
|
||||
import { roundToQuarterHour } from "src/common/utils/date-utils";
|
||||
import { LeaveRequestsUtils, normalizeDates, toDateOnly } from "../utils/leave-request.util";
|
||||
|
||||
@Injectable()
|
||||
export class SickLeaveRequestsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly sickService: SickLeaveService,
|
||||
private readonly leaveUtils: LeaveRequestsUtils,
|
||||
) {}
|
||||
|
||||
async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
|
||||
const email = dto.email.trim();
|
||||
const employee_id = await this.leaveUtils.resolveEmployeeIdByEmail(email);
|
||||
const bank_code = await this.leaveUtils.resolveBankCodeByType(LeaveTypes.SICK);
|
||||
if(!bank_code) throw new NotFoundException(`bank_code not found`);
|
||||
|
||||
const modifier = bank_code.modifier ?? 1;
|
||||
const dates = normalizeDates(dto.dates);
|
||||
if (!dates.length) throw new BadRequestException("Dates array must not be empty");
|
||||
const requested_hours_per_day = dto.requested_hours ?? 8;
|
||||
|
||||
const entries = dates.map((iso) => ({ iso, date: toDateOnly(iso) }));
|
||||
const reference_date = entries.reduce(
|
||||
(latest, entry) => (entry.date > latest ? entry.date : latest),
|
||||
entries[0].date,
|
||||
);
|
||||
const total_payable_hours = await this.sickService.calculateSickLeavePay(
|
||||
employee_id,
|
||||
reference_date,
|
||||
entries.length,
|
||||
requested_hours_per_day,
|
||||
modifier,
|
||||
);
|
||||
let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours));
|
||||
const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier);
|
||||
|
||||
const created: LeaveRequestViewDto[] = [];
|
||||
|
||||
for (const { iso, date } of entries) {
|
||||
const existing = await this.prisma.leaveRequests.findUnique({
|
||||
where: {
|
||||
leave_per_employee_date: {
|
||||
employee_id: employee_id,
|
||||
leave_type: LeaveTypes.SICK,
|
||||
date,
|
||||
},
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing) {
|
||||
throw new BadRequestException(`Sick request already exists for ${iso}`);
|
||||
}
|
||||
|
||||
const payable = Math.min(remaining_payable_hours, daily_payable_cap);
|
||||
const payable_rounded = roundToQuarterHour(Math.max(0, payable));
|
||||
remaining_payable_hours = roundToQuarterHour(
|
||||
Math.max(0, remaining_payable_hours - payable_rounded),
|
||||
);
|
||||
|
||||
const row = await this.prisma.leaveRequests.create({
|
||||
data: {
|
||||
employee_id: employee_id,
|
||||
bank_code_id: bank_code.id,
|
||||
leave_type: LeaveTypes.SICK,
|
||||
comment: dto.comment ?? "",
|
||||
requested_hours: requested_hours_per_day,
|
||||
payable_hours: payable_rounded,
|
||||
approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING,
|
||||
date,
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
|
||||
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
|
||||
if (row.approval_status === LeaveApprovalStatus.APPROVED) {
|
||||
await this.leaveUtils.syncShift(email, employee_id, iso, hours,LeaveTypes.SICK, row.comment);
|
||||
}
|
||||
|
||||
created.push({ ...mapRowToView(row), action: "create" });
|
||||
}
|
||||
|
||||
return { action: "create", leave_requests: created };
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
|
||||
import { UpsertLeaveRequestDto, UpsertResult } from "../dtos/upsert-leave-request.dto";
|
||||
import { LeaveRequestViewDto } from "../dtos/leave-request-view.dto";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||
import { VacationService } from "src/modules/business-logics/services/vacation.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { mapRowToView } from "../mappers/leave-requests.mapper";
|
||||
import { leaveRequestsSelect } from "../utils/leave-requests.select";
|
||||
import { roundToQuarterHour } from "src/common/utils/date-utils";
|
||||
import { LeaveRequestsUtils, normalizeDates, toDateOnly } from "../utils/leave-request.util";
|
||||
|
||||
@Injectable()
|
||||
export class VacationLeaveRequestsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly vacationService: VacationService,
|
||||
private readonly leaveUtils: LeaveRequestsUtils,
|
||||
) {}
|
||||
|
||||
async create(dto: UpsertLeaveRequestDto): Promise<UpsertResult> {
|
||||
const email = dto.email.trim();
|
||||
const employee_id = await this.leaveUtils.resolveEmployeeIdByEmail(email);
|
||||
const bank_code = await this.leaveUtils.resolveBankCodeByType(LeaveTypes.VACATION);
|
||||
if(!bank_code) throw new NotFoundException(`bank_code not found`);
|
||||
|
||||
const modifier = bank_code.modifier ?? 1;
|
||||
const dates = normalizeDates(dto.dates);
|
||||
const requested_hours_per_day = dto.requested_hours ?? 8;
|
||||
if (!dates.length) throw new BadRequestException("Dates array must not be empty");
|
||||
|
||||
const entries = dates
|
||||
.map((iso) => ({ iso, date: toDateOnly(iso) }))
|
||||
.sort((a, b) => a.date.getTime() - b.date.getTime());
|
||||
const start_date = entries[0].date;
|
||||
const total_payable_hours = await this.vacationService.calculateVacationPay(
|
||||
employee_id,
|
||||
start_date,
|
||||
entries.length,
|
||||
modifier,
|
||||
);
|
||||
let remaining_payable_hours = roundToQuarterHour(Math.max(0, total_payable_hours));
|
||||
const daily_payable_cap = roundToQuarterHour(requested_hours_per_day * modifier);
|
||||
|
||||
const created: LeaveRequestViewDto[] = [];
|
||||
|
||||
for (const { iso, date } of entries) {
|
||||
const existing = await this.prisma.leaveRequests.findUnique({
|
||||
where: {
|
||||
leave_per_employee_date: {
|
||||
employee_id: employee_id,
|
||||
leave_type: LeaveTypes.VACATION,
|
||||
date,
|
||||
},
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing) throw new BadRequestException(`Vacation request already exists for ${iso}`);
|
||||
|
||||
const payable = Math.min(remaining_payable_hours, daily_payable_cap);
|
||||
const payable_rounded = roundToQuarterHour(Math.max(0, payable));
|
||||
remaining_payable_hours = roundToQuarterHour(
|
||||
Math.max(0, remaining_payable_hours - payable_rounded),
|
||||
);
|
||||
|
||||
const row = await this.prisma.leaveRequests.create({
|
||||
data: {
|
||||
employee_id: employee_id,
|
||||
bank_code_id: bank_code.id,
|
||||
payable_hours: payable_rounded,
|
||||
requested_hours: requested_hours_per_day,
|
||||
leave_type: LeaveTypes.VACATION,
|
||||
comment: dto.comment ?? "",
|
||||
approval_status: dto.approval_status ?? LeaveApprovalStatus.PENDING,
|
||||
date,
|
||||
},
|
||||
select: leaveRequestsSelect,
|
||||
});
|
||||
|
||||
const hours = Number(row.payable_hours ?? row.requested_hours ?? 0);
|
||||
if (row.approval_status === LeaveApprovalStatus.APPROVED) {
|
||||
await this.leaveUtils.syncShift(email, employee_id, iso, hours, LeaveTypes.VACATION, row.comment);
|
||||
}
|
||||
created.push({ ...mapRowToView(row), action: "create" });
|
||||
}
|
||||
return { action: "create", leave_requests: created };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +1,19 @@
|
|||
import { LeaveRequestViewDto } from "../dtos/leave-request.view.dto";
|
||||
import { mapArchiveRowToView } from "../mappers/leave-requests-archive.mapper";
|
||||
import { mapRowToView } from "../mappers/leave-requests.mapper";
|
||||
import { LeaveRequestArchiveRow } from "./leave-requests-archive.select";
|
||||
import { LeaveRequestRow } from "./leave-requests.select";
|
||||
import { LeaveRequestViewDto } from '../dtos/leave-request-view.dto';
|
||||
import { mapArchiveRowToView } from '../mappers/leave-requests-archive.mapper';
|
||||
import { mapRowToView } from '../mappers/leave-requests.mapper';
|
||||
import { LeaveRequestArchiveRow } from './leave-requests-archive.select';
|
||||
import { LeaveRequestRow } from './leave-requests.select';
|
||||
|
||||
function toUTCDateOnly(date: Date): Date {
|
||||
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
||||
}
|
||||
|
||||
const MS_PER_DAY = 86_400_000;
|
||||
function computeDaysRequested(start_date: Date, end_date?: Date | null): number {
|
||||
const start = toUTCDateOnly(start_date);
|
||||
const end = toUTCDateOnly(end_date ?? start_date);
|
||||
const diff = Math.floor((end.getTime() - start.getTime()) / MS_PER_DAY) + 1;
|
||||
return Math.max(1, diff);
|
||||
}
|
||||
|
||||
/** Active (table leave_requests) : map + days_requested */
|
||||
/** Active (table leave_requests) : proxy to base mapper */
|
||||
export function mapRowToViewWithDays(row: LeaveRequestRow): LeaveRequestViewDto {
|
||||
const view = mapRowToView(row);
|
||||
view.days_requested = computeDaysRequested(row.start_date_time, row.end_date_time);
|
||||
return view;
|
||||
return mapRowToView(row);
|
||||
}
|
||||
|
||||
/** Archive (table leave_requests_archive) : map + days_requested */
|
||||
export function mapArchiveRowToViewWithDays(row: LeaveRequestArchiveRow, email: string, employee_full_name?: string):
|
||||
LeaveRequestViewDto {
|
||||
const view = mapArchiveRowToView(row, email, employee_full_name!);
|
||||
view.days_requested = computeDaysRequested(row.start_date_time, row.end_date_time);
|
||||
return view;
|
||||
/** Archive (table leave_requests_archive) : proxy to base mapper */
|
||||
export function mapArchiveRowToViewWithDays(
|
||||
row: LeaveRequestArchiveRow,
|
||||
email: string,
|
||||
employee_full_name?: string,
|
||||
): LeaveRequestViewDto {
|
||||
return mapArchiveRowToView(row, email, employee_full_name!);
|
||||
}
|
||||
124
src/modules/leave-requests/utils/leave-request.util.ts
Normal file
124
src/modules/leave-requests/utils/leave-request.util.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { LeaveTypes } from "@prisma/client";
|
||||
import { ShiftsCommandService } from "src/modules/shifts/services/shifts-command.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class LeaveRequestsUtils {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly shiftsCommand: ShiftsCommandService,
|
||||
){}
|
||||
|
||||
async resolveEmployeeIdByEmail(email: string): Promise<number> {
|
||||
const employee = await this.prisma.employees.findFirst({
|
||||
where: { user: { email } },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!employee) {
|
||||
throw new NotFoundException(`Employee with email ${email} not found`);
|
||||
}
|
||||
return employee.id;
|
||||
}
|
||||
|
||||
async resolveBankCodeByType(type: LeaveTypes) {
|
||||
const bankCode = await this.prisma.bankCodes.findFirst({
|
||||
where: { type },
|
||||
select: { id: true, bank_code: true, modifier: true },
|
||||
});
|
||||
if (!bankCode) throw new BadRequestException(`Bank code type "${type}" not found`);
|
||||
return bankCode;
|
||||
}
|
||||
|
||||
async syncShift(
|
||||
email: string,
|
||||
employee_id: number,
|
||||
iso_date: string,
|
||||
hours: number,
|
||||
type: LeaveTypes,
|
||||
comment?: string,
|
||||
) {
|
||||
if (hours <= 0) return;
|
||||
|
||||
const duration_minutes = Math.round(hours * 60);
|
||||
if (duration_minutes > 8 * 60) {
|
||||
throw new BadRequestException("Amount of hours cannot exceed 8 hours per day.");
|
||||
}
|
||||
|
||||
const start_minutes = 8 * 60;
|
||||
const end_minutes = start_minutes + duration_minutes;
|
||||
const toHHmm = (total: number) =>
|
||||
`${String(Math.floor(total / 60)).padStart(2, "0")}:${String(total % 60).padStart(2, "0")}`;
|
||||
|
||||
const existing = await this.prisma.shifts.findFirst({
|
||||
where: {
|
||||
date: new Date(iso_date),
|
||||
bank_code: { type },
|
||||
timesheet: { employee_id: employee_id },
|
||||
},
|
||||
include: { bank_code: true },
|
||||
});
|
||||
|
||||
await this.shiftsCommand.upsertShiftsByDate(email, iso_date, {
|
||||
old_shift: existing
|
||||
? {
|
||||
start_time: existing.start_time.toISOString().slice(11, 16),
|
||||
end_time: existing.end_time.toISOString().slice(11, 16),
|
||||
type: existing.bank_code?.type ?? type,
|
||||
is_remote: existing.is_remote,
|
||||
comment: existing.comment ?? undefined,
|
||||
}
|
||||
: undefined,
|
||||
new_shift: {
|
||||
start_time: toHHmm(start_minutes),
|
||||
end_time: toHHmm(end_minutes),
|
||||
is_remote: existing?.is_remote ?? false,
|
||||
comment: comment ?? existing?.comment ?? "",
|
||||
type: type,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async removeShift(
|
||||
email: string,
|
||||
employee_id: number,
|
||||
iso_date: string,
|
||||
type: LeaveTypes,
|
||||
) {
|
||||
const existing = await this.prisma.shifts.findFirst({
|
||||
where: {
|
||||
date: new Date(iso_date),
|
||||
bank_code: { type },
|
||||
timesheet: { employee_id: employee_id },
|
||||
},
|
||||
include: { bank_code: true },
|
||||
});
|
||||
if (!existing) return;
|
||||
|
||||
await this.shiftsCommand.upsertShiftsByDate(email, iso_date, {
|
||||
old_shift: {
|
||||
start_time: existing.start_time.toISOString().slice(11, 16),
|
||||
end_time: existing.end_time.toISOString().slice(11, 16),
|
||||
type: existing.bank_code?.type ?? type,
|
||||
is_remote: existing.is_remote,
|
||||
comment: existing.comment ?? undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const toDateOnly = (iso: string): Date => {
|
||||
const date = new Date(iso);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
throw new BadRequestException(`Invalid date: ${iso}`);
|
||||
}
|
||||
date.setHours(0, 0, 0, 0);
|
||||
return date;
|
||||
};
|
||||
|
||||
export const toISODateKey = (date: Date): string => date.toISOString().slice(0, 10);
|
||||
|
||||
export const normalizeDates = (dates: string[]): string[] =>
|
||||
Array.from(new Set(dates.map((iso) => toISODateKey(toDateOnly(iso)))));
|
||||
|
|
@ -6,11 +6,11 @@ export const leaveRequestsArchiveSelect = {
|
|||
archived_at: true,
|
||||
employee_id: true,
|
||||
leave_type: true,
|
||||
start_date_time: true,
|
||||
end_date_time: true,
|
||||
date: true,
|
||||
payable_hours: true,
|
||||
requested_hours: true,
|
||||
comment: true,
|
||||
approval_status: true,
|
||||
|
||||
} satisfies Prisma.LeaveRequestsArchiveSelect;
|
||||
|
||||
export type LeaveRequestArchiveRow = Prisma.LeaveRequestsArchiveGetPayload<{ select: typeof leaveRequestsArchiveSelect}>;
|
||||
|
|
@ -5,8 +5,9 @@ export const leaveRequestsSelect = {
|
|||
id: true,
|
||||
bank_code_id: true,
|
||||
leave_type: true,
|
||||
start_date_time: true,
|
||||
end_date_time: true,
|
||||
date: true,
|
||||
payable_hours: true,
|
||||
requested_hours: true,
|
||||
comment: true,
|
||||
approval_status: true,
|
||||
employee: { select: {
|
||||
|
|
|
|||
|
|
@ -18,13 +18,6 @@ export class PayPeriodsController {
|
|||
private readonly commandService: PayPeriodsCommandService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Find all pay period' })
|
||||
@ApiResponse({status: 200,description: 'List of pay period found', type: PayPeriodDto, isArray: true })
|
||||
async findAll(): Promise<PayPeriodDto[]> {
|
||||
return this.queryService.findAll();
|
||||
}
|
||||
|
||||
@Get('current-and-all')
|
||||
@ApiOperation({summary: 'Return current pay period and the full list'})
|
||||
@ApiQuery({name: 'date', required:false, example: '2025-08-11', description:'Override for resolving the current period'})
|
||||
|
|
@ -95,4 +88,16 @@ export class PayPeriodsController {
|
|||
): Promise<PayPeriodOverviewDto> {
|
||||
return this.queryService.getOverviewByYearPeriod(year, period_no);
|
||||
}
|
||||
|
||||
|
||||
//_____________________________________________________________________________________________
|
||||
// Deprecated or unused methods
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
// @Get()
|
||||
// @ApiOperation({ summary: 'Find all pay period' })
|
||||
// @ApiResponse({status: 200,description: 'List of pay period found', type: PayPeriodDto, isArray: true })
|
||||
// async findAll(): Promise<PayPeriodDto[]> {
|
||||
// return this.queryService.findAll();
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import { TimesheetsModule } from "../timesheets/timesheets.module";
|
|||
import { TimesheetsCommandService } from "../timesheets/services/timesheets-command.service";
|
||||
import { ExpensesCommandService } from "../expenses/services/expenses-command.service";
|
||||
import { ShiftsCommandService } from "../shifts/services/shifts-command.service";
|
||||
import { BankCodesRepo } from "../expenses/repos/bank-codes.repo";
|
||||
import { EmployeesRepo } from "../expenses/repos/employee.repo";
|
||||
import { TimesheetsRepo } from "../expenses/repos/timesheets.repo";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, TimesheetsModule],
|
||||
|
|
@ -16,12 +19,14 @@ import { ShiftsCommandService } from "../shifts/services/shifts-command.service"
|
|||
TimesheetsCommandService,
|
||||
ExpensesCommandService,
|
||||
ShiftsCommandService,
|
||||
BankCodesRepo,
|
||||
TimesheetsRepo,
|
||||
EmployeesRepo,
|
||||
],
|
||||
controllers: [PayPeriodsController],
|
||||
exports: [
|
||||
PayPeriodsQueryService,
|
||||
PayPeriodsCommandService,
|
||||
PayPeriodsQueryService,
|
||||
]
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import { Body, Controller, Param, Patch } from "@nestjs/common";
|
||||
import { PreferencesService } from "../services/preferences.service";
|
||||
import { PreferencesDto } from "../dtos/preferences.dto";
|
||||
|
||||
@Controller('preferences')
|
||||
export class PreferencesController {
|
||||
constructor(private readonly service: PreferencesService){}
|
||||
|
||||
@Patch(':email')
|
||||
async updatePreferences(@Param('email') email: string, @Body()payload: PreferencesDto) {
|
||||
return this.service.updatePreferences(email, payload);
|
||||
}
|
||||
|
||||
}
|
||||
16
src/modules/preferences/dtos/preferences.dto.ts
Normal file
16
src/modules/preferences/dtos/preferences.dto.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { IsBoolean, IsEmail, IsString } from "class-validator";
|
||||
|
||||
export class PreferencesDto {
|
||||
|
||||
@IsBoolean()
|
||||
notifications: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
dark_mode: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
lang_switch: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
lefty_mode: boolean;
|
||||
}
|
||||
11
src/modules/preferences/preferences.module.ts
Normal file
11
src/modules/preferences/preferences.module.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { PreferencesController } from "./controllers/preferences.controller";
|
||||
import { PreferencesService } from "./services/preferences.service";
|
||||
|
||||
@Module({
|
||||
controllers: [ PreferencesController ],
|
||||
providers: [ PreferencesService ],
|
||||
exports: [ PreferencesService ],
|
||||
})
|
||||
|
||||
export class PreferencesModule {}
|
||||
32
src/modules/preferences/services/preferences.service.ts
Normal file
32
src/modules/preferences/services/preferences.service.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Preferences } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { PreferencesDto } from "../dtos/preferences.dto";
|
||||
|
||||
@Injectable()
|
||||
export class PreferencesService {
|
||||
constructor(private readonly prisma: PrismaService){}
|
||||
|
||||
async resolveUserIdWithEmail(email: string): Promise<string> {
|
||||
const user = await this.prisma.users.findFirst({
|
||||
where: { email },
|
||||
select: { id: true },
|
||||
});
|
||||
if(!user) throw new NotFoundException(`User with email ${ email } not found`);
|
||||
return user.id;
|
||||
}
|
||||
|
||||
async updatePreferences(email: string, dto: PreferencesDto ): Promise<Preferences> {
|
||||
const user_id = await this.resolveUserIdWithEmail(email);
|
||||
return this.prisma.preferences.update({
|
||||
where: { user_id },
|
||||
data: {
|
||||
notifications: dto.notifications,
|
||||
dark_mode: dto.dark_mode,
|
||||
lang_switch: dto.lang_switch,
|
||||
lefty_mode: dto.lefty_mode,
|
||||
},
|
||||
include: { user: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ export class ShiftsController {
|
|||
constructor(
|
||||
private readonly shiftsService: ShiftsQueryService,
|
||||
private readonly shiftsCommandService: ShiftsCommandService,
|
||||
private readonly shiftsValidationService: ShiftsQueryService,
|
||||
){}
|
||||
|
||||
@Put('upsert/:email/:date')
|
||||
|
|
@ -28,53 +27,7 @@ export class ShiftsController {
|
|||
@Param('date') date_param: string,
|
||||
@Body() payload: UpsertShiftDto,
|
||||
) {
|
||||
return this.shiftsCommandService.upsertShfitsByDate(email_param, date_param, payload);
|
||||
}
|
||||
|
||||
@Post()
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Create shift' })
|
||||
@ApiResponse({ status: 201, description: 'Shift created',type: CreateShiftDto })
|
||||
@ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
||||
create(@Body() dto: CreateShiftDto): Promise<Shifts> {
|
||||
return this.shiftsService.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Find all shifts' })
|
||||
@ApiResponse({ status: 201, description: 'List of shifts found',type: CreateShiftDto, isArray: true })
|
||||
@ApiResponse({ status: 400, description: 'List of shifts not found' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
findAll(@Query() filters: SearchShiftsDto) {
|
||||
return this.shiftsService.findAll(filters);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Find shift' })
|
||||
@ApiResponse({ status: 201, description: 'Shift found',type: CreateShiftDto })
|
||||
@ApiResponse({ status: 400, description: 'Shift not found' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number): Promise<Shifts> {
|
||||
return this.shiftsService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Update shift' })
|
||||
@ApiResponse({ status: 201, description: 'Shift updated',type: CreateShiftDto })
|
||||
@ApiResponse({ status: 400, description: 'Shift not found' })
|
||||
update(@Param('id', ParseIntPipe) id: number,@Body() dto: UpdateShiftsDto): Promise<Shifts> {
|
||||
return this.shiftsService.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Delete shift' })
|
||||
@ApiResponse({ status: 201, description: 'Shift deleted',type: CreateShiftDto })
|
||||
@ApiResponse({ status: 400, description: 'Shift not found' })
|
||||
remove(@Param('id', ParseIntPipe) id: number): Promise<Shifts> {
|
||||
return this.shiftsService.remove(id);
|
||||
return this.shiftsCommandService.upsertShiftsByDate(email_param, date_param, payload);
|
||||
}
|
||||
|
||||
@Patch('approval/:id')
|
||||
|
|
@ -85,15 +38,14 @@ export class ShiftsController {
|
|||
|
||||
@Get('summary')
|
||||
async getSummary( @Query() query: GetShiftsOverviewDto): Promise<OverviewRow[]> {
|
||||
return this.shiftsValidationService.getSummary(query.period_id);
|
||||
return this.shiftsService.getSummary(query.period_id);
|
||||
}
|
||||
|
||||
@Get('export.csv')
|
||||
@Header('Content-Type', 'text/csv; charset=utf-8')
|
||||
@Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"')
|
||||
async exportCsv(@Query() query: GetShiftsOverviewDto): Promise<Buffer>{
|
||||
const rows = await this.shiftsValidationService.getSummary(query.period_id);
|
||||
|
||||
const rows = await this.shiftsService.getSummary(query.period_id);
|
||||
//CSV Headers
|
||||
const header = [
|
||||
'full_name',
|
||||
|
|
@ -125,4 +77,54 @@ export class ShiftsController {
|
|||
return Buffer.from('\uFEFF' + header + body, 'utf8');
|
||||
}
|
||||
|
||||
//_____________________________________________________________________________________________
|
||||
// Deprecated or unused methods
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
// @Post()
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Create shift' })
|
||||
// @ApiResponse({ status: 201, description: 'Shift created',type: CreateShiftDto })
|
||||
// @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
||||
// create(@Body() dto: CreateShiftDto): Promise<Shifts> {
|
||||
// return this.shiftsService.create(dto);
|
||||
// }
|
||||
|
||||
// @Get()
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Find all shifts' })
|
||||
// @ApiResponse({ status: 201, description: 'List of shifts found',type: CreateShiftDto, isArray: true })
|
||||
// @ApiResponse({ status: 400, description: 'List of shifts not found' })
|
||||
// @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
// findAll(@Query() filters: SearchShiftsDto) {
|
||||
// return this.shiftsService.findAll(filters);
|
||||
// }
|
||||
|
||||
// @Get(':id')
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Find shift' })
|
||||
// @ApiResponse({ status: 201, description: 'Shift found',type: CreateShiftDto })
|
||||
// @ApiResponse({ status: 400, description: 'Shift not found' })
|
||||
// findOne(@Param('id', ParseIntPipe) id: number): Promise<Shifts> {
|
||||
// return this.shiftsService.findOne(id);
|
||||
// }
|
||||
|
||||
// @Patch(':id')
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Update shift' })
|
||||
// @ApiResponse({ status: 201, description: 'Shift updated',type: CreateShiftDto })
|
||||
// @ApiResponse({ status: 400, description: 'Shift not found' })
|
||||
// update(@Param('id', ParseIntPipe) id: number,@Body() dto: UpdateShiftsDto): Promise<Shifts> {
|
||||
// return this.shiftsService.update(id, dto);
|
||||
// }
|
||||
|
||||
// @Delete(':id')
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Delete shift' })
|
||||
// @ApiResponse({ status: 201, description: 'Shift deleted',type: CreateShiftDto })
|
||||
// @ApiResponse({ status: 400, description: 'Shift not found' })
|
||||
// remove(@Param('id', ParseIntPipe) id: number): Promise<Shifts> {
|
||||
// return this.shiftsService.remove(id);
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Type } from "class-transformer";
|
||||
import { IsBoolean, IsOptional, IsString, Matches, MaxLength, ValidateNested } from "class-validator";
|
||||
|
||||
export const COMMENT_MAX_LENGTH = 512;
|
||||
export const COMMENT_MAX_LENGTH = 280;
|
||||
|
||||
export class ShiftPayloadDto {
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
|||
constructor(prisma: PrismaService) { super(prisma); }
|
||||
|
||||
//create/update/delete master method
|
||||
async upsertShfitsByDate(email:string, date_string: string, dto: UpsertShiftDto):
|
||||
async upsertShiftsByDate(email:string, date_string: string, dto: UpsertShiftDto):
|
||||
Promise<{ action: UpsertAction; day: DayShiftResponse[] }> {
|
||||
const { old_shift, new_shift } = dto;
|
||||
|
||||
|
|
|
|||
|
|
@ -99,10 +99,10 @@ export class ShiftsQueryService {
|
|||
data: {
|
||||
...(timesheet_id !== undefined && { timesheet_id }),
|
||||
...(bank_code_id !== undefined && { bank_code_id }),
|
||||
...(date !== undefined && { date }),
|
||||
...(start_time !== undefined && { start_time }),
|
||||
...(end_time !== undefined && { end_time }),
|
||||
...(comment !== undefined && { comment }),
|
||||
...(date !== undefined && { date }),
|
||||
...(start_time !== undefined && { start_time }),
|
||||
...(end_time !== undefined && { end_time }),
|
||||
...(comment !== undefined && { comment }),
|
||||
},
|
||||
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
||||
bank_code: true,
|
||||
|
|
@ -115,7 +115,7 @@ export class ShiftsQueryService {
|
|||
return this.prisma.shifts.delete({ where: { id } });
|
||||
}
|
||||
|
||||
async getSummary(period_id: number): Promise<OverviewRow[]> {
|
||||
async getSummary(period_id: number): Promise<OverviewRow[]> {
|
||||
//fetch pay-period to display
|
||||
const period = await this.prisma.payPeriods.findFirst({
|
||||
where: { pay_period_no: period_id },
|
||||
|
|
|
|||
|
|
@ -50,27 +50,34 @@ export class TimesheetsController {
|
|||
}
|
||||
|
||||
|
||||
@Get(':id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Find timesheet' })
|
||||
@ApiResponse({ status: 201, description: 'Timesheet found', type: CreateTimesheetDto })
|
||||
@ApiResponse({ status: 400, description: 'Timesheet not found' })
|
||||
findOne(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> {
|
||||
return this.timesheetsQuery.findOne(id);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
// @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
@ApiOperation({ summary: 'Delete timesheet' })
|
||||
@ApiResponse({ status: 201, description: 'Timesheet deleted', type: CreateTimesheetDto })
|
||||
@ApiResponse({ status: 400, description: 'Timesheet not found' })
|
||||
remove(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> {
|
||||
return this.timesheetsQuery.remove(id);
|
||||
}
|
||||
|
||||
@Patch('approval/:id')
|
||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) {
|
||||
return this.timesheetsCommand.updateApproval(id, isApproved);
|
||||
}
|
||||
//_____________________________________________________________________________________________
|
||||
// Deprecated or unused methods
|
||||
//_____________________________________________________________________________________________
|
||||
|
||||
// @Patch('approval/:id')
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) {
|
||||
// return this.timesheetsCommand.updateApproval(id, isApproved);
|
||||
// }
|
||||
|
||||
// @Get(':id')
|
||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Find timesheet' })
|
||||
// @ApiResponse({ status: 201, description: 'Timesheet found', type: CreateTimesheetDto })
|
||||
// @ApiResponse({ status: 400, description: 'Timesheet not found' })
|
||||
// findOne(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> {
|
||||
// return this.timesheetsQuery.findOne(id);
|
||||
// }
|
||||
|
||||
// @Delete(':id')
|
||||
// // @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||
// @ApiOperation({ summary: 'Delete timesheet' })
|
||||
// @ApiResponse({ status: 201, description: 'Timesheet deleted', type: CreateTimesheetDto })
|
||||
// @ApiResponse({ status: 400, description: 'Timesheet not found' })
|
||||
// remove(@Param('id', ParseIntPipe) id: number): Promise<Timesheets> {
|
||||
// return this.timesheetsQuery.remove(id);
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Timesheets, TimesheetsArchive } from '@prisma/client';
|
||||
import { OvertimeService } from 'src/modules/business-logics/services/overtime.service';
|
||||
import { computeHours, formatDateISO, getWeekEnd, getWeekStart } from 'src/common/utils/date-utils';
|
||||
import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { buildPeriod, endOfDayUTC, toUTCDateOnly } from '../utils/timesheet.helpers';
|
||||
import { computeHours, formatDateISO, getWeekEnd, getWeekStart } from 'src/common/utils/date-utils';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { OvertimeService } from 'src/modules/business-logics/services/overtime.service';
|
||||
import { TimesheetDto } from '../dtos/overview-timesheet.dto';
|
||||
import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
||||
import type { ShiftRow, ExpenseRow } from '../utils/timesheet.helpers';
|
||||
import { TimesheetDto } from '../dtos/overview-timesheet.dto';
|
||||
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -174,14 +174,14 @@ export class TimesheetsQueryService {
|
|||
const to_HH_mm = (date: Date) => date.toISOString().slice(11, 16);
|
||||
|
||||
//maps all shifts of selected timesheet
|
||||
const shifts = timesheet.shift.map((sft) => ({
|
||||
bank_type: sft.bank_code?.type ?? '',
|
||||
date: formatDateISO(sft.date),
|
||||
start_time: to_HH_mm(sft.start_time),
|
||||
end_time: to_HH_mm(sft.end_time),
|
||||
comment: sft.comment ?? '',
|
||||
is_approved: sft.is_approved ?? false,
|
||||
is_remote: sft.is_remote ?? false,
|
||||
const shifts = timesheet.shift.map((shift_row) => ({
|
||||
bank_type: shift_row.bank_code?.type ?? '',
|
||||
date: formatDateISO(shift_row.date),
|
||||
start_time: to_HH_mm(shift_row.start_time),
|
||||
end_time: to_HH_mm(shift_row.end_time),
|
||||
comment: shift_row.comment ?? '',
|
||||
is_approved: shift_row.is_approved ?? false,
|
||||
is_remote: shift_row.is_remote ?? false,
|
||||
}));
|
||||
|
||||
//maps all expenses of selected timsheet
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import { BusinessLogicsModule } from 'src/modules/business-logics/business-logic
|
|||
import { TimesheetsCommandService } from './services/timesheets-command.service';
|
||||
import { ShiftsCommandService } from '../shifts/services/shifts-command.service';
|
||||
import { ExpensesCommandService } from '../expenses/services/expenses-command.service';
|
||||
import { BankCodesRepo } from '../expenses/repos/bank-codes.repo';
|
||||
import { TimesheetsRepo } from '../expenses/repos/timesheets.repo';
|
||||
import { EmployeesRepo } from '../expenses/repos/employee.repo';
|
||||
|
||||
@Module({
|
||||
imports: [BusinessLogicsModule],
|
||||
|
|
@ -13,7 +16,10 @@ import { ExpensesCommandService } from '../expenses/services/expenses-command.se
|
|||
TimesheetsQueryService,
|
||||
TimesheetsCommandService,
|
||||
ShiftsCommandService,
|
||||
ExpensesCommandService
|
||||
ExpensesCommandService,
|
||||
BankCodesRepo,
|
||||
TimesheetsRepo,
|
||||
EmployeesRepo,
|
||||
],
|
||||
exports: [TimesheetsQueryService],
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user