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",
|
"globals": "^16.0.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prisma": "^6.14.0",
|
"prisma": "^6.16.3",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
|
|
@ -3148,9 +3148,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/config": {
|
"node_modules/@prisma/config": {
|
||||||
"version": "6.14.0",
|
"version": "6.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.3.tgz",
|
||||||
"integrity": "sha512-IwC7o5KNNGhmblLs23swnfBjADkacBb7wvyDXUWLwuvUQciKJZqyecU0jw0d7JRkswrj+XTL8fdr0y2/VerKQQ==",
|
"integrity": "sha512-VlsLnG4oOuKGGMToEeVaRhoTBZu5H3q51jTQXb/diRags3WV0+BQK5MolJTtP6G7COlzoXmWeS11rNBtvg+qFQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"c12": "3.1.0",
|
"c12": "3.1.0",
|
||||||
|
|
@ -3160,48 +3160,48 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/debug": {
|
"node_modules/@prisma/debug": {
|
||||||
"version": "6.14.0",
|
"version": "6.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.3.tgz",
|
||||||
"integrity": "sha512-j4Lf+y+5QIJgQD4sJWSbkOD7geKx9CakaLp/TyTy/UDu9Wo0awvWCBH/BAxTHUaCpIl9USA5VS/KJhDqKJSwug==",
|
"integrity": "sha512-89DdqWtdKd7qoc9/qJCKLTazj3W3zPEiz0hc7HfZdpjzm21c7orOUB5oHWJsG+4KbV4cWU5pefq3CuDVYF9vgA==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "6.14.0",
|
"version": "6.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.3.tgz",
|
||||||
"integrity": "sha512-LhJjqsALFEcoAtF07nSaOkVguaxw/ZsgfROIYZ8bAZDobe7y8Wy+PkYQaPOK1iLSsFgV2MhCO/eNrI1gdSOj6w==",
|
"integrity": "sha512-b+Rl4nzQDcoqe6RIpSHv8f5lLnwdDGvXhHjGDiokObguAAv/O1KaX1Oc69mBW/GFWKQpCkOraobLjU6s1h8HGg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.14.0",
|
"@prisma/debug": "6.16.3",
|
||||||
"@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
|
"@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
||||||
"@prisma/fetch-engine": "6.14.0",
|
"@prisma/fetch-engine": "6.16.3",
|
||||||
"@prisma/get-platform": "6.14.0"
|
"@prisma/get-platform": "6.16.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines-version": {
|
"node_modules/@prisma/engines-version": {
|
||||||
"version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
|
"version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a.tgz",
|
||||||
"integrity": "sha512-EgN9ODJpiX45yvwcngoStp3uQPJ3l+AEVoQ6dMMO2QvmwIlnxfApzKmJQExzdo7/hqQANrz5txHJdGYHzOnGHA==",
|
"integrity": "sha512-fftRmosBex48Ph1v2ll1FrPpirwtPZpNkE5CDCY1Lw2SD2ctyrLlVlHiuxDAAlALwWBOkPbAll4+EaqdGuMhJw==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine": {
|
"node_modules/@prisma/fetch-engine": {
|
||||||
"version": "6.14.0",
|
"version": "6.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.3.tgz",
|
||||||
"integrity": "sha512-MPzYPOKMENYOaY3AcAbaKrfvXVlvTc6iHmTXsp9RiwCX+bPyfDMqMFVUSVXPYrXnrvEzhGHfyiFy0PRLHPysNg==",
|
"integrity": "sha512-bUoRIkVaI+CCaVGrSfcKev0/Mk4ateubqWqGZvQ9uCqFv2ENwWIR3OeNuGin96nZn5+SkebcD7RGgKr/+mJelw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.14.0",
|
"@prisma/debug": "6.16.3",
|
||||||
"@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49",
|
"@prisma/engines-version": "6.16.1-1.bb420e667c1820a8c05a38023385f6cc7ef8e83a",
|
||||||
"@prisma/get-platform": "6.14.0"
|
"@prisma/get-platform": "6.16.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/get-platform": {
|
"node_modules/@prisma/get-platform": {
|
||||||
"version": "6.14.0",
|
"version": "6.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.3.tgz",
|
||||||
"integrity": "sha512-7VjuxKNwjnBhKfqPpMeWiHEa2sVjYzmHdl1slW6STuUCe9QnOY0OY1ljGSvz6wpG4U8DfbDqkG1yofd/1GINww==",
|
"integrity": "sha512-X1LxiFXinJ4iQehrodGp0f66Dv6cDL0GbRlcCoLtSu6f4Wi+hgo7eND/afIs5029GQLgNWKZ46vn8hjyXTsHLA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.14.0"
|
"@prisma/debug": "6.16.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@scarf/scarf": {
|
"node_modules/@scarf/scarf": {
|
||||||
|
|
@ -9450,15 +9450,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nypm": {
|
"node_modules/nypm": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz",
|
||||||
"integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==",
|
"integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"citty": "^0.1.6",
|
"citty": "^0.1.6",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"pkg-types": "^2.2.0",
|
"pkg-types": "^2.3.0",
|
||||||
"tinyexec": "^1.0.1"
|
"tinyexec": "^1.0.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -9967,9 +9967,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pkg-types": {
|
"node_modules/pkg-types": {
|
||||||
"version": "2.2.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||||
"integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==",
|
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"confbox": "^0.2.2",
|
"confbox": "^0.2.2",
|
||||||
|
|
@ -10049,14 +10049,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prisma": {
|
"node_modules/prisma": {
|
||||||
"version": "6.14.0",
|
"version": "6.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.3.tgz",
|
||||||
"integrity": "sha512-QEuCwxu+Uq9BffFw7in8In+WfbSUN0ewnaSUKloLkbJd42w6EyFckux4M0f7VwwHlM3A8ssaz4OyniCXlsn0WA==",
|
"integrity": "sha512-4tJq3KB9WRshH5+QmzOLV54YMkNlKOtLKaSdvraI5kC/axF47HuOw6zDM8xrxJ6s9o2WodY654On4XKkrobQdQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/config": "6.14.0",
|
"@prisma/config": "6.16.3",
|
||||||
"@prisma/engines": "6.14.0"
|
"@prisma/engines": "6.16.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"prisma": "build/index.js"
|
"prisma": "build/index.js"
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prisma": "^6.14.0",
|
"prisma": "^6.16.3",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"ts-jest": "^29.2.5",
|
"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';
|
import { PrismaClient, Prisma, LeaveTypes, LeaveApprovalStatus } from '@prisma/client';
|
||||||
|
|
||||||
if (process.env.SKIP_LEAVE_REQUESTS === 'true') {
|
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);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
function dateOn(y: number, m: number, d: number) {
|
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));
|
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 employees = await prisma.employees.findMany({ select: { id: true } });
|
||||||
const bankCodes = await prisma.bankCodes.findMany({
|
const bankCodes = await prisma.bankCodes.findMany({
|
||||||
where: { categorie: 'LEAVE' },
|
where: { categorie: 'LEAVE' },
|
||||||
select: { id: true },
|
select: { id: true, type: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!employees.length || !bankCodes.length) {
|
if (!employees.length || !bankCodes.length) {
|
||||||
|
|
@ -44,30 +44,31 @@ async function main() {
|
||||||
LeaveApprovalStatus.ESCALATED,
|
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[] = [];
|
const rows: Prisma.LeaveRequestsCreateManyInput[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
const emp = employees[i % employees.length];
|
const emp = employees[i % employees.length];
|
||||||
const m = futureMonths[i % futureMonths.length];
|
const m = futureMonths[i % futureMonths.length];
|
||||||
const start = dateOn(year, m, 5 + i); // 5..14
|
const date = dateOn(year, m, 5 + i); // 5..14
|
||||||
if (start <= today) continue; // garantir "futur"
|
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 type = types[i % types.length];
|
||||||
const status = statuses[i % statuses.length];
|
const status = statuses[i % statuses.length];
|
||||||
const bc = bankCodes[i % bankCodes.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({
|
rows.push({
|
||||||
employee_id: emp.id,
|
employee_id: emp.id,
|
||||||
bank_code_id: bc.id,
|
bank_code_id: bc.id,
|
||||||
leave_type: type,
|
leave_type: type,
|
||||||
start_date_time: start,
|
date,
|
||||||
end_date_time: end, // ok: Date | null
|
comment: `Future leave #${i + 1} (${bc.type})`,
|
||||||
comment: `Future leave #${i + 1}`,
|
|
||||||
approval_status: status,
|
approval_status: status,
|
||||||
|
requested_hours: requestedHours,
|
||||||
|
payable_hours: payableHours,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,7 +76,7 @@ async function main() {
|
||||||
await prisma.leaveRequests.createMany({ data: rows });
|
await prisma.leaveRequests.createMany({ data: rows });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✓ LeaveRequests (future): ${rows.length} rows`);
|
console.log(`? LeaveRequests (future): ${rows.length} rows`);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().finally(() => prisma.$disconnect());
|
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') {
|
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);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -15,65 +15,73 @@ function daysAgo(n: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// 1) Récupère tous les employés
|
|
||||||
const employees = await prisma.employees.findMany({ select: { id: true } });
|
const employees = await prisma.employees.findMany({ select: { id: true } });
|
||||||
if (!employees.length) {
|
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({
|
const leaveCodes = await prisma.bankCodes.findMany({
|
||||||
where: { type: { in: ['SICK', 'VACATION'] } },
|
where: { type: { in: ['SICK', 'VACATION', 'HOLIDAY'] } },
|
||||||
select: { id: true, type: true, bank_code: true },
|
select: { id: true, type: true },
|
||||||
});
|
});
|
||||||
if (!leaveCodes.length) {
|
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 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;
|
const COUNT = 12;
|
||||||
for (let i = 0; i < COUNT; i++) {
|
for (let i = 0; i < COUNT; i++) {
|
||||||
const emp = employees[i % employees.length];
|
const emp = employees[i % employees.length];
|
||||||
const leaveCode = leaveCodes[Math.floor(Math.random() * leaveCodes.length)];
|
const leaveCode = leaveCodes[Math.floor(Math.random() * leaveCodes.length)];
|
||||||
|
|
||||||
const start = daysAgo(120 - i * 3);
|
const date = daysAgo(120 - i * 3);
|
||||||
const end = Math.random() < 0.6 ? daysAgo(119 - i * 3) : null;
|
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({
|
const lr = await prisma.leaveRequests.create({
|
||||||
data: {
|
data: {
|
||||||
employee_id: emp.id,
|
employee_id: emp.id,
|
||||||
bank_code_id: leaveCode.id,
|
bank_code_id: leaveCode.id,
|
||||||
// on stocke le "type" tel qu’il est défini dans bank_codes
|
leave_type: leaveCode.type as LeaveTypes,
|
||||||
leave_type: leaveCode.type as any,
|
date,
|
||||||
start_date_time: start,
|
|
||||||
end_date_time: end,
|
|
||||||
comment: `Past leave #${i + 1} (${leaveCode.type})`,
|
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) {
|
for (const lr of created) {
|
||||||
await prisma.leaveRequestsArchive.create({
|
await prisma.leaveRequestsArchive.create({
|
||||||
data: {
|
data: {
|
||||||
leave_request_id: lr.id,
|
leave_request_id: lr.id,
|
||||||
employee_id: lr.employee_id,
|
employee_id: lr.employee_id,
|
||||||
leave_type: lr.leave_type,
|
leave_type: lr.leave_type,
|
||||||
start_date_time: lr.start_date_time,
|
date: lr.date,
|
||||||
end_date_time: lr.end_date_time,
|
|
||||||
comment: lr.comment,
|
comment: lr.comment,
|
||||||
approval_status: lr.approval_status,
|
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());
|
main().finally(() => prisma.$disconnect());
|
||||||
|
|
@ -143,7 +143,7 @@ async function main() {
|
||||||
bank_code_id,
|
bank_code_id,
|
||||||
date,
|
date,
|
||||||
amount, // string "xx.yy" (2 décimales exactes)
|
amount, // string "xx.yy" (2 décimales exactes)
|
||||||
attachement: null,
|
attachment: null,
|
||||||
comment: `Expense ${code} ${amount}$ (emp ${e.id})`,
|
comment: `Expense ${code} ${amount}$ (emp ${e.id})`,
|
||||||
is_approved: Math.random() < 0.65,
|
is_approved: Math.random() < 0.65,
|
||||||
supervisor_comment: Math.random() < 0.25 ? 'OK' : null,
|
supervisor_comment: Math.random() < 0.25 ? 'OK' : null,
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ async function main() {
|
||||||
bank_code_id: bc.id,
|
bank_code_id: bc.id,
|
||||||
date: daysAgo(60 + i),
|
date: daysAgo(60 + i),
|
||||||
amount: (20 + i * 3.5).toFixed(2), // ok: Decimal accepte string
|
amount: (20 + i * 3.5).toFixed(2), // ok: Decimal accepte string
|
||||||
attachement: null,
|
attachment: null,
|
||||||
comment: `Old expense #${i + 1}`,
|
comment: `Old expense #${i + 1}`,
|
||||||
is_approved: true,
|
is_approved: true,
|
||||||
supervisor_comment: null,
|
supervisor_comment: null,
|
||||||
|
|
@ -50,7 +50,7 @@ async function main() {
|
||||||
bank_code_id: e.bank_code_id,
|
bank_code_id: e.bank_code_id,
|
||||||
date: e.date,
|
date: e.date,
|
||||||
amount: e.amount,
|
amount: e.amount,
|
||||||
attachement: e.attachement,
|
attachment: e.attachment,
|
||||||
comment: e.comment,
|
comment: e.comment,
|
||||||
is_approved: e.is_approved,
|
is_approved: e.is_approved,
|
||||||
supervisor_comment: e.supervisor_comment,
|
supervisor_comment: e.supervisor_comment,
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ model Users {
|
||||||
oauth_sessions OAuthSessions[] @relation("UserOAuthSessions")
|
oauth_sessions OAuthSessions[] @relation("UserOAuthSessions")
|
||||||
employees_archive EmployeesArchive[] @relation("UsersToEmployeesToArchive")
|
employees_archive EmployeesArchive[] @relation("UsersToEmployeesToArchive")
|
||||||
customer_archive CustomersArchive[] @relation("UserToCustomersToArchive")
|
customer_archive CustomersArchive[] @relation("UserToCustomersToArchive")
|
||||||
|
preferences Preferences? @relation("UserPreferences")
|
||||||
@@map("users")
|
@@map("users")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,16 +105,19 @@ model LeaveRequests {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
employee Employees @relation("LeaveRequestEmployee", fields: [employee_id], references: [id])
|
employee Employees @relation("LeaveRequestEmployee", fields: [employee_id], references: [id])
|
||||||
employee_id Int
|
employee_id Int
|
||||||
bank_code BankCodes? @relation("LeaveRequestBankCodes", fields: [bank_code_id], references: [id])
|
bank_code BankCodes @relation("LeaveRequestBankCodes", fields: [bank_code_id], references: [id])
|
||||||
bank_code_id Int
|
bank_code_id Int
|
||||||
leave_type LeaveTypes
|
leave_type LeaveTypes
|
||||||
start_date_time DateTime @db.Date
|
date DateTime @db.Date
|
||||||
end_date_time DateTime? @db.Date
|
payable_hours Decimal? @db.Decimal(5,2)
|
||||||
|
requested_hours Decimal? @db.Decimal(5,2)
|
||||||
comment String
|
comment String
|
||||||
approval_status LeaveApprovalStatus @default(PENDING)
|
approval_status LeaveApprovalStatus @default(PENDING)
|
||||||
|
|
||||||
archive LeaveRequestsArchive[] @relation("LeaveRequestToArchive")
|
archive LeaveRequestsArchive[] @relation("LeaveRequestToArchive")
|
||||||
|
|
||||||
|
@@unique([employee_id, leave_type, date], name: "leave_per_employee_date")
|
||||||
|
@@index([employee_id, date])
|
||||||
@@map("leave_requests")
|
@@map("leave_requests")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,11 +128,14 @@ model LeaveRequestsArchive {
|
||||||
archived_at DateTime @default(now())
|
archived_at DateTime @default(now())
|
||||||
employee_id Int
|
employee_id Int
|
||||||
leave_type LeaveTypes
|
leave_type LeaveTypes
|
||||||
start_date_time DateTime @db.Date
|
date DateTime @db.Date
|
||||||
end_date_time DateTime? @db.Date
|
payable_hours Decimal? @db.Decimal(5,2)
|
||||||
|
requested_hours Decimal? @db.Decimal(5,2)
|
||||||
comment String
|
comment String
|
||||||
approval_status LeaveApprovalStatus
|
approval_status LeaveApprovalStatus
|
||||||
|
|
||||||
|
@@unique([leave_request_id])
|
||||||
|
@@index([employee_id, date])
|
||||||
@@map("leave_requests_archive")
|
@@map("leave_requests_archive")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,8 +232,10 @@ model Expenses {
|
||||||
bank_code_id Int
|
bank_code_id Int
|
||||||
date DateTime @db.Date
|
date DateTime @db.Date
|
||||||
amount Decimal @db.Money
|
amount Decimal @db.Money
|
||||||
attachement String?
|
mileage Decimal?
|
||||||
comment String?
|
attachment Int?
|
||||||
|
attachment_record Attachments? @relation("ExpenseAttachment", fields: [attachment], references: [id], onDelete: SetNull)
|
||||||
|
comment String
|
||||||
is_approved Boolean @default(false)
|
is_approved Boolean @default(false)
|
||||||
supervisor_comment String?
|
supervisor_comment String?
|
||||||
|
|
||||||
|
|
@ -244,8 +252,10 @@ model ExpensesArchive {
|
||||||
archived_at DateTime @default(now())
|
archived_at DateTime @default(now())
|
||||||
bank_code_id Int
|
bank_code_id Int
|
||||||
date DateTime @db.Date
|
date DateTime @db.Date
|
||||||
amount Decimal @db.Money
|
amount Decimal? @db.Money
|
||||||
attachement String?
|
mileage Decimal?
|
||||||
|
attachment Int?
|
||||||
|
attachment_record Attachments? @relation("ExpenseArchiveAttachment", fields: [attachment], references: [id], onDelete: SetNull)
|
||||||
comment String?
|
comment String?
|
||||||
is_approved Boolean
|
is_approved Boolean
|
||||||
supervisor_comment String?
|
supervisor_comment String?
|
||||||
|
|
@ -296,11 +306,27 @@ model Attachments {
|
||||||
created_by String
|
created_by String
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
|
|
||||||
|
expenses Expenses[] @relation("ExpenseAttachment")
|
||||||
|
expenses_archive ExpensesArchive[] @relation("ExpenseArchiveAttachment")
|
||||||
|
|
||||||
@@index([owner_type, owner_id, created_at])
|
@@index([owner_type, owner_id, created_at])
|
||||||
@@index([sha256])
|
@@index([sha256])
|
||||||
@@map("attachments")
|
@@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 {
|
enum AttachmentStatus {
|
||||||
ACTIVE
|
ACTIVE
|
||||||
DELETED
|
DELETED
|
||||||
|
|
@ -333,6 +359,7 @@ enum LeaveTypes {
|
||||||
PARENTAL // maternite/paternite/adoption
|
PARENTAL // maternite/paternite/adoption
|
||||||
LEGAL // obligations legales comme devoir de juree
|
LEGAL // obligations legales comme devoir de juree
|
||||||
WEDDING // mariage
|
WEDDING // mariage
|
||||||
|
HOLIDAY // férier
|
||||||
|
|
||||||
@@map("leave_types")
|
@@map("leave_types")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import { EmployeesModule } from "../employees/employees.module";
|
||||||
LeaveRequestsArchiveController,
|
LeaveRequestsArchiveController,
|
||||||
ShiftsArchiveController,
|
ShiftsArchiveController,
|
||||||
TimesheetsArchiveController,
|
TimesheetsArchiveController,
|
||||||
]
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
export class ArchivalModule {}
|
export class ArchivalModule {}
|
||||||
|
|
@ -1,33 +1,7 @@
|
||||||
import { Get, Param, ParseIntPipe, NotFoundException, Controller, UseGuards } from "@nestjs/common";
|
import { Controller } from '@nestjs/common';
|
||||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
import { 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";
|
|
||||||
|
|
||||||
@ApiTags('LeaveRequests Archives')
|
@ApiTags('LeaveRequests Archives')
|
||||||
// @UseGuards()
|
// @UseGuards()
|
||||||
@Controller('archives/leaveRequests')
|
@Controller('archives/leaveRequests')
|
||||||
export class LeaveRequestsArchiveController {
|
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`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
import { Cron } from "@nestjs/schedule";
|
import { Cron } from "@nestjs/schedule";
|
||||||
import { ExpensesQueryService } from "src/modules/expenses/services/expenses-query.service";
|
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 { ShiftsQueryService } from "src/modules/shifts/services/shifts-query.service";
|
||||||
import { TimesheetsQueryService } from "src/modules/timesheets/services/timesheets-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 timesheetsService: TimesheetsQueryService,
|
||||||
private readonly expensesService: ExpensesQueryService,
|
private readonly expensesService: ExpensesQueryService,
|
||||||
private readonly shiftsService: ShiftsQueryService,
|
private readonly shiftsService: ShiftsQueryService,
|
||||||
private readonly leaveRequestsService: LeaveRequestsService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Cron('0 0 3 * * 1', {timeZone:'America/Toronto'}) // chaque premier lundi du mois à 03h00
|
@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.timesheetsService.archiveOld();
|
||||||
await this.expensesService.archiveOld();
|
await this.expensesService.archiveOld();
|
||||||
await this.shiftsService.archiveOld();
|
await this.shiftsService.archiveOld();
|
||||||
await this.leaveRequestsService.archiveExpired();
|
// await this.leaveRequestsService.archiveExpired();
|
||||||
this.logger.log('archivation process done');
|
this.logger.log('archivation process done');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error('an error occured during archivation process ', err);
|
this.logger.error('an error occured during archivation process ', err);
|
||||||
|
|
|
||||||
|
|
@ -7,40 +7,43 @@ import { ApiBadRequestResponse, ApiNotFoundResponse, ApiOperation, ApiResponse }
|
||||||
@Controller('bank-codes')
|
@Controller('bank-codes')
|
||||||
export class BankCodesControllers {
|
export class BankCodesControllers {
|
||||||
constructor(private readonly bankCodesService: BankCodesService) {}
|
constructor(private readonly bankCodesService: BankCodesService) {}
|
||||||
|
//_____________________________________________________________________________________________
|
||||||
|
// Deprecated or unused methods
|
||||||
|
//_____________________________________________________________________________________________
|
||||||
|
|
||||||
@Post()
|
// @Post()
|
||||||
@ApiOperation({ summary: 'Create a new bank code' })
|
// @ApiOperation({ summary: 'Create a new bank code' })
|
||||||
@ApiResponse({ status: 201, description: 'Bank code successfully created.' })
|
// @ApiResponse({ status: 201, description: 'Bank code successfully created.' })
|
||||||
@ApiBadRequestResponse({ description: 'Invalid input data.' })
|
// @ApiBadRequestResponse({ description: 'Invalid input data.' })
|
||||||
create(@Body() dto: CreateBankCodeDto) {
|
// create(@Body() dto: CreateBankCodeDto) {
|
||||||
return this.bankCodesService.create(dto);
|
// return this.bankCodesService.create(dto);
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Get()
|
// @Get()
|
||||||
@ApiOperation({ summary: 'Retrieve all bank codes' })
|
// @ApiOperation({ summary: 'Retrieve all bank codes' })
|
||||||
@ApiResponse({ status: 200, description: 'List of bank codes.' })
|
// @ApiResponse({ status: 200, description: 'List of bank codes.' })
|
||||||
findAll() {
|
// findAll() {
|
||||||
return this.bankCodesService.findAll();
|
// return this.bankCodesService.findAll();
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Get(':id')
|
// @Get(':id')
|
||||||
@ApiOperation({ summary: 'Retrieve a bank code by its ID' })
|
// @ApiOperation({ summary: 'Retrieve a bank code by its ID' })
|
||||||
@ApiNotFoundResponse({ description: 'Bank code not found.' })
|
// @ApiNotFoundResponse({ description: 'Bank code not found.' })
|
||||||
findOne(@Param('id', ParseIntPipe) id: number){
|
// findOne(@Param('id', ParseIntPipe) id: number){
|
||||||
return this.bankCodesService.findOne(id);
|
// return this.bankCodesService.findOne(id);
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Patch(':id')
|
// @Patch(':id')
|
||||||
@ApiOperation({ summary: 'Update an existing bank code' })
|
// @ApiOperation({ summary: 'Update an existing bank code' })
|
||||||
@ApiNotFoundResponse({ description: 'Bank code not found.' })
|
// @ApiNotFoundResponse({ description: 'Bank code not found.' })
|
||||||
update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateBankCodeDto) {
|
// update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateBankCodeDto) {
|
||||||
return this.bankCodesService.update(id, dto)
|
// return this.bankCodesService.update(id, dto)
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Delete(':id')
|
// @Delete(':id')
|
||||||
@ApiOperation({ summary: 'Delete a bank code' })
|
// @ApiOperation({ summary: 'Delete a bank code' })
|
||||||
@ApiNotFoundResponse({ description: 'Bank code not found.' })
|
// @ApiNotFoundResponse({ description: 'Bank code not found.' })
|
||||||
remove(@Param('id', ParseIntPipe) id: number) {
|
// remove(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.bankCodesService.remove(id);
|
// return this.bankCodesService.remove(id);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||||
import { PrismaService } from "../../../prisma/prisma.service";
|
|
||||||
import { computeHours, getWeekStart } from "src/common/utils/date-utils";
|
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()
|
@Injectable()
|
||||||
export class HolidayService {
|
export class HolidayService {
|
||||||
|
|
@ -22,36 +30,49 @@ export class HolidayService {
|
||||||
|
|
||||||
private async computeHoursPrevious4WeeksByEmail(email: string, holiday_date: Date): Promise<number> {
|
private async computeHoursPrevious4WeeksByEmail(email: string, holiday_date: Date): Promise<number> {
|
||||||
const employee_id = await this.resolveEmployeeByEmail(email);
|
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> {
|
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 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);
|
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'];
|
const valid_codes = ['G1', 'G43', 'G56', 'G104', 'G105', 'G700'];
|
||||||
//fetches all shift of the employee in said window ( 4 previous completed weeks )
|
|
||||||
const shifts = await this.prisma.shifts.findMany({
|
const shifts = await this.prisma.shifts.findMany({
|
||||||
where: { timesheet: { employee_id: employee_id } ,
|
where: {
|
||||||
|
timesheet: { employee_id: employee_id },
|
||||||
date: { gte: window_start, lte: window_end },
|
date: { gte: window_start, lte: window_end },
|
||||||
bank_code: { bank_code: { in: valid_codes } },
|
bank_code: { bank_code: { in: valid_codes } },
|
||||||
},
|
},
|
||||||
select: { date: true, start_time: true, end_time: true },
|
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 hours_by_week = new Map<number, number>();
|
||||||
const daily_hours = total_hours / 20;
|
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> {
|
async calculateHolidayPay( email: string, holiday_date: Date, modifier: number): Promise<number> {
|
||||||
const hours = await this.computeHoursPrevious4WeeksByEmail(email, holiday_date);
|
const average_daily_hours = await this.computeHoursPrevious4WeeksByEmail(email, holiday_date);
|
||||||
const daily_rate = Math.min(hours, 8);
|
const daily_rate = Math.min(average_daily_hours, 8);
|
||||||
this.logger.debug(`Holiday pay calculation: hours= ${hours.toFixed(2)}`);
|
this.logger.debug(`Holiday pay calculation: cappedHoursPerDay= ${average_daily_hours.toFixed(2)}, appliedDailyRate= ${daily_rate.toFixed(2)}`);
|
||||||
return daily_rate * modifier;
|
return daily_rate * modifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
import { getYearStart, roundToQuarterHour } from "src/common/utils/date-utils";
|
||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
import { PrismaService } from "../../../prisma/prisma.service";
|
import { PrismaService } from "../../../prisma/prisma.service";
|
||||||
import { getYearStart, roundToQuarterHour } from "src/common/utils/date-utils";
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SickLeaveService {
|
export class SickLeaveService {
|
||||||
|
|
@ -9,8 +9,17 @@ export class SickLeaveService {
|
||||||
private readonly logger = new Logger(SickLeaveService.name);
|
private readonly logger = new Logger(SickLeaveService.name);
|
||||||
|
|
||||||
//switch employeeId for email
|
//switch employeeId for email
|
||||||
async calculateSickLeavePay(employee_id: number, reference_date: Date, days_requested: number, modifier: number):
|
async calculateSickLeavePay(
|
||||||
Promise<number> {
|
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
|
//sets the year to jan 1st to dec 31st
|
||||||
const period_start = getYearStart(reference_date);
|
const period_start = getYearStart(reference_date);
|
||||||
const period_end = reference_date;
|
const period_end = reference_date;
|
||||||
|
|
@ -19,18 +28,19 @@ export class SickLeaveService {
|
||||||
const shifts = await this.prisma.shifts.findMany({
|
const shifts = await this.prisma.shifts.findMany({
|
||||||
where: {
|
where: {
|
||||||
timesheet: { employee_id: employee_id },
|
timesheet: { employee_id: employee_id },
|
||||||
date: { gte: period_start, lte: period_end},
|
date: { gte: period_start, lte: period_end },
|
||||||
},
|
},
|
||||||
select: { date: true },
|
select: { date: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
//count the amount of worked days
|
//count the amount of worked days
|
||||||
const worked_dates = new Set(
|
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;
|
const days_worked = worked_dates.size;
|
||||||
this.logger.debug(`Sick leave: days worked= ${days_worked} in ${period_start.toDateString()}
|
this.logger.debug(
|
||||||
-> ${period_end.toDateString()}`);
|
`Sick leave: days worked= ${days_worked} in ${period_start.toDateString()} -> ${period_end.toDateString()}`,
|
||||||
|
);
|
||||||
|
|
||||||
//less than 30 worked days returns 0
|
//less than 30 worked days returns 0
|
||||||
if (days_worked < 30) {
|
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
|
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
|
//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);
|
const first_bonus_date = new Date(
|
||||||
let months = (period_end.getFullYear() - first_bonus_date.getFullYear()) * 12 +
|
threshold_date.getFullYear(),
|
||||||
(period_end.getMonth() - first_bonus_date.getMonth()) + 1;
|
threshold_date.getMonth() + 1,
|
||||||
if(months < 0) months = 0;
|
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;
|
acquired_days += months;
|
||||||
|
|
||||||
//cap of 10 days
|
//cap of 10 days
|
||||||
if (acquired_days > 10) acquired_days = 10;
|
if (acquired_days > 10) acquired_days = 10;
|
||||||
|
|
||||||
this.logger.debug(`Sick leave: threshold Date = ${threshold_date.toDateString()}
|
this.logger.debug(
|
||||||
, bonusMonths = ${months}, acquired Days = ${acquired_days}`);
|
`Sick leave: threshold Date = ${threshold_date.toDateString()}, bonusMonths = ${months}, acquired Days = ${acquired_days}`,
|
||||||
|
);
|
||||||
|
|
||||||
const payable_days = Math.min(acquired_days, days_requested);
|
const payable_days = Math.min(acquired_days, days_requested);
|
||||||
const raw_hours = payable_days * 8 * modifier;
|
const raw_hours = payable_days * hours_per_day * modifier;
|
||||||
const rounded = roundToQuarterHour(raw_hours)
|
const rounded = roundToQuarterHour(raw_hours);
|
||||||
this.logger.debug(`Sick leave pay: days= ${payable_days}, modifier= ${modifier}, hours= ${rounded}`);
|
this.logger.debug(
|
||||||
|
`Sick leave pay: days= ${payable_days}, hoursPerDay= ${hours_per_day}, modifier= ${modifier}, hours= ${rounded}`,
|
||||||
|
);
|
||||||
return rounded;
|
return rounded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,15 +6,7 @@ export class VacationService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
private readonly logger = new Logger(VacationService.name);
|
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> {
|
async calculateVacationPay(employee_id: number, start_date: Date, days_requested: number, modifier: number): Promise<number> {
|
||||||
//fetch hiring date
|
//fetch hiring date
|
||||||
|
|
|
||||||
|
|
@ -14,51 +14,55 @@ import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagg
|
||||||
export class CustomersController {
|
export class CustomersController {
|
||||||
constructor(private readonly customersService: CustomersService) {}
|
constructor(private readonly customersService: CustomersService) {}
|
||||||
|
|
||||||
@Post()
|
//_____________________________________________________________________________________________
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.SUPERVISOR)
|
// Deprecated or unused methods
|
||||||
@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()
|
// @Post()
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({ summary: 'Find all customers' })
|
// @ApiOperation({ summary: 'Create customer' })
|
||||||
@ApiResponse({ status: 201, description: 'List of customers found', type: CreateCustomerDto, isArray: true })
|
// @ApiResponse({ status: 201, description: 'Customer created', type: CreateCustomerDto })
|
||||||
@ApiResponse({ status: 400, description: 'List of customers not found' })
|
// @ApiResponse({ status: 400, description: 'Invalid task or invalid data' })
|
||||||
findAll(): Promise<Customers[]> {
|
// create(@Body() dto: CreateCustomerDto): Promise<Customers> {
|
||||||
return this.customersService.findAll();
|
// return this.customersService.create(dto);
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Get(':id')
|
// @Get()
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({ summary: 'Find customer' })
|
// @ApiOperation({ summary: 'Find all customers' })
|
||||||
@ApiResponse({ status: 201, description: 'Customer found', type: CreateCustomerDto })
|
// @ApiResponse({ status: 201, description: 'List of customers found', type: CreateCustomerDto, isArray: true })
|
||||||
@ApiResponse({ status: 400, description: 'Customer not found' })
|
// @ApiResponse({ status: 400, description: 'List of customers not found' })
|
||||||
findOne(@Param('id', ParseIntPipe) id: number): Promise<Customers> {
|
// findAll(): Promise<Customers[]> {
|
||||||
return this.customersService.findOne(id);
|
// return this.customersService.findAll();
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Patch(':id')
|
// @Get(':id')
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE,RoleEnum.SUPERVISOR)
|
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({ summary: 'Update customer' })
|
// @ApiOperation({ summary: 'Find customer' })
|
||||||
@ApiResponse({ status: 201, description: 'Customer updated', type: CreateCustomerDto })
|
// @ApiResponse({ status: 201, description: 'Customer found', type: CreateCustomerDto })
|
||||||
@ApiResponse({ status: 400, description: 'Customer not found' })
|
// @ApiResponse({ status: 400, description: 'Customer not found' })
|
||||||
update(
|
// findOne(@Param('id', ParseIntPipe) id: number): Promise<Customers> {
|
||||||
@Param('id', ParseIntPipe) id: number,
|
// return this.customersService.findOne(id);
|
||||||
@Body() dto: UpdateCustomerDto,
|
// }
|
||||||
): Promise<Customers> {
|
|
||||||
return this.customersService.update(id, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(':id')
|
// @Patch(':id')
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.SUPERVISOR)
|
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE,RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({ summary: 'Delete customer' })
|
// @ApiOperation({ summary: 'Update customer' })
|
||||||
@ApiResponse({ status: 201, description: 'Customer deleted', type: CreateCustomerDto })
|
// @ApiResponse({ status: 201, description: 'Customer updated', type: CreateCustomerDto })
|
||||||
@ApiResponse({ status: 400, description: 'Customer not found' })
|
// @ApiResponse({ status: 400, description: 'Customer not found' })
|
||||||
remove(@Param('id', ParseIntPipe) id: number): Promise<Customers>{
|
// update(
|
||||||
return this.customersService.remove(id);
|
// @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);
|
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')
|
@Get('employee-list')
|
||||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING)
|
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR, RoleEnum.ACCOUNTING)
|
||||||
@ApiOperation({summary: 'Find all employees with scoped info' })
|
@ApiOperation({summary: 'Find all employees with scoped info' })
|
||||||
|
|
@ -42,34 +33,6 @@ export class EmployeesController {
|
||||||
return this.employeesService.findListEmployees();
|
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')
|
@Patch(':email')
|
||||||
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
//@RolesAllowed(RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
@ApiBearerAuth('access-token')
|
@ApiBearerAuth('access-token')
|
||||||
|
|
@ -88,4 +51,47 @@ export class EmployeesController {
|
||||||
}
|
}
|
||||||
return result;
|
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 { ExpensesQueryService } from "../services/expenses-query.service";
|
||||||
import { CreateExpenseDto } from "../dtos/create-expense.dto";
|
import { CreateExpenseDto } from "../dtos/create-expense.dto";
|
||||||
import { Expenses } from "@prisma/client";
|
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 { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { ExpensesCommandService } from "../services/expenses-command.service";
|
import { ExpensesCommandService } from "../services/expenses-command.service";
|
||||||
import { SearchExpensesDto } from "../dtos/search-expense.dto";
|
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')
|
@ApiTags('Expenses')
|
||||||
@ApiBearerAuth('access-token')
|
@ApiBearerAuth('access-token')
|
||||||
|
|
@ -15,60 +17,73 @@ import { SearchExpensesDto } from "../dtos/search-expense.dto";
|
||||||
@Controller('Expenses')
|
@Controller('Expenses')
|
||||||
export class ExpensesController {
|
export class ExpensesController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly expensesService: ExpensesQueryService,
|
private readonly query: ExpensesQueryService,
|
||||||
private readonly expensesApprovalService: ExpensesCommandService,
|
private readonly command: ExpensesCommandService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post()
|
@Put('upsert/:email/:date')
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
async upsert_by_date(
|
||||||
@ApiOperation({ summary: 'Create expense' })
|
@Param('email') email: string,
|
||||||
@ApiResponse({ status: 201, description: 'Expense created',type: CreateExpenseDto })
|
@Param('date') date: string,
|
||||||
@ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
@Body() dto: UpsertExpenseDto,
|
||||||
create(@Body() dto: CreateExpenseDto): Promise<Expenses> {
|
): Promise<UpsertExpenseResult> {
|
||||||
return this.expensesService.create(dto);
|
return this.command.upsertExpensesByDate(email, date, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
//_____________________________________________________________________________________________
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
// Deprecated or unused methods
|
||||||
@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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':id')
|
// @Post()
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({ summary: 'Find expense' })
|
// @ApiOperation({ summary: 'Create expense' })
|
||||||
@ApiResponse({ status: 201, description: 'Expense found',type: CreateExpenseDto })
|
// @ApiResponse({ status: 201, description: 'Expense created',type: CreateExpenseDto })
|
||||||
@ApiResponse({ status: 400, description: 'Expense not found' })
|
// @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
||||||
findOne(@Param('id', ParseIntPipe) id: number): Promise <Expenses> {
|
// create(@Body() dto: CreateExpenseDto): Promise<Expenses> {
|
||||||
return this.expensesService.findOne(id);
|
// return this.query.create(dto);
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Patch(':id')
|
// @Get()
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({ summary: 'Expense shift' })
|
// @ApiOperation({ summary: 'Find all expenses' })
|
||||||
@ApiResponse({ status: 201, description: 'Expense updated',type: CreateExpenseDto })
|
// @ApiResponse({ status: 201, description: 'List of expenses found',type: CreateExpenseDto, isArray: true })
|
||||||
@ApiResponse({ status: 400, description: 'Expense not found' })
|
// @ApiResponse({ status: 400, description: 'List of expenses not found' })
|
||||||
update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateExpenseDto) {
|
// @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||||
return this.expensesService.update(id,dto);
|
// findAll(@Query() filters: SearchExpensesDto): Promise<Expenses[]> {
|
||||||
}
|
// return this.query.findAll(filters);
|
||||||
|
// }
|
||||||
|
|
||||||
@Delete(':id')
|
// @Get(':id')
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({ summary: 'Delete expense' })
|
// @ApiOperation({ summary: 'Find expense' })
|
||||||
@ApiResponse({ status: 201, description: 'Expense deleted',type: CreateExpenseDto })
|
// @ApiResponse({ status: 201, description: 'Expense found',type: CreateExpenseDto })
|
||||||
@ApiResponse({ status: 400, description: 'Expense not found' })
|
// @ApiResponse({ status: 400, description: 'Expense not found' })
|
||||||
remove(@Param('id', ParseIntPipe) id: number): Promise<Expenses> {
|
// findOne(@Param('id', ParseIntPipe) id: number): Promise <Expenses> {
|
||||||
return this.expensesService.remove(id);
|
// return this.query.findOne(id);
|
||||||
}
|
// }
|
||||||
|
|
||||||
@Patch('approval/:id')
|
// @Patch(':id')
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) {
|
// @ApiOperation({ summary: 'Expense shift' })
|
||||||
return this.expensesApprovalService.updateApproval(id, isApproved);
|
// @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'
|
description:'explain`s why the expense was made'
|
||||||
})
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
comment?: string;
|
comment: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: 'DENIED, APPROUVED, PENDING, etc...',
|
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 { ExpensesQueryService } from "./services/expenses-query.service";
|
||||||
import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module";
|
import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module";
|
||||||
import { ExpensesCommandService } from "./services/expenses-command.service";
|
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({
|
@Module({
|
||||||
imports: [BusinessLogicsModule],
|
imports: [BusinessLogicsModule],
|
||||||
controllers: [ExpensesController],
|
controllers: [ExpensesController],
|
||||||
providers: [ExpensesQueryService, ExpensesCommandService],
|
providers: [
|
||||||
exports: [ ExpensesQueryService ],
|
ExpensesQueryService,
|
||||||
|
ExpensesCommandService,
|
||||||
|
BankCodesRepo,
|
||||||
|
TimesheetsRepo,
|
||||||
|
EmployeesRepo,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
ExpensesQueryService,
|
||||||
|
BankCodesRepo,
|
||||||
|
TimesheetsRepo,
|
||||||
|
EmployeesRepo,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
export class ExpensesModule {}
|
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 { BaseApprovalService } from "src/common/shared/base-approval.service";
|
||||||
|
import { Expenses, Prisma } from "@prisma/client";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
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()
|
@Injectable()
|
||||||
export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
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() {
|
protected get delegate() {
|
||||||
return this.prisma.expenses;
|
return this.prisma.expenses;
|
||||||
|
|
@ -22,4 +41,273 @@ export class ExpensesCommandService extends BaseApprovalService<Expenses> {
|
||||||
this.updateApprovalWithTransaction(transaction, id, isApproved),
|
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,
|
bank_code_id: exp.bank_code_id,
|
||||||
date: exp.date,
|
date: exp.date,
|
||||||
amount: exp.amount,
|
amount: exp.amount,
|
||||||
attachement: exp.attachement,
|
attachment: exp.attachment,
|
||||||
comment: exp.comment,
|
comment: exp.comment,
|
||||||
is_approved: exp.is_approved,
|
is_approved: exp.is_approved,
|
||||||
supervisor_comment: exp.supervisor_comment,
|
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 { Body, Controller, Post } from "@nestjs/common";
|
||||||
import { LeaveRequestsService } from "../services/leave-requests.service";
|
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||||
import { CreateLeaveRequestsDto } from "../dtos/create-leave-request.dto";
|
import { LeaveRequestsService } from "../services/leave-request.service";
|
||||||
import { LeaveRequests } from "@prisma/client";
|
import { UpsertLeaveRequestDto } from "../dtos/upsert-leave-request.dto";
|
||||||
import { UpdateLeaveRequestsDto } from "../dtos/update-leave-request.dto";
|
import { LeaveTypes } from "@prisma/client";
|
||||||
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";
|
|
||||||
|
|
||||||
@ApiTags('Leave Requests')
|
@ApiTags('Leave Requests')
|
||||||
@ApiBearerAuth('access-token')
|
@ApiBearerAuth('access-token')
|
||||||
// @UseGuards()
|
// @UseGuards()
|
||||||
@Controller('leave-requests')
|
@Controller('leave-requests')
|
||||||
export class LeaveRequestController {
|
export class LeaveRequestController {
|
||||||
constructor(private readonly leaveRequetsService: LeaveRequestsService){}
|
constructor(private readonly leave_service: LeaveRequestsService){}
|
||||||
|
|
||||||
@Post()
|
@Post('upsert')
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
async upsertLeaveRequest(@Body() dto: UpsertLeaveRequestDto) {
|
||||||
@ApiOperation({summary: 'Create leave request' })
|
const { action, leave_requests } = await this.leave_service.handle(dto);
|
||||||
@ApiResponse({ status: 201, description: 'Leave request created',type: CreateLeaveRequestsDto })
|
return { action, leave_requests };
|
||||||
@ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
}q
|
||||||
create(@Body() dto: CreateLeaveRequestsDto): Promise<LeaveRequestViewDto> {
|
|
||||||
return this. leaveRequetsService.create(dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get()
|
//TODO:
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
/*
|
||||||
@ApiOperation({summary: 'Find all leave request' })
|
@Get('archive')
|
||||||
@ApiResponse({ status: 201, description: 'List of Leave requests found',type: LeaveRequestViewDto, isArray: true })
|
findAllArchived(){...}
|
||||||
@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);
|
|
||||||
}
|
|
||||||
|
|
||||||
//remove emp_id and use email
|
@Get('archive/:id')
|
||||||
@Delete(':id')
|
findOneArchived(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);
|
|
||||||
}
|
|
||||||
|
|
||||||
//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";
|
import { LeaveApprovalStatus, LeaveTypes } from "@prisma/client";
|
||||||
|
|
||||||
export class LeaveRequestViewDto {
|
export class LeaveRequestViewDto {
|
||||||
id!: number;
|
id: number;
|
||||||
leave_type!: LeaveTypes;
|
leave_type!: LeaveTypes;
|
||||||
start_date_time!: string;
|
date!: string;
|
||||||
end_date_time!: string | null;
|
comment!: string;
|
||||||
comment!: string | null;
|
|
||||||
approval_status: LeaveApprovalStatus;
|
approval_status: LeaveApprovalStatus;
|
||||||
email!: string;
|
email!: string;
|
||||||
employee_full_name: string;
|
employee_full_name!: string;
|
||||||
days_requested?: number;
|
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 { 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 { Module } from "@nestjs/common";
|
||||||
import { BusinessLogicsModule } from "src/modules/business-logics/business-logics.module";
|
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({
|
@Module({
|
||||||
imports: [BusinessLogicsModule],
|
imports: [BusinessLogicsModule, ShiftsModule],
|
||||||
controllers: [LeaveRequestController],
|
controllers: [LeaveRequestController],
|
||||||
providers: [LeaveRequestsService],
|
providers: [
|
||||||
exports: [LeaveRequestsService],
|
VacationLeaveRequestsService,
|
||||||
|
SickLeaveRequestsService,
|
||||||
|
HolidayLeaveRequestsService,
|
||||||
|
LeaveRequestsService,
|
||||||
|
PrismaService,
|
||||||
|
LeaveRequestsUtils,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
LeaveRequestsService,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
export class LeaveRequestsModule {}
|
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";
|
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 {
|
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 {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
leave_type: row.leave_type,
|
leave_type: row.leave_type,
|
||||||
start_date_time: toISO(row.start_date_time)!,
|
date: isoDate,
|
||||||
end_date_time: toISO(row.end_date_time),
|
payable_hours: toNum(row.payable_hours),
|
||||||
|
requested_hours: toNum(row.requested_hours),
|
||||||
comment: row.comment,
|
comment: row.comment,
|
||||||
approval_status: row.approval_status,
|
approval_status: row.approval_status,
|
||||||
email,
|
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";
|
import { LeaveRequestRow } from "../utils/leave-requests.select";
|
||||||
|
|
||||||
function toISODateString(date:Date | null): string | null {
|
const toNum = (value?: Prisma.Decimal | null) =>
|
||||||
return date ? date.toISOString().slice(0,10) : null;
|
value !== null && value !== undefined ? Number(value) : undefined;
|
||||||
}
|
|
||||||
|
|
||||||
export function mapRowToView(row: LeaveRequestRow): LeaveRequestViewDto {
|
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 {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
leave_type: row.leave_type,
|
leave_type: row.leave_type,
|
||||||
start_date_time: toISODateString(row.start_date_time)!,
|
date: iso_date,
|
||||||
end_date_time: toISODateString(row.end_date_time),
|
payable_hours: toNum(row.payable_hours),
|
||||||
|
requested_hours: toNum(row.requested_hours),
|
||||||
comment: row.comment,
|
comment: row.comment,
|
||||||
approval_status: row.approval_status,
|
approval_status: row.approval_status,
|
||||||
email: row.employee.user.email,
|
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 { LeaveRequestViewDto } from '../dtos/leave-request-view.dto';
|
||||||
import { mapArchiveRowToView } from "../mappers/leave-requests-archive.mapper";
|
import { mapArchiveRowToView } from '../mappers/leave-requests-archive.mapper';
|
||||||
import { mapRowToView } from "../mappers/leave-requests.mapper";
|
import { mapRowToView } from '../mappers/leave-requests.mapper';
|
||||||
import { LeaveRequestArchiveRow } from "./leave-requests-archive.select";
|
import { LeaveRequestArchiveRow } from './leave-requests-archive.select';
|
||||||
import { LeaveRequestRow } from "./leave-requests.select";
|
import { LeaveRequestRow } from './leave-requests.select';
|
||||||
|
|
||||||
function toUTCDateOnly(date: Date): Date {
|
/** Active (table leave_requests) : proxy to base mapper */
|
||||||
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 */
|
|
||||||
export function mapRowToViewWithDays(row: LeaveRequestRow): LeaveRequestViewDto {
|
export function mapRowToViewWithDays(row: LeaveRequestRow): LeaveRequestViewDto {
|
||||||
const view = mapRowToView(row);
|
return mapRowToView(row);
|
||||||
view.days_requested = computeDaysRequested(row.start_date_time, row.end_date_time);
|
|
||||||
return view;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Archive (table leave_requests_archive) : map + days_requested */
|
/** Archive (table leave_requests_archive) : proxy to base mapper */
|
||||||
export function mapArchiveRowToViewWithDays(row: LeaveRequestArchiveRow, email: string, employee_full_name?: string):
|
export function mapArchiveRowToViewWithDays(
|
||||||
LeaveRequestViewDto {
|
row: LeaveRequestArchiveRow,
|
||||||
const view = mapArchiveRowToView(row, email, employee_full_name!);
|
email: string,
|
||||||
view.days_requested = computeDaysRequested(row.start_date_time, row.end_date_time);
|
employee_full_name?: string,
|
||||||
return view;
|
): 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,
|
archived_at: true,
|
||||||
employee_id: true,
|
employee_id: true,
|
||||||
leave_type: true,
|
leave_type: true,
|
||||||
start_date_time: true,
|
date: true,
|
||||||
end_date_time: true,
|
payable_hours: true,
|
||||||
|
requested_hours: true,
|
||||||
comment: true,
|
comment: true,
|
||||||
approval_status: true,
|
approval_status: true,
|
||||||
|
|
||||||
} satisfies Prisma.LeaveRequestsArchiveSelect;
|
} satisfies Prisma.LeaveRequestsArchiveSelect;
|
||||||
|
|
||||||
export type LeaveRequestArchiveRow = Prisma.LeaveRequestsArchiveGetPayload<{ select: typeof leaveRequestsArchiveSelect}>;
|
export type LeaveRequestArchiveRow = Prisma.LeaveRequestsArchiveGetPayload<{ select: typeof leaveRequestsArchiveSelect}>;
|
||||||
|
|
@ -5,8 +5,9 @@ export const leaveRequestsSelect = {
|
||||||
id: true,
|
id: true,
|
||||||
bank_code_id: true,
|
bank_code_id: true,
|
||||||
leave_type: true,
|
leave_type: true,
|
||||||
start_date_time: true,
|
date: true,
|
||||||
end_date_time: true,
|
payable_hours: true,
|
||||||
|
requested_hours: true,
|
||||||
comment: true,
|
comment: true,
|
||||||
approval_status: true,
|
approval_status: true,
|
||||||
employee: { select: {
|
employee: { select: {
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,6 @@ export class PayPeriodsController {
|
||||||
private readonly commandService: PayPeriodsCommandService,
|
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')
|
@Get('current-and-all')
|
||||||
@ApiOperation({summary: 'Return current pay period and the full list'})
|
@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'})
|
@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> {
|
): Promise<PayPeriodOverviewDto> {
|
||||||
return this.queryService.getOverviewByYearPeriod(year, period_no);
|
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 { TimesheetsCommandService } from "../timesheets/services/timesheets-command.service";
|
||||||
import { ExpensesCommandService } from "../expenses/services/expenses-command.service";
|
import { ExpensesCommandService } from "../expenses/services/expenses-command.service";
|
||||||
import { ShiftsCommandService } from "../shifts/services/shifts-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({
|
@Module({
|
||||||
imports: [PrismaModule, TimesheetsModule],
|
imports: [PrismaModule, TimesheetsModule],
|
||||||
|
|
@ -16,12 +19,14 @@ import { ShiftsCommandService } from "../shifts/services/shifts-command.service"
|
||||||
TimesheetsCommandService,
|
TimesheetsCommandService,
|
||||||
ExpensesCommandService,
|
ExpensesCommandService,
|
||||||
ShiftsCommandService,
|
ShiftsCommandService,
|
||||||
|
BankCodesRepo,
|
||||||
|
TimesheetsRepo,
|
||||||
|
EmployeesRepo,
|
||||||
],
|
],
|
||||||
controllers: [PayPeriodsController],
|
controllers: [PayPeriodsController],
|
||||||
exports: [
|
exports: [
|
||||||
PayPeriodsQueryService,
|
PayPeriodsQueryService,
|
||||||
PayPeriodsCommandService,
|
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(
|
constructor(
|
||||||
private readonly shiftsService: ShiftsQueryService,
|
private readonly shiftsService: ShiftsQueryService,
|
||||||
private readonly shiftsCommandService: ShiftsCommandService,
|
private readonly shiftsCommandService: ShiftsCommandService,
|
||||||
private readonly shiftsValidationService: ShiftsQueryService,
|
|
||||||
){}
|
){}
|
||||||
|
|
||||||
@Put('upsert/:email/:date')
|
@Put('upsert/:email/:date')
|
||||||
|
|
@ -28,53 +27,7 @@ export class ShiftsController {
|
||||||
@Param('date') date_param: string,
|
@Param('date') date_param: string,
|
||||||
@Body() payload: UpsertShiftDto,
|
@Body() payload: UpsertShiftDto,
|
||||||
) {
|
) {
|
||||||
return this.shiftsCommandService.upsertShfitsByDate(email_param, date_param, payload);
|
return this.shiftsCommandService.upsertShiftsByDate(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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('approval/:id')
|
@Patch('approval/:id')
|
||||||
|
|
@ -85,15 +38,14 @@ export class ShiftsController {
|
||||||
|
|
||||||
@Get('summary')
|
@Get('summary')
|
||||||
async getSummary( @Query() query: GetShiftsOverviewDto): Promise<OverviewRow[]> {
|
async getSummary( @Query() query: GetShiftsOverviewDto): Promise<OverviewRow[]> {
|
||||||
return this.shiftsValidationService.getSummary(query.period_id);
|
return this.shiftsService.getSummary(query.period_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('export.csv')
|
@Get('export.csv')
|
||||||
@Header('Content-Type', 'text/csv; charset=utf-8')
|
@Header('Content-Type', 'text/csv; charset=utf-8')
|
||||||
@Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"')
|
@Header('Content-Disposition', 'attachment; filename="shifts-validation.csv"')
|
||||||
async exportCsv(@Query() query: GetShiftsOverviewDto): Promise<Buffer>{
|
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
|
//CSV Headers
|
||||||
const header = [
|
const header = [
|
||||||
'full_name',
|
'full_name',
|
||||||
|
|
@ -125,4 +77,54 @@ export class ShiftsController {
|
||||||
return Buffer.from('\uFEFF' + header + body, 'utf8');
|
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 { Type } from "class-transformer";
|
||||||
import { IsBoolean, IsOptional, IsString, Matches, MaxLength, ValidateNested } from "class-validator";
|
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 {
|
export class ShiftPayloadDto {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export class ShiftsCommandService extends BaseApprovalService<Shifts> {
|
||||||
constructor(prisma: PrismaService) { super(prisma); }
|
constructor(prisma: PrismaService) { super(prisma); }
|
||||||
|
|
||||||
//create/update/delete master method
|
//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[] }> {
|
Promise<{ action: UpsertAction; day: DayShiftResponse[] }> {
|
||||||
const { old_shift, new_shift } = dto;
|
const { old_shift, new_shift } = dto;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
// Deprecated or unused methods
|
||||||
async approve(@Param('id', ParseIntPipe) id: number, @Body('is_approved', ParseBoolPipe) isApproved: boolean) {
|
//_____________________________________________________________________________________________
|
||||||
return this.timesheetsCommand.updateApproval(id, isApproved);
|
|
||||||
}
|
// @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 { Timesheets, TimesheetsArchive } from '@prisma/client';
|
||||||
import { OvertimeService } from 'src/modules/business-logics/services/overtime.service';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { computeHours, formatDateISO, getWeekEnd, getWeekStart } from 'src/common/utils/date-utils';
|
|
||||||
import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
|
||||||
import { buildPeriod, endOfDayUTC, toUTCDateOnly } from '../utils/timesheet.helpers';
|
import { buildPeriod, endOfDayUTC, toUTCDateOnly } from '../utils/timesheet.helpers';
|
||||||
import type { ShiftRow, ExpenseRow } 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 { TimesheetDto } from '../dtos/overview-timesheet.dto';
|
||||||
|
import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
||||||
|
import type { ShiftRow, ExpenseRow } from '../utils/timesheet.helpers';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -174,14 +174,14 @@ export class TimesheetsQueryService {
|
||||||
const to_HH_mm = (date: Date) => date.toISOString().slice(11, 16);
|
const to_HH_mm = (date: Date) => date.toISOString().slice(11, 16);
|
||||||
|
|
||||||
//maps all shifts of selected timesheet
|
//maps all shifts of selected timesheet
|
||||||
const shifts = timesheet.shift.map((sft) => ({
|
const shifts = timesheet.shift.map((shift_row) => ({
|
||||||
bank_type: sft.bank_code?.type ?? '',
|
bank_type: shift_row.bank_code?.type ?? '',
|
||||||
date: formatDateISO(sft.date),
|
date: formatDateISO(shift_row.date),
|
||||||
start_time: to_HH_mm(sft.start_time),
|
start_time: to_HH_mm(shift_row.start_time),
|
||||||
end_time: to_HH_mm(sft.end_time),
|
end_time: to_HH_mm(shift_row.end_time),
|
||||||
comment: sft.comment ?? '',
|
comment: shift_row.comment ?? '',
|
||||||
is_approved: sft.is_approved ?? false,
|
is_approved: shift_row.is_approved ?? false,
|
||||||
is_remote: sft.is_remote ?? false,
|
is_remote: shift_row.is_remote ?? false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
//maps all expenses of selected timsheet
|
//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 { TimesheetsCommandService } from './services/timesheets-command.service';
|
||||||
import { ShiftsCommandService } from '../shifts/services/shifts-command.service';
|
import { ShiftsCommandService } from '../shifts/services/shifts-command.service';
|
||||||
import { ExpensesCommandService } from '../expenses/services/expenses-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({
|
@Module({
|
||||||
imports: [BusinessLogicsModule],
|
imports: [BusinessLogicsModule],
|
||||||
|
|
@ -13,7 +16,10 @@ import { ExpensesCommandService } from '../expenses/services/expenses-command.se
|
||||||
TimesheetsQueryService,
|
TimesheetsQueryService,
|
||||||
TimesheetsCommandService,
|
TimesheetsCommandService,
|
||||||
ShiftsCommandService,
|
ShiftsCommandService,
|
||||||
ExpensesCommandService
|
ExpensesCommandService,
|
||||||
|
BankCodesRepo,
|
||||||
|
TimesheetsRepo,
|
||||||
|
EmployeesRepo,
|
||||||
],
|
],
|
||||||
exports: [TimesheetsQueryService],
|
exports: [TimesheetsQueryService],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user