Merge branch 'main' of git.targo.ca:Targo/targo_backend into auth-session-fullstack
This commit is contained in:
commit
75f07aaf99
|
|
@ -468,6 +468,88 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/timesheets/{email}": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "TimesheetsController_getByEmail",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "email",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "offset",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"access-token": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Timesheets"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/timesheets/shifts/{email}": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "TimesheetsController_createTimesheetShifts",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "email",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "offset",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreateWeekShiftsDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"access-token": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Timesheets"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/timesheets/{id}": {
|
"/timesheets/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "TimesheetsController_findOne",
|
"operationId": "TimesheetsController_findOne",
|
||||||
|
|
@ -506,53 +588,6 @@
|
||||||
"Timesheets"
|
"Timesheets"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"patch": {
|
|
||||||
"operationId": "TimesheetsController_update",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/UpdateTimesheetDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": "Timesheet updated",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/CreateTimesheetDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Timesheet not found"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"access-token": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"summary": "Update timesheet",
|
|
||||||
"tags": [
|
|
||||||
"Timesheets"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"delete": {
|
"delete": {
|
||||||
"operationId": "TimesheetsController_remove",
|
"operationId": "TimesheetsController_remove",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
|
@ -2370,50 +2405,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CreateWeekShiftsDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {}
|
||||||
|
},
|
||||||
"CreateTimesheetDto": {
|
"CreateTimesheetDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {}
|
||||||
"id": {
|
|
||||||
"type": "number",
|
|
||||||
"example": 1,
|
|
||||||
"description": "timesheet`s unique ID (auto-generated)"
|
|
||||||
},
|
|
||||||
"employee_id": {
|
|
||||||
"type": "number",
|
|
||||||
"example": 426433,
|
|
||||||
"description": "employee`s ID number of linked timsheet"
|
|
||||||
},
|
|
||||||
"is_approved": {
|
|
||||||
"type": "boolean",
|
|
||||||
"example": true,
|
|
||||||
"description": "Timesheet`s status approval"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"id",
|
|
||||||
"employee_id",
|
|
||||||
"is_approved"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"UpdateTimesheetDto": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "number",
|
|
||||||
"example": 1,
|
|
||||||
"description": "timesheet`s unique ID (auto-generated)"
|
|
||||||
},
|
|
||||||
"employee_id": {
|
|
||||||
"type": "number",
|
|
||||||
"example": 426433,
|
|
||||||
"description": "employee`s ID number of linked timsheet"
|
|
||||||
},
|
|
||||||
"is_approved": {
|
|
||||||
"type": "boolean",
|
|
||||||
"example": true,
|
|
||||||
"description": "Timesheet`s status approval"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"CreateExpenseDto": {
|
"CreateExpenseDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "public"."shifts" ADD COLUMN "is_remote" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
@ -4,14 +4,16 @@ const prisma = new PrismaClient();
|
||||||
|
|
||||||
// ====== Config ======
|
// ====== Config ======
|
||||||
const PREVIOUS_WEEKS = 5;
|
const PREVIOUS_WEEKS = 5;
|
||||||
const INCLUDE_CURRENT = false;
|
const INCLUDE_CURRENT = true;
|
||||||
|
const INCR = 15; // incrément ferme de 15 minutes (0.25 h)
|
||||||
|
const DAY_MIN = 5 * 60; // 5h
|
||||||
|
const DAY_MAX = 11 * 60; // 11h
|
||||||
|
const HARD_END = 19 * 60 + 30; // 19:30
|
||||||
|
|
||||||
// Times-only via Date (UTC 1970-01-01)
|
// ====== Helpers temps ======
|
||||||
function timeAt(hour: number, minute: number) {
|
function timeAt(hour: number, minute: number) {
|
||||||
return new Date(Date.UTC(1970, 0, 1, hour, minute, 0));
|
return new Date(Date.UTC(1970, 0, 1, hour, minute, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lundi (UTC) de la date fournie
|
|
||||||
function mondayOfThisWeekUTC(now = new Date()) {
|
function mondayOfThisWeekUTC(now = new Date()) {
|
||||||
const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
||||||
const day = d.getUTCDay();
|
const day = d.getUTCDay();
|
||||||
|
|
@ -20,7 +22,6 @@ function mondayOfThisWeekUTC(now = new Date()) {
|
||||||
d.setUTCHours(0, 0, 0, 0);
|
d.setUTCHours(0, 0, 0, 0);
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
function weekDatesFromMonday(monday: Date) {
|
function weekDatesFromMonday(monday: Date) {
|
||||||
return Array.from({ length: 5 }, (_, i) => {
|
return Array.from({ length: 5 }, (_, i) => {
|
||||||
const d = new Date(monday);
|
const d = new Date(monday);
|
||||||
|
|
@ -28,16 +29,35 @@ function weekDatesFromMonday(monday: Date) {
|
||||||
return d;
|
return d;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function mondayNWeeksBefore(monday: Date, n: number) {
|
function mondayNWeeksBefore(monday: Date, n: number) {
|
||||||
const d = new Date(monday);
|
const d = new Date(monday);
|
||||||
d.setUTCDate(d.getUTCDate() - n * 7);
|
d.setUTCDate(d.getUTCDate() - n * 7);
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rndInt(min: number, max: number) {
|
function rndInt(min: number, max: number) {
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
}
|
}
|
||||||
|
function clamp(n: number, min: number, max: number) {
|
||||||
|
return Math.min(max, Math.max(min, n));
|
||||||
|
}
|
||||||
|
function addMinutes(h: number, m: number, delta: number) {
|
||||||
|
const total = h * 60 + m + delta;
|
||||||
|
const hh = Math.floor(total / 60);
|
||||||
|
const mm = ((total % 60) + 60) % 60;
|
||||||
|
return { h: hh, m: mm };
|
||||||
|
}
|
||||||
|
// Aligne vers le multiple de INCR le plus proche
|
||||||
|
function quantize(mins: number): number {
|
||||||
|
const q = Math.round(mins / INCR) * INCR;
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
// Tire un multiple de INCR dans [min,max] (inclus), supposés entiers minutes
|
||||||
|
function rndQuantized(min: number, max: number): number {
|
||||||
|
const qmin = Math.ceil(min / INCR);
|
||||||
|
const qmax = Math.floor(max / INCR);
|
||||||
|
const q = rndInt(qmin, qmax);
|
||||||
|
return q * INCR;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper: garantit le timesheet de la semaine (upsert)
|
// Helper: garantit le timesheet de la semaine (upsert)
|
||||||
async function getOrCreateTimesheet(employee_id: number, start_date: Date) {
|
async function getOrCreateTimesheet(employee_id: number, start_date: Date) {
|
||||||
|
|
@ -50,8 +70,13 @@ async function getOrCreateTimesheet(employee_id: number, start_date: Date) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Bank codes utilisés
|
// --- Bank codes (pondérés: surtout G1 = régulier) ---
|
||||||
const BANKS = ['G1', 'G56', 'G48', 'G700', 'G105', 'G305', 'G43'] as const;
|
const BANKS = ['G1', 'G56', 'G48', 'G700', 'G105', 'G305', 'G43'] as const;
|
||||||
|
const WEIGHTED_CODES = [
|
||||||
|
'G1','G1','G1','G1','G1','G1','G1','G1', // 8x régulier
|
||||||
|
'G56','G48','G700','G105','G305','G43'
|
||||||
|
] as const;
|
||||||
|
|
||||||
const bcRows = await prisma.bankCodes.findMany({
|
const bcRows = await prisma.bankCodes.findMany({
|
||||||
where: { bank_code: { in: BANKS as unknown as string[] } },
|
where: { bank_code: { in: BANKS as unknown as string[] } },
|
||||||
select: { id: true, bank_code: true },
|
select: { id: true, bank_code: true },
|
||||||
|
|
@ -70,59 +95,140 @@ async function main() {
|
||||||
const mondayThisWeek = mondayOfThisWeekUTC();
|
const mondayThisWeek = mondayOfThisWeekUTC();
|
||||||
const mondays: Date[] = [];
|
const mondays: Date[] = [];
|
||||||
if (INCLUDE_CURRENT) mondays.push(mondayThisWeek);
|
if (INCLUDE_CURRENT) mondays.push(mondayThisWeek);
|
||||||
for (let n = 1; n <= PREVIOUS_WEEKS; n++) {
|
for (let n = 1; n <= PREVIOUS_WEEKS; n++) mondays.push(mondayNWeeksBefore(mondayThisWeek, n));
|
||||||
mondays.push(mondayNWeeksBefore(mondayThisWeek, n));
|
|
||||||
}
|
|
||||||
|
|
||||||
let created = 0;
|
let created = 0;
|
||||||
|
|
||||||
for (let wi = 0; wi < mondays.length; wi++) {
|
for (let wi = 0; wi < mondays.length; wi++) {
|
||||||
const monday = mondays[wi];
|
const monday = mondays[wi];
|
||||||
const weekDays = weekDatesFromMonday(monday);
|
const days = weekDatesFromMonday(monday);
|
||||||
|
|
||||||
for (let ei = 0; ei < employees.length; ei++) {
|
for (let ei = 0; ei < employees.length; ei++) {
|
||||||
const e = employees[ei];
|
const e = employees[ei];
|
||||||
|
|
||||||
const baseStartHour = 6 + (ei % 5);
|
// Cible hebdo 35–45h, multiple de 15 min
|
||||||
const baseStartMinute = (ei * 15) % 60;
|
const weeklyTargetMin = rndQuantized(35 * 60, 45 * 60);
|
||||||
|
|
||||||
for (let di = 0; di < weekDays.length; di++) {
|
// Start de base (7:00, 7:15, 7:30, 7:45, 8:00, 8:15, 8:30, 8:45, 9:00 ...)
|
||||||
const date = weekDays[di];
|
const baseStartH = 7 + (ei % 3); // 7,8,9
|
||||||
|
const baseStartM = ( (ei * 15) % 60 ); // aligné 15 min
|
||||||
|
|
||||||
// 1) Trouver/Créer le timesheet de CETTE semaine pour CET employé
|
// Planification journalière (5 jours) ~8h ± 45 min, quantisée 15 min
|
||||||
const weekStart = mondayOfThisWeekUTC(date);
|
const plannedDaily: number[] = [];
|
||||||
const ts = await getOrCreateTimesheet(e.id, weekStart);
|
for (let d = 0; d < 5; d++) {
|
||||||
|
const jitter = rndInt(-3, 3) * INCR; // -45..+45 par pas de 15
|
||||||
|
const base = 8 * 60 + jitter;
|
||||||
|
plannedDaily.push(quantize(clamp(base, DAY_MIN, DAY_MAX)));
|
||||||
|
}
|
||||||
|
|
||||||
// 2) Tirage aléatoire du bank_code
|
// Ajuster le 5e jour pour atteindre la cible hebdo exactement (par pas de 15)
|
||||||
const randomCode = BANKS[Math.floor(Math.random() * BANKS.length)];
|
const sumFirst4 = plannedDaily.slice(0, 4).reduce((a, b) => a + b, 0);
|
||||||
const bank_code_id = bcMap.get(randomCode)!;
|
plannedDaily[4] = quantize(clamp(weeklyTargetMin - sumFirst4, DAY_MIN, DAY_MAX));
|
||||||
|
|
||||||
// 3) Horaire
|
// Corriger le petit écart restant (devrait être multiple de 15) en redistribuant ±15
|
||||||
const duration = rndInt(4, 10);
|
let diff = weeklyTargetMin - plannedDaily.reduce((a, b) => a + b, 0);
|
||||||
const dayWeekOffset = (di + wi + (ei % 3)) % 3;
|
const step = diff > 0 ? INCR : -INCR;
|
||||||
const startH = Math.min(12, baseStartHour + dayWeekOffset);
|
let guard = 100; // anti-boucle
|
||||||
const startM = baseStartMinute;
|
while (diff !== 0 && guard-- > 0) {
|
||||||
const endH = startH + duration;
|
for (let d = 0; d < 5 && diff !== 0; d++) {
|
||||||
const endM = startM;
|
const next = plannedDaily[d] + step;
|
||||||
|
if (next >= DAY_MIN && next <= DAY_MAX) {
|
||||||
|
plannedDaily[d] = next;
|
||||||
|
diff -= step;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert du timesheet (semaine)
|
||||||
|
const ts = await getOrCreateTimesheet(e.id, mondayOfThisWeekUTC(days[0]));
|
||||||
|
|
||||||
|
for (let di = 0; di < 5; di++) {
|
||||||
|
const date = days[di];
|
||||||
|
const targetWorkMin = plannedDaily[di]; // multiple de 15
|
||||||
|
|
||||||
|
// Départ ~ base + jitter (par pas de 15 min aussi)
|
||||||
|
const startJitter = rndInt(-1, 2) * INCR; // -15,0,+15,+30
|
||||||
|
const { h: startH, m: startM } = addMinutes(baseStartH, baseStartM, startJitter);
|
||||||
|
|
||||||
|
// Pause: entre 11:00 et 14:00, mais pas avant start+3h ni après start+6h (le tout quantisé 15)
|
||||||
|
const earliestLunch = Math.max((startH * 60 + startM) + 3 * 60, 11 * 60);
|
||||||
|
const latestLunch = Math.min((startH * 60 + startM) + 6 * 60, 14 * 60);
|
||||||
|
const lunchStartMin = rndQuantized(earliestLunch, latestLunch);
|
||||||
|
const lunchDur = rndQuantized(30, 120); // 30..120 min en pas de 15
|
||||||
|
const lunchEndMin = lunchStartMin + lunchDur;
|
||||||
|
|
||||||
|
// Travail = (lunchStart - start) + (end - lunchEnd)
|
||||||
|
const morningWork = Math.max(0, lunchStartMin - (startH * 60 + startM)); // multiple de 15
|
||||||
|
let afternoonWork = Math.max(60, targetWorkMin - morningWork); // multiple de 15 (diff de deux multiples de 15)
|
||||||
|
if (afternoonWork % INCR !== 0) {
|
||||||
|
// sécurité (ne devrait pas arriver)
|
||||||
|
afternoonWork = quantize(afternoonWork);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fin de journée (quantisée par construction)
|
||||||
|
const endMinRaw = lunchEndMin + afternoonWork;
|
||||||
|
const endMin = Math.min(endMinRaw, HARD_END);
|
||||||
|
|
||||||
|
// Bank codes variés
|
||||||
|
const bcMorningCode = WEIGHTED_CODES[rndInt(0, WEIGHTED_CODES.length - 1)];
|
||||||
|
const bcAfternoonCode = WEIGHTED_CODES[rndInt(0, WEIGHTED_CODES.length - 1)];
|
||||||
|
const bcMorningId = bcMap.get(bcMorningCode)!;
|
||||||
|
const bcAfternoonId = bcMap.get(bcAfternoonCode)!;
|
||||||
|
|
||||||
|
// Shift matin
|
||||||
|
const lunchStartHM = { h: Math.floor(lunchStartMin / 60), m: lunchStartMin % 60 };
|
||||||
await prisma.shifts.create({
|
await prisma.shifts.create({
|
||||||
data: {
|
data: {
|
||||||
timesheet_id: ts.id,
|
timesheet_id: ts.id,
|
||||||
bank_code_id,
|
bank_code_id: bcMorningId,
|
||||||
description: `Shift ${di + 1} (semaine du ${monday.toISOString().slice(0, 10)}) emp ${e.id} — ${randomCode}`,
|
description: `Matin J${di + 1} (sem ${monday.toISOString().slice(0, 10)}) emp ${e.id} — ${bcMorningCode}`,
|
||||||
date,
|
date,
|
||||||
start_time: timeAt(startH, startM),
|
start_time: timeAt(startH, startM),
|
||||||
end_time: timeAt(endH, endM),
|
end_time: timeAt(lunchStartHM.h, lunchStartHM.m),
|
||||||
is_approved: Math.random() < 0.5,
|
is_approved: Math.random() < 0.6,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
created++;
|
created++;
|
||||||
|
|
||||||
|
// Shift après-midi (si >= 30 min — sera de toute façon multiple de 15)
|
||||||
|
const pmDuration = endMin - lunchEndMin;
|
||||||
|
if (pmDuration >= 30) {
|
||||||
|
const lunchEndHM = { h: Math.floor(lunchEndMin / 60), m: lunchEndMin % 60 };
|
||||||
|
const finalEndHM = { h: Math.floor(endMin / 60), m: endMin % 60 };
|
||||||
|
await prisma.shifts.create({
|
||||||
|
data: {
|
||||||
|
timesheet_id: ts.id,
|
||||||
|
bank_code_id: bcAfternoonId,
|
||||||
|
description: `Après-midi J${di + 1} (sem ${monday.toISOString().slice(0, 10)}) emp ${e.id} — ${bcAfternoonCode}`,
|
||||||
|
date,
|
||||||
|
start_time: timeAt(lunchEndHM.h, lunchEndHM.m),
|
||||||
|
end_time: timeAt(finalEndHM.h, finalEndHM.m),
|
||||||
|
is_approved: Math.random() < 0.6,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
created++;
|
||||||
|
} else {
|
||||||
|
// Fallback très rare : un seul shift couvrant la journée (tout en multiples de 15)
|
||||||
|
const fallbackEnd = addMinutes(startH, startM, targetWorkMin + lunchDur);
|
||||||
|
await prisma.shifts.create({
|
||||||
|
data: {
|
||||||
|
timesheet_id: ts.id,
|
||||||
|
bank_code_id: bcMap.get('G1')!,
|
||||||
|
description: `Fallback J${di + 1} (sem ${monday.toISOString().slice(0, 10)}) emp ${e.id} — G1`,
|
||||||
|
date,
|
||||||
|
start_time: timeAt(startH, startM),
|
||||||
|
end_time: timeAt(fallbackEnd.h, fallbackEnd.m),
|
||||||
|
is_approved: Math.random() < 0.6,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
created++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const total = await prisma.shifts.count();
|
const total = await prisma.shifts.count();
|
||||||
console.log(`✓ Shifts: ${created} nouvelles lignes, ${total} total rows (${INCLUDE_CURRENT ? 'courante +' : ''}${PREVIOUS_WEEKS} semaines précédentes, L→V)`);
|
console.log(`✓ Shifts créés: ${created} | total en DB: ${total} (${INCLUDE_CURRENT ? 'inclut semaine courante, ' : ''}${PREVIOUS_WEEKS} sem passées, L→V, 2 shifts/jour, pas de décimaux foireux})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().finally(() => prisma.$disconnect());
|
main().finally(() => prisma.$disconnect());
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,12 @@ import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
// Lundi (UTC) de la date fournie
|
// ====== Config ======
|
||||||
|
const WEEKS_BACK = 4; // 4 semaines avant + semaine courante
|
||||||
|
const INCLUDE_CURRENT = true; // inclure la semaine courante
|
||||||
|
const STEP_CENTS = 25; // montants en quarts de dollar (.00/.25/.50/.75)
|
||||||
|
|
||||||
|
// ====== Helpers dates ======
|
||||||
function mondayOfThisWeekUTC(now = new Date()) {
|
function mondayOfThisWeekUTC(now = new Date()) {
|
||||||
const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
||||||
const day = d.getUTCDay();
|
const day = d.getUTCDay();
|
||||||
|
|
@ -11,10 +16,13 @@ function mondayOfThisWeekUTC(now = new Date()) {
|
||||||
d.setUTCHours(0, 0, 0, 0);
|
d.setUTCHours(0, 0, 0, 0);
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
function mondayNWeeksBefore(monday: Date, n: number) {
|
||||||
// Dates Lundi→Vendredi (UTC minuit)
|
const d = new Date(monday);
|
||||||
function currentWeekDates() {
|
d.setUTCDate(monday.getUTCDate() - n * 7);
|
||||||
const monday = mondayOfThisWeekUTC();
|
return d;
|
||||||
|
}
|
||||||
|
// L→V (UTC minuit)
|
||||||
|
function weekDatesMonToFri(monday: Date) {
|
||||||
return Array.from({ length: 5 }, (_, i) => {
|
return Array.from({ length: 5 }, (_, i) => {
|
||||||
const d = new Date(monday);
|
const d = new Date(monday);
|
||||||
d.setUTCDate(monday.getUTCDate() + i);
|
d.setUTCDate(monday.getUTCDate() + i);
|
||||||
|
|
@ -22,15 +30,30 @@ function currentWeekDates() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====== Helpers random / amount ======
|
||||||
function rndInt(min: number, max: number) {
|
function rndInt(min: number, max: number) {
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
}
|
}
|
||||||
function rndAmount(minCents: number, maxCents: number) {
|
// String "xx.yy" à partir de cents ENTiers (jamais de float)
|
||||||
const cents = rndInt(minCents, maxCents);
|
function centsToAmountString(cents: number): string {
|
||||||
return (cents / 100).toFixed(2);
|
const sign = cents < 0 ? '-' : '';
|
||||||
|
const abs = Math.abs(cents);
|
||||||
|
const dollars = Math.floor(abs / 100);
|
||||||
|
const c = abs % 100;
|
||||||
|
return `${sign}${dollars}.${c.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
// Tire un multiple de STEP_CENTS entre minCents et maxCents (inclus)
|
||||||
|
function rndQuantizedCents(minCents: number, maxCents: number, step = STEP_CENTS): number {
|
||||||
|
const qmin = Math.ceil(minCents / step);
|
||||||
|
const qmax = Math.floor(maxCents / step);
|
||||||
|
const q = rndInt(qmin, qmax);
|
||||||
|
return q * step;
|
||||||
|
}
|
||||||
|
function rndAmount(minCents: number, maxCents: number): string {
|
||||||
|
return centsToAmountString(rndQuantizedCents(minCents, maxCents));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: garantit le timesheet de la semaine (upsert)
|
// ====== Timesheet upsert ======
|
||||||
async function getOrCreateTimesheet(employee_id: number, start_date: Date) {
|
async function getOrCreateTimesheet(employee_id: number, start_date: Date) {
|
||||||
return prisma.timesheets.upsert({
|
return prisma.timesheets.upsert({
|
||||||
where: { employee_id_start_date: { employee_id, start_date } },
|
where: { employee_id_start_date: { employee_id, start_date } },
|
||||||
|
|
@ -41,8 +64,10 @@ async function getOrCreateTimesheet(employee_id: number, start_date: Date) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Codes autorisés (aléatoires à chaque dépense)
|
// Codes d'EXPENSES (exemples)
|
||||||
const BANKS = ['G517', 'G56', 'G502', 'G202', 'G234'] as const;
|
const BANKS = ['G517', 'G503', 'G502', 'G202', 'G234'] as const;
|
||||||
|
|
||||||
|
// Précharger les bank codes
|
||||||
const bcRows = await prisma.bankCodes.findMany({
|
const bcRows = await prisma.bankCodes.findMany({
|
||||||
where: { bank_code: { in: BANKS as unknown as string[] } },
|
where: { bank_code: { in: BANKS as unknown as string[] } },
|
||||||
select: { id: true, bank_code: true },
|
select: { id: true, bank_code: true },
|
||||||
|
|
@ -52,58 +77,85 @@ async function main() {
|
||||||
if (!bcMap.has(c)) throw new Error(`Bank code manquant: ${c}`);
|
if (!bcMap.has(c)) throw new Error(`Bank code manquant: ${c}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
console.warn('Aucun employé — rien à insérer.');
|
console.warn('Aucun employé — rien à insérer.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const weekDays = currentWeekDates();
|
// Liste des lundis (courant + 4 précédents)
|
||||||
const monday = weekDays[0];
|
const mondayThisWeek = mondayOfThisWeekUTC();
|
||||||
const friday = weekDays[4];
|
const mondays: Date[] = [];
|
||||||
|
if (INCLUDE_CURRENT) mondays.push(mondayThisWeek);
|
||||||
|
for (let n = 1; n <= WEEKS_BACK; n++) mondays.push(mondayNWeeksBefore(mondayThisWeek, n));
|
||||||
|
|
||||||
let created = 0;
|
let created = 0;
|
||||||
|
|
||||||
for (const e of employees) {
|
for (const monday of mondays) {
|
||||||
// 1) Semaine courante → assurer le timesheet de la semaine
|
const weekDays = weekDatesMonToFri(monday);
|
||||||
const weekStart = mondayOfThisWeekUTC();
|
const friday = weekDays[4];
|
||||||
const ts = await getOrCreateTimesheet(e.id, weekStart);
|
|
||||||
|
|
||||||
// 2) Skip si l’employé a déjà une dépense cette semaine (on garantit ≥1)
|
for (const e of employees) {
|
||||||
const already = await prisma.expenses.findFirst({
|
// Upsert timesheet pour CETTE semaine/employee
|
||||||
where: { timesheet_id: ts.id, date: { gte: monday, lte: friday } },
|
const ts = await getOrCreateTimesheet(e.id, monday);
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
if (already) continue;
|
|
||||||
|
|
||||||
// 3) Choix aléatoire du code + jour
|
// Idempotence: si déjà au moins une expense L→V, on skip la semaine
|
||||||
const randomCode = BANKS[Math.floor(Math.random() * BANKS.length)];
|
const already = await prisma.expenses.findFirst({
|
||||||
const bank_code_id = bcMap.get(randomCode)!;
|
where: { timesheet_id: ts.id, date: { gte: monday, lte: friday } },
|
||||||
const date = weekDays[Math.floor(Math.random() * weekDays.length)];
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (already) continue;
|
||||||
|
|
||||||
// 4) Montant varié
|
// 1 à 3 expenses (jours distincts)
|
||||||
const amount =
|
const count = rndInt(1, 3);
|
||||||
randomCode === 'G56'
|
const dayIndexes = [0, 1, 2, 3, 4].sort(() => Math.random() - 0.5).slice(0, count);
|
||||||
? rndAmount(1000, 7500) // 10.00..75.00
|
|
||||||
: rndAmount(2000, 25000); // 20.00..250.00
|
|
||||||
|
|
||||||
await prisma.expenses.create({
|
for (const idx of dayIndexes) {
|
||||||
data: {
|
const date = weekDays[idx];
|
||||||
timesheet_id: ts.id,
|
const code = BANKS[rndInt(0, BANKS.length - 1)];
|
||||||
bank_code_id,
|
const bank_code_id = bcMap.get(code)!;
|
||||||
date,
|
|
||||||
amount,
|
// Montants (cents) quantisés à 25¢ => aucun flottant binaire plus tard
|
||||||
attachement: null,
|
let amount: string;
|
||||||
description: `Expense ${randomCode} ${amount}$ (emp ${e.id})`,
|
switch (code) {
|
||||||
is_approved: Math.random() < 0.6,
|
case 'G503': // petites fournitures
|
||||||
supervisor_comment: Math.random() < 0.2 ? 'OK' : null,
|
amount = rndAmount(1000, 7500); // 10.00 à 75.00
|
||||||
},
|
break;
|
||||||
});
|
case 'G502': // repas
|
||||||
created++;
|
amount = rndAmount(1500, 3000); // 15.00 à 30.00
|
||||||
|
break;
|
||||||
|
case 'G202': // essence
|
||||||
|
amount = rndAmount(2000, 15000); // 20.00 à 150.00
|
||||||
|
break;
|
||||||
|
case 'G234': // hébergement
|
||||||
|
amount = rndAmount(6000, 25000); // 60.00 à 250.00
|
||||||
|
break;
|
||||||
|
case 'G517': // péages / divers
|
||||||
|
default:
|
||||||
|
amount = rndAmount(500, 5000); // 5.00 à 50.00
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.expenses.create({
|
||||||
|
data: {
|
||||||
|
timesheet_id: ts.id,
|
||||||
|
bank_code_id,
|
||||||
|
date,
|
||||||
|
amount, // string "xx.yy" (2 décimales exactes)
|
||||||
|
attachement: null,
|
||||||
|
description: `Expense ${code} ${amount}$ (emp ${e.id})`,
|
||||||
|
is_approved: Math.random() < 0.65,
|
||||||
|
supervisor_comment: Math.random() < 0.25 ? 'OK' : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const total = await prisma.expenses.count();
|
const total = await prisma.expenses.count();
|
||||||
console.log(`✓ Expenses: ${created} nouvelles lignes, ${total} total rows (≥1 expense/employee pour la semaine courante)`);
|
console.log(`✓ Expenses: ${created} nouvelles lignes, ${total} total rows (sem courante + ${WEEKS_BACK} précédentes, L→V uniquement, montants en quarts de dollar)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().finally(() => prisma.$disconnect());
|
main().finally(() => prisma.$disconnect());
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,7 @@ model Shifts {
|
||||||
start_time DateTime @db.Time(0)
|
start_time DateTime @db.Time(0)
|
||||||
end_time DateTime @db.Time(0)
|
end_time DateTime @db.Time(0)
|
||||||
is_approved Boolean @default(false)
|
is_approved Boolean @default(false)
|
||||||
|
is_remote Boolean @default(false)
|
||||||
|
|
||||||
archive ShiftsArchive[] @relation("ShiftsToArchive")
|
archive ShiftsArchive[] @relation("ShiftsToArchive")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,13 @@ export function getYearStart(date:Date): Date {
|
||||||
return new Date(date.getFullYear(),0,1,0,0,0,0);
|
return new Date(date.getFullYear(),0,1,0,0,0,0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCurrentWeek(): { start_date_week: Date; end_date_week: Date } {
|
||||||
|
const now = new Date();
|
||||||
|
const start_date_week = getWeekStart(now, 0);
|
||||||
|
const end_date_week = getWeekEnd(start_date_week);
|
||||||
|
return { start_date_week, end_date_week };
|
||||||
|
}
|
||||||
|
|
||||||
//cloning methods (helps with notify for overtime in a single day)
|
//cloning methods (helps with notify for overtime in a single day)
|
||||||
// export function toDateOnly(day: Date): Date {
|
// export function toDateOnly(day: Date): Date {
|
||||||
// const d = new Date(day);
|
// const d = new Date(day);
|
||||||
|
|
|
||||||
|
|
@ -41,4 +41,6 @@ export class EmployeePeriodOverviewDto {
|
||||||
description: 'Tous les timesheets de la période sont approuvés pour cet employé',
|
description: 'Tous les timesheets de la période sont approuvés pour cet employé',
|
||||||
})
|
})
|
||||||
is_approved: boolean;
|
is_approved: boolean;
|
||||||
|
|
||||||
|
is_remote: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,38 +68,4 @@ export class PayPeriodsCommandService {
|
||||||
});
|
});
|
||||||
return {updated};
|
return {updated};
|
||||||
}
|
}
|
||||||
|
|
||||||
//function to approve a single pay-period of a single employee (deprecated)
|
|
||||||
// async approvalPayPeriod(pay_year: number , period_no: number): Promise<void> {
|
|
||||||
// const period = await this.prisma.payPeriods.findFirst({
|
|
||||||
// where: { pay_year, pay_period_no: period_no},
|
|
||||||
// });
|
|
||||||
// if (!period) throw new NotFoundException(`PayPeriod #${pay_year}-${period_no} not found`);
|
|
||||||
|
|
||||||
// //fetches timesheet of selected period if the timesheet has atleast 1 shift or 1 expense
|
|
||||||
// const timesheet_ist = await this.prisma.timesheets.findMany({
|
|
||||||
// where: {
|
|
||||||
// OR: [
|
|
||||||
// { shift: {some: { date: { gte: period.period_start,
|
|
||||||
// lte: period.period_end,
|
|
||||||
// },
|
|
||||||
// }},
|
|
||||||
// },
|
|
||||||
// { expense: { some: { date: { gte: period.period_start,
|
|
||||||
// lte: period.period_end,
|
|
||||||
// },
|
|
||||||
// }},
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// select: { id: true },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// //approval of both timesheet (cascading to the approval of related shifts and expenses)
|
|
||||||
// await this.prisma.$transaction(async (transaction)=> {
|
|
||||||
// for(const {id} of timesheet_ist) {
|
|
||||||
// await this.timesheets_approval.updateApprovalWithTransaction(transaction,id, true);
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
@ -40,6 +40,7 @@ export class PayPeriodsQueryService {
|
||||||
} as any);
|
} as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//find crew member associated with supervisor
|
||||||
private async resolveCrew(supervisor_id: number, include_subtree: boolean):
|
private async resolveCrew(supervisor_id: number, include_subtree: boolean):
|
||||||
Promise<Array<{ id: number; first_name: string; last_name: string; email: string }>> {
|
Promise<Array<{ id: number; first_name: string; last_name: string; email: string }>> {
|
||||||
const result: Array<{ id: number; first_name: string; last_name: string; email: string; }> = [];
|
const result: Array<{ id: number; first_name: string; last_name: string; email: string; }> = [];
|
||||||
|
|
@ -69,6 +70,7 @@ export class PayPeriodsQueryService {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//fetchs crew emails
|
||||||
async resolveCrewEmails(supervisor_id: number, include_subtree: boolean): Promise<Set<string>> {
|
async resolveCrewEmails(supervisor_id: number, include_subtree: boolean): Promise<Set<string>> {
|
||||||
const crew = await this.resolveCrew(supervisor_id, include_subtree);
|
const crew = await this.resolveCrew(supervisor_id, include_subtree);
|
||||||
return new Set(crew.map(crew_member => crew_member.email).filter(Boolean));
|
return new Set(crew.map(crew_member => crew_member.email).filter(Boolean));
|
||||||
|
|
@ -149,6 +151,7 @@ export class PayPeriodsQueryService {
|
||||||
select: {
|
select: {
|
||||||
start_time: true,
|
start_time: true,
|
||||||
end_time: true,
|
end_time: true,
|
||||||
|
is_remote: true,
|
||||||
timesheet: { select: {
|
timesheet: { select: {
|
||||||
is_approved: true,
|
is_approved: true,
|
||||||
employee: { select: {
|
employee: { select: {
|
||||||
|
|
@ -161,9 +164,7 @@ export class PayPeriodsQueryService {
|
||||||
} },
|
} },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bank_code: { select: {
|
bank_code: { select: { categorie: true, type: true } },
|
||||||
type: true,
|
|
||||||
categorie: true } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -186,10 +187,7 @@ export class PayPeriodsQueryService {
|
||||||
} },
|
} },
|
||||||
} },
|
} },
|
||||||
} },
|
} },
|
||||||
bank_code: { select: {
|
bank_code: { select: { categorie: true, modifier: true, type: true } },
|
||||||
type: true,
|
|
||||||
categorie: true,
|
|
||||||
modifier: true } },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -208,6 +206,7 @@ export class PayPeriodsQueryService {
|
||||||
expenses: 0,
|
expenses: 0,
|
||||||
mileage: 0,
|
mileage: 0,
|
||||||
is_approved: true,
|
is_approved: true,
|
||||||
|
is_remote: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -224,6 +223,7 @@ export class PayPeriodsQueryService {
|
||||||
expenses: 0,
|
expenses: 0,
|
||||||
mileage: 0,
|
mileage: 0,
|
||||||
is_approved: true,
|
is_approved: true,
|
||||||
|
is_remote: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return by_employee.get(id)!;
|
return by_employee.get(id)!;
|
||||||
|
|
@ -235,12 +235,12 @@ export class PayPeriodsQueryService {
|
||||||
const record = ensure(employee.id, name, employee.user.email);
|
const record = ensure(employee.id, name, employee.user.email);
|
||||||
|
|
||||||
const hours = computeHours(shift.start_time, shift.end_time);
|
const hours = computeHours(shift.start_time, shift.end_time);
|
||||||
const type = (shift.bank_code?.type).toUpperCase();
|
const categorie = (shift.bank_code?.type).toUpperCase();
|
||||||
switch (type) {
|
switch (categorie) {
|
||||||
case "EVENING": record.evening_hours += hours; break;
|
case "EVENING": record.evening_hours += hours; break;
|
||||||
case "EMERGENCY": record.emergency_hours += hours; break;
|
case "EMERGENCY": record.emergency_hours += hours; break;
|
||||||
case "OVERTIME": record.overtime_hours += hours; break;
|
case "OVERTIME": record.overtime_hours += hours; break;
|
||||||
case "REGULAR": record.regular_hours += hours; break;
|
case "REGULAR" : record.regular_hours += hours; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
record.is_approved = record.is_approved && shift.timesheet.is_approved;
|
record.is_approved = record.is_approved && shift.timesheet.is_approved;
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ export class ShiftsController {
|
||||||
r.total_overtime_hrs.toFixed(2),
|
r.total_overtime_hrs.toFixed(2),
|
||||||
r.total_expenses.toFixed(2),
|
r.total_expenses.toFixed(2),
|
||||||
r.total_mileage.toFixed(2),
|
r.total_mileage.toFixed(2),
|
||||||
r.is_validated,
|
r.is_approved,
|
||||||
].join(',');
|
].join(',');
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export interface OverviewRow {
|
||||||
total_overtime_hrs: number;
|
total_overtime_hrs: number;
|
||||||
total_expenses: number;
|
total_expenses: number;
|
||||||
total_mileage: number;
|
total_mileage: number;
|
||||||
is_validated: boolean;
|
is_approved: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -168,7 +168,7 @@ export class ShiftsQueryService {
|
||||||
total_overtime_hrs: 0,
|
total_overtime_hrs: 0,
|
||||||
total_expenses: 0,
|
total_expenses: 0,
|
||||||
total_mileage: 0,
|
total_mileage: 0,
|
||||||
is_validated: false,
|
is_approved: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const hours = computeHours(shift.start_time, shift.end_time);
|
const hours = computeHours(shift.start_time, shift.end_time);
|
||||||
|
|
@ -200,7 +200,7 @@ export class ShiftsQueryService {
|
||||||
total_overtime_hrs: 0,
|
total_overtime_hrs: 0,
|
||||||
total_expenses: 0,
|
total_expenses: 0,
|
||||||
total_mileage: 0,
|
total_mileage: 0,
|
||||||
is_validated: false,
|
is_approved: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const amount = Number(exp.amount);
|
const amount = Number(exp.amount);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import { BadRequestException, Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common';
|
import { BadRequestException, Body, Controller, Delete, Get, Param, ParseBoolPipe, ParseIntPipe, Patch, Post, Query } from '@nestjs/common';
|
||||||
import { TimesheetsQueryService } from '../services/timesheets-query.service';
|
import { TimesheetsQueryService } from '../services/timesheets-query.service';
|
||||||
import { CreateTimesheetDto } from '../dtos/create-timesheet.dto';
|
import { CreateTimesheetDto, CreateWeekShiftsDto } from '../dtos/create-timesheet.dto';
|
||||||
import { Timesheets } from '@prisma/client';
|
import { Timesheets } from '@prisma/client';
|
||||||
import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto';
|
|
||||||
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
import { RolesAllowed } from "src/common/decorators/roles.decorators";
|
||||||
import { Roles as RoleEnum } from '.prisma/client';
|
import { Roles as RoleEnum } from '.prisma/client';
|
||||||
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { TimesheetsCommandService } from '../services/timesheets-command.service';
|
import { TimesheetsCommandService } from '../services/timesheets-command.service';
|
||||||
import { SearchTimesheetDto } from '../dtos/search-timesheet.dto';
|
|
||||||
import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
||||||
|
import { TimesheetDto } from '../dtos/overview-timesheet.dto';
|
||||||
|
|
||||||
@ApiTags('Timesheets')
|
@ApiTags('Timesheets')
|
||||||
@ApiBearerAuth('access-token')
|
@ApiBearerAuth('access-token')
|
||||||
|
|
@ -20,15 +19,6 @@ export class TimesheetsController {
|
||||||
private readonly timesheetsCommand: TimesheetsCommandService,
|
private readonly timesheetsCommand: TimesheetsCommandService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// @Post()
|
|
||||||
// //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
|
||||||
// @ApiOperation({ summary: 'Create timesheet' })
|
|
||||||
// @ApiResponse({ status: 201, description: 'Timesheet created', type: CreateTimesheetDto })
|
|
||||||
// @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
|
|
||||||
// create(@Body() dto: CreateTimesheetDto): Promise<Timesheets> {
|
|
||||||
// return this.timesheetsQuery.create(dto);
|
|
||||||
// }
|
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
async getPeriodByQuery(
|
async getPeriodByQuery(
|
||||||
|
|
@ -40,6 +30,26 @@ export class TimesheetsController {
|
||||||
return this.timesheetsQuery.findAll(year, period_no, email);
|
return this.timesheetsQuery.findAll(year, period_no, email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('/:email')
|
||||||
|
async getByEmail(
|
||||||
|
@Param('email') email: string,
|
||||||
|
@Query('offset') offset?: string,
|
||||||
|
): Promise<TimesheetDto> {
|
||||||
|
const week_offset = Number.isFinite(Number(offset)) ? Number(offset) : 0;
|
||||||
|
return this.timesheetsQuery.getTimesheetByEmail(email, week_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('shifts/:email')
|
||||||
|
async createTimesheetShifts(
|
||||||
|
@Param('email') email: string,
|
||||||
|
@Body() dto: CreateWeekShiftsDto,
|
||||||
|
@Query('offset') offset?: string,
|
||||||
|
): Promise<TimesheetDto> {
|
||||||
|
const week_offset = Number.isFinite(Number(offset)) ? Number(offset) : 0;
|
||||||
|
return this.timesheetsCommand.createWeekShiftsAndReturnOverview(email, dto.shifts, week_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Get(':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: 'Find timesheet' })
|
@ApiOperation({ summary: 'Find timesheet' })
|
||||||
|
|
@ -49,18 +59,6 @@ export class TimesheetsController {
|
||||||
return this.timesheetsQuery.findOne(id);
|
return this.timesheetsQuery.findOne(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
|
||||||
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
|
||||||
@ApiOperation({ summary: 'Update timesheet' })
|
|
||||||
@ApiResponse({ status: 201, description: 'Timesheet updated', type: CreateTimesheetDto })
|
|
||||||
@ApiResponse({ status: 400, description: 'Timesheet not found' })
|
|
||||||
update(
|
|
||||||
@Param('id', ParseIntPipe) id:number,
|
|
||||||
@Body() dto: UpdateTimesheetDto,
|
|
||||||
): Promise<Timesheets> {
|
|
||||||
return this.timesheetsQuery.update(id, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
// @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
// @RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.HR, RoleEnum.SUPERVISOR)
|
||||||
@ApiOperation({ summary: 'Delete timesheet' })
|
@ApiOperation({ summary: 'Delete timesheet' })
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,33 @@
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { Allow, IsBoolean, IsInt, IsOptional } from "class-validator";
|
import { IsArray, IsOptional, IsString, Length, Matches, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
export class CreateTimesheetDto {
|
export class CreateTimesheetDto {
|
||||||
@ApiProperty({
|
|
||||||
example: 1,
|
|
||||||
description: 'timesheet`s unique ID (auto-generated)',
|
|
||||||
})
|
|
||||||
@Allow()
|
|
||||||
id?: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
@IsString()
|
||||||
example: 426433,
|
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||||
description: 'employee`s ID number of linked timsheet',
|
date!: string;
|
||||||
})
|
|
||||||
@Type(() => Number)
|
|
||||||
@IsInt()
|
|
||||||
employee_id: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
@IsString()
|
||||||
example: true,
|
@Length(1,64)
|
||||||
description: 'Timesheet`s status approval',
|
type!: string;
|
||||||
})
|
|
||||||
@IsOptional()
|
@IsString()
|
||||||
@IsBoolean()
|
@Matches(/^\d{2}:\d{2}$/)
|
||||||
is_approved?: boolean;
|
start_time!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Matches(/^\d{2}:\d{2}$/)
|
||||||
|
end_time!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(0,512)
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateWeekShiftsDto {
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({each:true})
|
||||||
|
@Type(()=> CreateTimesheetDto)
|
||||||
|
shifts!: CreateTimesheetDto[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
src/modules/timesheets/dtos/overview-timesheet.dto.ts
Normal file
28
src/modules/timesheets/dtos/overview-timesheet.dto.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
export class TimesheetDto {
|
||||||
|
is_approved: boolean;
|
||||||
|
start_day: string;
|
||||||
|
end_day: string;
|
||||||
|
label: string;
|
||||||
|
shifts: ShiftsDto[];
|
||||||
|
expenses: ExpensesDto[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShiftsDto {
|
||||||
|
bank_type: string;
|
||||||
|
date: string;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
description: string;
|
||||||
|
is_approved: boolean;
|
||||||
|
is_remote: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExpensesDto {
|
||||||
|
bank_type: string;
|
||||||
|
date: string;
|
||||||
|
amount: number;
|
||||||
|
km: number;
|
||||||
|
description: string;
|
||||||
|
supervisor_comment: string;
|
||||||
|
is_approved: boolean;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
export class ShiftDto {
|
export class ShiftDto {
|
||||||
start: string;
|
date: string;
|
||||||
end : string;
|
type: string;
|
||||||
|
start_time: string;
|
||||||
|
end_time : string;
|
||||||
is_approved: boolean;
|
is_approved: boolean;
|
||||||
|
is_remote: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExpenseDto {
|
export class ExpenseDto {
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,24 @@
|
||||||
|
|
||||||
import { Injectable } from "@nestjs/common";
|
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { Prisma, Timesheets } from "@prisma/client";
|
import { Prisma, Timesheets } from "@prisma/client";
|
||||||
import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
import { BaseApprovalService } from "src/common/shared/base-approval.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { TimesheetsQueryService } from "./timesheets-query.service";
|
||||||
|
import { CreateTimesheetDto } from "../dtos/create-timesheet.dto";
|
||||||
|
import { TimesheetDto } from "../dtos/overview-timesheet.dto";
|
||||||
|
import { getWeekEnd, getWeekStart } from "src/common/utils/date-utils";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
||||||
constructor(prisma: PrismaService) {super(prisma);}
|
constructor(
|
||||||
|
prisma: PrismaService,
|
||||||
|
private readonly query: TimesheetsQueryService,
|
||||||
|
) {super(prisma);}
|
||||||
|
|
||||||
protected get delegate() {
|
protected get delegate() {
|
||||||
return this.prisma.timesheets;
|
return this.prisma.timesheets;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected delegateFor(transaction: Prisma.TransactionClient) {
|
protected delegateFor(transaction: Prisma.TransactionClient) {
|
||||||
return transaction.timesheets;
|
return transaction.timesheets;
|
||||||
}
|
}
|
||||||
|
|
@ -37,4 +45,84 @@ export class TimesheetsCommandService extends BaseApprovalService<Timesheets>{
|
||||||
return timesheet;
|
return timesheet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//create shifts within timesheet's week - employee overview functions
|
||||||
|
private parseISODate(iso: string): Date {
|
||||||
|
const [ y, m, d ] = iso.split('-').map(Number);
|
||||||
|
return new Date(y, (m ?? 1) - 1, d ?? 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseHHmm(t: string): Date {
|
||||||
|
const [ hh, mm ] = t.split(':').map(Number);
|
||||||
|
return new Date(1970, 0, 1, hh || 0, mm || 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createWeekShiftsAndReturnOverview(
|
||||||
|
email:string,
|
||||||
|
shifts: CreateTimesheetDto[],
|
||||||
|
week_offset = 0,
|
||||||
|
): Promise<TimesheetDto> {
|
||||||
|
|
||||||
|
//match user's email with email
|
||||||
|
const user = await this.prisma.users.findUnique({
|
||||||
|
where: { email },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if(!user) throw new NotFoundException(`user with email ${email} not found`);
|
||||||
|
|
||||||
|
//fetchs employee matchint user's email
|
||||||
|
const employee = await this.prisma.employees.findFirst({
|
||||||
|
where: { user_id: user?.id },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if(!employee) throw new NotFoundException(`employee for ${ email } not found`);
|
||||||
|
|
||||||
|
//insure that the week starts on sunday and finishes on saturday
|
||||||
|
const base = new Date();
|
||||||
|
base.setDate(base.getDate() + week_offset * 7);
|
||||||
|
const start_week = getWeekStart(base, 0);
|
||||||
|
const end_week = getWeekEnd(start_week);
|
||||||
|
|
||||||
|
const timesheet = await this.prisma.timesheets.upsert({
|
||||||
|
where: {
|
||||||
|
employee_id_start_date: {
|
||||||
|
employee_id: employee.id,
|
||||||
|
start_date: start_week,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
employee_id: employee.id,
|
||||||
|
start_date: start_week,
|
||||||
|
is_approved: false,
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
//validations and insertions
|
||||||
|
for(const shift of shifts) {
|
||||||
|
const date = this.parseISODate(shift.date);
|
||||||
|
if (date < start_week || date > end_week) throw new BadRequestException(`date ${shift.date} not in current week`);
|
||||||
|
|
||||||
|
const bank_code = await this.prisma.bankCodes.findFirst({
|
||||||
|
where: { type: shift.type },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if(!bank_code) throw new BadRequestException(`Invalid bank_code type: ${shift.type}`);
|
||||||
|
|
||||||
|
await this.prisma.shifts.create({
|
||||||
|
data: {
|
||||||
|
timesheet_id: timesheet.id,
|
||||||
|
bank_code_id: bank_code.id,
|
||||||
|
date: date,
|
||||||
|
start_time: this.parseHHmm(shift.start_time),
|
||||||
|
end_time: this.parseHHmm(shift.end_time),
|
||||||
|
description: shift.description ?? null,
|
||||||
|
is_approved: false,
|
||||||
|
is_remote: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.query.getTimesheetByEmail(email, week_offset);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { CreateTimesheetDto } from '../dtos/create-timesheet.dto';
|
|
||||||
import { Timesheets, TimesheetsArchive } from '@prisma/client';
|
import { Timesheets, TimesheetsArchive } from '@prisma/client';
|
||||||
import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto';
|
import { UpdateTimesheetDto } from '../dtos/update-timesheet.dto';
|
||||||
import { OvertimeService } from 'src/modules/business-logics/services/overtime.service';
|
import { OvertimeService } from 'src/modules/business-logics/services/overtime.service';
|
||||||
import { computeHours } from 'src/common/utils/date-utils';
|
import { computeHours, formatDateISO, getCurrentWeek, getWeekEnd, getWeekStart } from 'src/common/utils/date-utils';
|
||||||
import { TimesheetPeriodDto } from '../dtos/timesheet-period.dto';
|
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 type { ShiftRow, ExpenseRow } from '../utils/timesheet.helpers';
|
||||||
|
import { TimesheetDto } from '../dtos/overview-timesheet.dto';
|
||||||
|
import { CreateWeekShiftsDto } from '../dtos/create-timesheet.dto';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -17,17 +18,6 @@ export class TimesheetsQueryService {
|
||||||
private readonly overtime: OvertimeService,
|
private readonly overtime: OvertimeService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// async create(dto : CreateTimesheetDto): Promise<Timesheets> {
|
|
||||||
// const { employee_id, is_approved } = dto;
|
|
||||||
// return this.prisma.timesheets.create({
|
|
||||||
// data: { employee_id, is_approved: is_approved ?? false },
|
|
||||||
// include: {
|
|
||||||
// employee: { include: { user: true }
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
async findAll(year: number, period_no: number, email: string): Promise<TimesheetPeriodDto> {
|
async findAll(year: number, period_no: number, email: string): Promise<TimesheetPeriodDto> {
|
||||||
//finds the employee
|
//finds the employee
|
||||||
const employee = await this.prisma.employees.findFirst({
|
const employee = await this.prisma.employees.findFirst({
|
||||||
|
|
@ -56,6 +46,7 @@ export class TimesheetsQueryService {
|
||||||
start_time: true,
|
start_time: true,
|
||||||
end_time: true,
|
end_time: true,
|
||||||
is_approved: true,
|
is_approved: true,
|
||||||
|
is_remote: true,
|
||||||
bank_code: { select: { type: true } },
|
bank_code: { select: { type: true } },
|
||||||
},
|
},
|
||||||
orderBy:[ { date:'asc'}, { start_time: 'asc'} ],
|
orderBy:[ { date:'asc'}, { start_time: 'asc'} ],
|
||||||
|
|
@ -75,8 +66,9 @@ export class TimesheetsQueryService {
|
||||||
orderBy: { date: 'asc' },
|
orderBy: { date: 'asc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const to_num = (value: any) => typeof value.toNumber === 'function' ? value.toNumber() : Number(value);
|
const to_num = (value: any) => value && typeof (value as any).toNumber === 'function'
|
||||||
|
? (value as any).toNumber()
|
||||||
|
: Number(value);
|
||||||
|
|
||||||
// data mapping
|
// data mapping
|
||||||
const shifts: ShiftRow[] = raw_shifts.map(shift => ({
|
const shifts: ShiftRow[] = raw_shifts.map(shift => ({
|
||||||
|
|
@ -85,12 +77,12 @@ export class TimesheetsQueryService {
|
||||||
end_time: shift.end_time,
|
end_time: shift.end_time,
|
||||||
type: String(shift.bank_code?.type ?? '').toUpperCase(),
|
type: String(shift.bank_code?.type ?? '').toUpperCase(),
|
||||||
is_approved: shift.is_approved ?? true,
|
is_approved: shift.is_approved ?? true,
|
||||||
|
is_remote: shift.is_remote ?? true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const expenses: ExpenseRow[] = raw_expenses.map(expense => ({
|
const expenses: ExpenseRow[] = raw_expenses.map(expense => ({
|
||||||
date: expense.date,
|
date: expense.date,
|
||||||
amount: typeof (expense.amount as any)?.to_num === 'function' ?
|
amount: to_num(expense.amount),
|
||||||
(expense.amount as any).to_num() : Number(expense.amount),
|
|
||||||
type: String(expense.bank_code?.type ?? '').toUpperCase(),
|
type: String(expense.bank_code?.type ?? '').toUpperCase(),
|
||||||
is_approved: expense.is_approved ?? true,
|
is_approved: expense.is_approved ?? true,
|
||||||
}));
|
}));
|
||||||
|
|
@ -98,6 +90,103 @@ export class TimesheetsQueryService {
|
||||||
return buildPeriod(period.period_start, period.period_end, shifts , expenses);
|
return buildPeriod(period.period_start, period.period_end, shifts , expenses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTimesheetByEmail(email: string, week_offset = 0): Promise<TimesheetDto> {
|
||||||
|
|
||||||
|
//fetch user related to email
|
||||||
|
const user = await this.prisma.users.findUnique({
|
||||||
|
where: { email },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if(!user) throw new NotFoundException(`user with email ${email} not found`);
|
||||||
|
|
||||||
|
//fetch employee_id matching the email
|
||||||
|
const employee = await this.prisma.employees.findFirst({
|
||||||
|
where: { user_id: user.id },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if(!employee) throw new NotFoundException(`Employee with email: ${email} not found`);
|
||||||
|
|
||||||
|
//sets current week Sunday -> Saturday
|
||||||
|
const base = new Date();
|
||||||
|
const offset = new Date(base);
|
||||||
|
offset.setDate(offset.getDate() + (week_offset * 7));
|
||||||
|
|
||||||
|
const start_date_week = getWeekStart(offset, 0);
|
||||||
|
const end_date_week = getWeekEnd(start_date_week);
|
||||||
|
const start_day = formatDateISO(start_date_week);
|
||||||
|
const end_day = formatDateISO(end_date_week);
|
||||||
|
|
||||||
|
//build the label MM/DD/YYYY.MM/DD.YYYY
|
||||||
|
const mm_dd = (date: Date) => `${String(date.getFullYear())}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2,'0')}`;
|
||||||
|
const label = `${mm_dd(start_date_week)}.${mm_dd(end_date_week)}`;
|
||||||
|
|
||||||
|
//fetch timesheet shifts and expenses
|
||||||
|
const timesheet = await this.prisma.timesheets.findUnique({
|
||||||
|
where: {
|
||||||
|
employee_id_start_date: {
|
||||||
|
employee_id: employee.id,
|
||||||
|
start_date: start_date_week,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
shift: {
|
||||||
|
include: { bank_code: true },
|
||||||
|
orderBy: [{ date: 'asc'}, { start_time: 'asc'}],
|
||||||
|
},
|
||||||
|
expense: {
|
||||||
|
include: { bank_code: true },
|
||||||
|
orderBy: [{date: 'asc'}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
//returns an empty timesheet if not found
|
||||||
|
if(!timesheet) {
|
||||||
|
return {
|
||||||
|
is_approved: false,
|
||||||
|
start_day,
|
||||||
|
end_day,
|
||||||
|
label,
|
||||||
|
shifts:[],
|
||||||
|
expenses: [],
|
||||||
|
} as TimesheetDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
//small helper to format hours:minutes
|
||||||
|
const to_HH_mm = (date: Date) => date.toISOString().slice(11, 16);
|
||||||
|
|
||||||
|
//maps all shifts of selected timesheet
|
||||||
|
const shifts = timesheet.shift.map((sft) => ({
|
||||||
|
bank_type: sft.bank_code?.type ?? '',
|
||||||
|
date: formatDateISO(sft.date),
|
||||||
|
start_time: to_HH_mm(sft.start_time),
|
||||||
|
end_time: to_HH_mm(sft.end_time),
|
||||||
|
description: sft.description ?? '',
|
||||||
|
is_approved: sft.is_approved ?? false,
|
||||||
|
is_remote: sft.is_remote ?? false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
//maps all expenses of selected timsheet
|
||||||
|
const expenses = timesheet.expense.map((exp) => ({
|
||||||
|
bank_type: exp.bank_code?.type ?? '',
|
||||||
|
date: formatDateISO(exp.date),
|
||||||
|
amount: Number(exp.amount) || 0,
|
||||||
|
km: 0,
|
||||||
|
description: exp.description ?? '',
|
||||||
|
supervisor_comment: exp.supervisor_comment ?? '',
|
||||||
|
is_approved: exp.is_approved ?? false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
is_approved: timesheet.is_approved,
|
||||||
|
start_day,
|
||||||
|
end_day,
|
||||||
|
label,
|
||||||
|
shifts,
|
||||||
|
expenses,
|
||||||
|
} as TimesheetDto;
|
||||||
|
}
|
||||||
|
|
||||||
async findOne(id: number): Promise<any> {
|
async findOne(id: number): Promise<any> {
|
||||||
const timesheet = await this.prisma.timesheets.findUnique({
|
const timesheet = await this.prisma.timesheets.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
|
@ -126,20 +215,6 @@ export class TimesheetsQueryService {
|
||||||
return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours };
|
return { ...timesheet, shift: detailedShifts, weeklyOvertimeHours };
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: number, dto:UpdateTimesheetDto): Promise<Timesheets> {
|
|
||||||
await this.findOne(id);
|
|
||||||
const { employee_id, is_approved } = dto;
|
|
||||||
return this.prisma.timesheets.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
...(employee_id !== undefined && { employee_id }),
|
|
||||||
...(is_approved !== undefined && { is_approved }),
|
|
||||||
},
|
|
||||||
include: { employee: { include: { user: true } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async remove(id: number): Promise<Timesheets> {
|
async remove(id: number): Promise<Timesheets> {
|
||||||
await this.findOne(id);
|
await this.findOne(id);
|
||||||
return this.prisma.timesheets.delete({ where: { id } });
|
return this.prisma.timesheets.delete({ where: { id } });
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { toDateString } from "src/modules/pay-periods/utils/pay-year.util";
|
||||||
import { DayExpensesDto, DetailedShifts, ShiftDto, TimesheetPeriodDto, WeekDto } from "../dtos/timesheet-period.dto";
|
import { DayExpensesDto, DetailedShifts, ShiftDto, TimesheetPeriodDto, WeekDto } from "../dtos/timesheet-period.dto";
|
||||||
|
|
||||||
//makes the strings indexes for arrays
|
//makes the strings indexes for arrays
|
||||||
|
|
@ -33,8 +34,8 @@ const EXPENSE_TYPES = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
//DB line types
|
//DB line types
|
||||||
export type ShiftRow = { date: Date; start_time: Date; end_time: Date; is_approved?: boolean; type: string };
|
export type ShiftRow = { date: Date; start_time: Date; end_time: Date; is_approved?: boolean; is_remote: boolean; type: string };
|
||||||
export type ExpenseRow = { date: Date; amount: number; type: string; is_approved?: boolean };
|
export type ExpenseRow = { date: Date; amount: number; type: string; is_approved?: boolean; };
|
||||||
|
|
||||||
//helper functions
|
//helper functions
|
||||||
export function toUTCDateOnly(date: Date | string): Date {
|
export function toUTCDateOnly(date: Date | string): Date {
|
||||||
|
|
@ -154,9 +155,12 @@ export function buildWeek(
|
||||||
for (const shift of week_shifts) {
|
for (const shift of week_shifts) {
|
||||||
const key = dayKeyFromDate(shift.date, true);
|
const key = dayKeyFromDate(shift.date, true);
|
||||||
week.shifts[key].shifts.push({
|
week.shifts[key].shifts.push({
|
||||||
start: toTimeString(shift.start_time),
|
date: toDateString(shift.date),
|
||||||
end: toTimeString(shift.end_time),
|
type: shift.type,
|
||||||
|
start_time: toTimeString(shift.start_time),
|
||||||
|
end_time: toTimeString(shift.end_time),
|
||||||
is_approved: shift.is_approved ?? true,
|
is_approved: shift.is_approved ?? true,
|
||||||
|
is_remote: shift.is_remote,
|
||||||
} as ShiftDto);
|
} as ShiftDto);
|
||||||
|
|
||||||
day_times[key].push({ start: shift.start_time, end: shift.end_time});
|
day_times[key].push({ start: shift.start_time, end: shift.end_time});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user