law-common 14.0.3-beta.0 → 14.0.3-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/entities/interface/timesheet.entity.interface.d.ts +2 -1
- package/dist/src/entities/model/timesheet.entity.model.d.ts +31 -2
- package/dist/src/entities/model/timesheet.entity.model.js +77 -30
- package/dist/src/utils/date.util.d.ts +19 -0
- package/dist/src/utils/date.util.js +29 -0
- package/package.json +3 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TimesheetClassificationStatusEnum } from "../enums/timesheet_classification_status_enum";
|
|
2
2
|
import { TimesheetStatusEnum } from "../enums/timesheet_status_enum";
|
|
3
3
|
import { IEntityAuditColumn } from "./entity-audit-columns.interface";
|
|
4
|
+
import { Nullable } from "./entity.utils.interface";
|
|
4
5
|
export interface ITimesheetEntity extends IEntityAuditColumn {
|
|
5
6
|
id: number;
|
|
6
7
|
userId: number;
|
|
@@ -15,6 +16,6 @@ export interface ITimesheetEntity extends IEntityAuditColumn {
|
|
|
15
16
|
billedDuration?: number;
|
|
16
17
|
notBilledDuration?: number;
|
|
17
18
|
classificationStatus: TimesheetClassificationStatusEnum;
|
|
18
|
-
classifiedBy?: number
|
|
19
|
+
classifiedBy?: Nullable<number>;
|
|
19
20
|
}
|
|
20
21
|
export type PendingApprovalStatus = TimesheetStatusEnum.CREATE_APPROVAL_PENDING | TimesheetStatusEnum.UPDATE_APPROVAL_PENDING | TimesheetStatusEnum.DELETE_APPROVAL_PENDING | TimesheetStatusEnum.RESOLVED_REJECTED;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ITimesheetFlowContextData, ITimesheetUpdateDto } from "../../api";
|
|
2
2
|
import { CurrencyEnum } from "../../enums";
|
|
3
3
|
import { IClassificationWindowConfig, ITimesheetActionItem, SortableTimesheet } from "../../model/entities/interface/timesheet.model.interface";
|
|
4
4
|
import { TimesheetClassificationStatusEnum } from "../enums/timesheet_classification_status_enum";
|
|
@@ -51,11 +51,33 @@ export declare class TimesheetEntityModel extends BaseEntityModel<EntityEnum.TIM
|
|
|
51
51
|
get latestStatusFromHistory(): TimesheetStatusEnum | undefined;
|
|
52
52
|
getDateCodeDay(): number;
|
|
53
53
|
getClassificationDeadline(config: IClassificationWindowConfig): Date;
|
|
54
|
+
/**
|
|
55
|
+
* IST point-in-time AUTO-CLASSIFY trigger. Returns the classification window
|
|
56
|
+
* whose `endDay + bufferDays + 1 == today` (IST) — i.e. it fires on a SINGLE day
|
|
57
|
+
* per window (the auto-classify cron runs at 01:00 IST each morning).
|
|
58
|
+
* Trailing window (endDay === null) evaluates against the PREVIOUS month's EOM.
|
|
59
|
+
*
|
|
60
|
+
* "today" is `getTodayInIST()` (IST calendar day). Concrete per getTodayInIST()
|
|
61
|
+
* rollover rule: endDay 13 + buffer 3 -> trigger IST day 17; that 01:00 IST IS
|
|
62
|
+
* UTC 19:30 the PREVIOUS day, so a UTC-default container reads getDate() = 16 ->
|
|
63
|
+
* 17 !== 16 -> returns null -> auto-classification silently no-ops.
|
|
64
|
+
*/
|
|
54
65
|
static getTimesheetAutoClassificationDateCodeRange(config: IClassificationWindowConfig): {
|
|
55
66
|
windowStart: string;
|
|
56
67
|
windowEnd: string;
|
|
57
68
|
} | null;
|
|
58
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Resolves the classification window that contains `now` (used by the reminder).
|
|
71
|
+
* `now` defaults to the IST calendar day (getTodayInIST()).
|
|
72
|
+
*
|
|
73
|
+
* Boundary examples per getTodayInIST() rollover rule (IST < 05:30 -> UTC prev day):
|
|
74
|
+
* IST day 1 00:xx -> UTC last day of the PREVIOUS month
|
|
75
|
+
* (a UTC clock would skip the day-1-3 spillover onto the
|
|
76
|
+
* prev-month 21-EOM window);
|
|
77
|
+
* IST day 21 00:30 -> UTC day 20
|
|
78
|
+
* (a UTC clock would miss the current-month 21-EOM tail).
|
|
79
|
+
*/
|
|
80
|
+
static getActiveClassificationWindowRange(config: IClassificationWindowConfig, now?: Date): {
|
|
59
81
|
windowStart: string;
|
|
60
82
|
windowEnd: string;
|
|
61
83
|
} | null;
|
|
@@ -63,5 +85,12 @@ export declare class TimesheetEntityModel extends BaseEntityModel<EntityEnum.TIM
|
|
|
63
85
|
startDay: number;
|
|
64
86
|
endDay: Nullable<number>;
|
|
65
87
|
}>;
|
|
88
|
+
/**
|
|
89
|
+
* Fields on a timesheet that, when changed via an UPDATE, reset the timesheet's
|
|
90
|
+
* `classificationStatus` back to `PENDING`. Shared by backend (toUpdateDto) and frontend
|
|
91
|
+
* (edit warning). `billingId` intentionally NOT included — a billing change does not reset
|
|
92
|
+
* classification. Anything not listed here is by definition "non-reset".
|
|
93
|
+
*/
|
|
94
|
+
static timesheetClassificationResetFieldsSet(): string[];
|
|
66
95
|
static sortByProjectAndDesignation(data: SortableTimesheet[], designations: IDesignationEntity[]): SortableTimesheet[];
|
|
67
96
|
}
|
|
@@ -84,8 +84,10 @@ class TimesheetEntityModel extends base_entity_model_1.BaseEntityModel {
|
|
|
84
84
|
return false;
|
|
85
85
|
if (actionKey === timesheet_action_enum_1.TimesheetActionEnum.CLASSIFY)
|
|
86
86
|
return false;
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
// LW-842: allow UPDATE regardless of classification status — the backend now resets
|
|
88
|
+
// classificationStatus to PENDING in toUpdateDto when a reset-field changes.
|
|
89
|
+
// if (actionKey === TimesheetActionEnum.UPDATE && this.classificationStatus && this.classificationStatus !== TimesheetClassificationStatusEnum.PENDING)
|
|
90
|
+
// return false;
|
|
89
91
|
return true;
|
|
90
92
|
})
|
|
91
93
|
.map(([actionKey, actionConfig]) => {
|
|
@@ -225,10 +227,21 @@ class TimesheetEntityModel extends base_entity_model_1.BaseEntityModel {
|
|
|
225
227
|
d.setHours(23, 59, 59, 999);
|
|
226
228
|
return d;
|
|
227
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* IST point-in-time AUTO-CLASSIFY trigger. Returns the classification window
|
|
232
|
+
* whose `endDay + bufferDays + 1 == today` (IST) — i.e. it fires on a SINGLE day
|
|
233
|
+
* per window (the auto-classify cron runs at 01:00 IST each morning).
|
|
234
|
+
* Trailing window (endDay === null) evaluates against the PREVIOUS month's EOM.
|
|
235
|
+
*
|
|
236
|
+
* "today" is `getTodayInIST()` (IST calendar day). Concrete per getTodayInIST()
|
|
237
|
+
* rollover rule: endDay 13 + buffer 3 -> trigger IST day 17; that 01:00 IST IS
|
|
238
|
+
* UTC 19:30 the PREVIOUS day, so a UTC-default container reads getDate() = 16 ->
|
|
239
|
+
* 17 !== 16 -> returns null -> auto-classification silently no-ops.
|
|
240
|
+
*/
|
|
228
241
|
static getTimesheetAutoClassificationDateCodeRange(config) {
|
|
229
242
|
var _a;
|
|
230
243
|
const { bufferDays, windows } = config;
|
|
231
|
-
const today =
|
|
244
|
+
const today = (0, utils_1.getTodayInIST)();
|
|
232
245
|
for (const window of windows) {
|
|
233
246
|
// Last window (endDay === null) belongs to the previous month
|
|
234
247
|
const targetDate = window.endDay === null ? (0, date_fns_1.subMonths)(today, 1) : today;
|
|
@@ -245,35 +258,54 @@ class TimesheetEntityModel extends base_entity_model_1.BaseEntityModel {
|
|
|
245
258
|
}
|
|
246
259
|
return null;
|
|
247
260
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Resolves the classification window that contains `now` (used by the reminder).
|
|
263
|
+
* `now` defaults to the IST calendar day (getTodayInIST()).
|
|
264
|
+
*
|
|
265
|
+
* Boundary examples per getTodayInIST() rollover rule (IST < 05:30 -> UTC prev day):
|
|
266
|
+
* IST day 1 00:xx -> UTC last day of the PREVIOUS month
|
|
267
|
+
* (a UTC clock would skip the day-1-3 spillover onto the
|
|
268
|
+
* prev-month 21-EOM window);
|
|
269
|
+
* IST day 21 00:30 -> UTC day 20
|
|
270
|
+
* (a UTC clock would miss the current-month 21-EOM tail).
|
|
271
|
+
*/
|
|
272
|
+
static getActiveClassificationWindowRange(config, now = (0, utils_1.getTodayInIST)()) {
|
|
273
|
+
const { bufferDays, windows } = config; // bufferDays = 3
|
|
274
|
+
const year = now.getFullYear(); // now = Oct 1 2026 -> year = 2026
|
|
275
|
+
const month = now.getMonth(); // month = 9 (Oct)
|
|
276
|
+
// Spillover check: does `now` still fall within the PREVIOUS month's trailing
|
|
277
|
+
// window (21-EOM) plus its buffer? If so, that window is still pending
|
|
278
|
+
// classification and takes priority over the current month's own windows.
|
|
279
|
+
const lastWindow = windows[windows.length - 1]; // lastWindow = { startDay: 21, endDay: null }
|
|
280
|
+
if ((lastWindow === null || lastWindow === void 0 ? void 0 : lastWindow.endDay) === null) {
|
|
281
|
+
const prevMonthDate = (0, date_fns_1.subMonths)(new Date(year, month, 1), 1); // prevMonthDate = Sep 1 2026
|
|
282
|
+
const prevYear = prevMonthDate.getFullYear(); // prevYear = 2026
|
|
283
|
+
const prevMonth = prevMonthDate.getMonth(); // prevMonth = 8 (Sep)
|
|
284
|
+
const prevMonthEom = (0, date_fns_1.lastDayOfMonth)(prevMonthDate).getDate(); // prevMonthEom = 30
|
|
285
|
+
const prevWindowStartDate = new Date(prevYear, prevMonth, lastWindow.startDay); // Sep 21 2026
|
|
286
|
+
prevWindowStartDate.setHours(0, 0, 0, 0); // Sep 21 2026 00:00:00.000
|
|
287
|
+
const prevWindowEndDate = new Date(prevYear, prevMonth, prevMonthEom); // Sep 30 2026
|
|
288
|
+
prevWindowEndDate.setHours(23, 59, 59, 999); // Sep 30 2026 23:59:59.999
|
|
289
|
+
const prevWindowEndDateWithBuffer = (0, date_fns_1.addDays)(new Date(prevYear, prevMonth, prevMonthEom), bufferDays); // Sep30 + 3 -> Oct 3 2026
|
|
290
|
+
prevWindowEndDateWithBuffer.setHours(23, 59, 59, 999); // Oct 3 2026 23:59:59.999
|
|
291
|
+
if (now.getTime() >= prevWindowStartDate.getTime() && now.getTime() <= prevWindowEndDateWithBuffer.getTime()) {
|
|
292
|
+
// Oct1 >= Sep21 -> true, Oct1 <= Oct3 -> true, so this branch matches and returns here
|
|
293
|
+
return {
|
|
294
|
+
windowStart: (0, date_fns_1.format)(prevWindowStartDate, "yyyyMMdd"), // "20260921"
|
|
295
|
+
windowEnd: (0, date_fns_1.format)(prevWindowEndDate, "yyyyMMdd"), // "20260930"
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
255
299
|
for (const window of windows) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const windowEndDay = actualEndDay;
|
|
263
|
-
// check if today falls within this window's active period
|
|
264
|
-
// active period = from windowStart until triggerDate (endDay + bufferDays)
|
|
265
|
-
const triggerDate = (0, date_fns_1.addDays)(new Date(targetYear, targetMonth, actualEndDay), bufferDays);
|
|
266
|
-
const triggerDay = triggerDate.getDate();
|
|
267
|
-
const triggerMonth = triggerDate.getMonth();
|
|
268
|
-
const triggerYear = triggerDate.getFullYear();
|
|
269
|
-
const todayTime = today.getTime();
|
|
270
|
-
const windowOpenDate = new Date(targetYear, targetMonth, windowStartDay);
|
|
271
|
-
const windowCloseDate = new Date(triggerYear, triggerMonth, triggerDay);
|
|
272
|
-
windowCloseDate.setHours(23, 59, 59, 999);
|
|
273
|
-
if (todayTime >= windowOpenDate.getTime() && todayTime <= windowCloseDate.getTime()) {
|
|
300
|
+
const windowStartDate = new Date(year, month, window.startDay);
|
|
301
|
+
windowStartDate.setHours(0, 0, 0, 0);
|
|
302
|
+
const windowEndDate = window.endDay === null ? (0, date_fns_1.lastDayOfMonth)(new Date(year, month, 1)) : new Date(year, month, window.endDay);
|
|
303
|
+
windowEndDate.setHours(23, 59, 59, 999);
|
|
304
|
+
if (now.getTime() >= windowStartDate.getTime() && now.getTime() <= windowEndDate.getTime()) {
|
|
305
|
+
// not reached for Oct 1 -- spillover branch already returned above
|
|
274
306
|
return {
|
|
275
|
-
windowStart: (0, date_fns_1.format)(
|
|
276
|
-
windowEnd: (0, date_fns_1.format)(
|
|
307
|
+
windowStart: (0, date_fns_1.format)(windowStartDate, "yyyyMMdd"),
|
|
308
|
+
windowEnd: (0, date_fns_1.format)(windowEndDate, "yyyyMMdd"),
|
|
277
309
|
};
|
|
278
310
|
}
|
|
279
311
|
}
|
|
@@ -288,6 +320,21 @@ class TimesheetEntityModel extends base_entity_model_1.BaseEntityModel {
|
|
|
288
320
|
};
|
|
289
321
|
});
|
|
290
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Fields on a timesheet that, when changed via an UPDATE, reset the timesheet's
|
|
325
|
+
* `classificationStatus` back to `PENDING`. Shared by backend (toUpdateDto) and frontend
|
|
326
|
+
* (edit warning). `billingId` intentionally NOT included — a billing change does not reset
|
|
327
|
+
* classification. Anything not listed here is by definition "non-reset".
|
|
328
|
+
*/
|
|
329
|
+
static timesheetClassificationResetFieldsSet() {
|
|
330
|
+
return [
|
|
331
|
+
"userId",
|
|
332
|
+
"projectId",
|
|
333
|
+
"dateCode",
|
|
334
|
+
"totalDuration",
|
|
335
|
+
// "task", "description",
|
|
336
|
+
];
|
|
337
|
+
}
|
|
291
338
|
static sortByProjectAndDesignation(data, designations) {
|
|
292
339
|
const priorityMap = new Map();
|
|
293
340
|
designations.forEach((d) => {
|
|
@@ -1,3 +1,22 @@
|
|
|
1
1
|
export declare function compareDates(recordDate: string, buffer: number): boolean;
|
|
2
2
|
export declare function epochToDateStringFormat(epoch: number, format?: string): string;
|
|
3
3
|
export declare function convertDateCodeToFormat(dateString: string, toFormat: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Current calendar date resolved in Asia/Kolkata (IST).
|
|
6
|
+
*
|
|
7
|
+
* TIMEZONE RULE: IST = UTC + 5:30 (fixed offset, no DST).
|
|
8
|
+
* Converting IST -> UTC: subtract 5:30.
|
|
9
|
+
* - If IST time < 05:30 -> UTC date is the PREVIOUS calendar day.
|
|
10
|
+
* - If IST time >= 05:30 -> UTC date is the SAME calendar day.
|
|
11
|
+
*
|
|
12
|
+
* Example: IST 01:00 -> UTC 19:30 (prev day).
|
|
13
|
+
*
|
|
14
|
+
* Why this matters: the production container (node:20.10-alpine) sets no TZ,
|
|
15
|
+
* so it defaults to UTC. A bare `new Date().getDate()` at 01:00 IST would read
|
|
16
|
+
* UTC's date (still "yesterday"), causing classification jobs to target the
|
|
17
|
+
* wrong day or silently miss their trigger window.
|
|
18
|
+
*
|
|
19
|
+
* This function returns a Date whose local getFullYear()/getMonth()/getDate()
|
|
20
|
+
* always equal the IST calendar date, regardless of container timezone.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getTodayInIST(): Date;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.compareDates = compareDates;
|
|
4
4
|
exports.epochToDateStringFormat = epochToDateStringFormat;
|
|
5
5
|
exports.convertDateCodeToFormat = convertDateCodeToFormat;
|
|
6
|
+
exports.getTodayInIST = getTodayInIST;
|
|
6
7
|
const date_fns_1 = require("date-fns");
|
|
7
8
|
const date_code_model_util_1 = require("./models/date-code.model.util");
|
|
8
9
|
function compareDates(recordDate, buffer) {
|
|
@@ -16,3 +17,31 @@ function convertDateCodeToFormat(dateString, toFormat) {
|
|
|
16
17
|
const dateObj = new date_code_model_util_1.DateCodeModel(dateString);
|
|
17
18
|
return dateObj.toStringFormat(toFormat);
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Current calendar date resolved in Asia/Kolkata (IST).
|
|
22
|
+
*
|
|
23
|
+
* TIMEZONE RULE: IST = UTC + 5:30 (fixed offset, no DST).
|
|
24
|
+
* Converting IST -> UTC: subtract 5:30.
|
|
25
|
+
* - If IST time < 05:30 -> UTC date is the PREVIOUS calendar day.
|
|
26
|
+
* - If IST time >= 05:30 -> UTC date is the SAME calendar day.
|
|
27
|
+
*
|
|
28
|
+
* Example: IST 01:00 -> UTC 19:30 (prev day).
|
|
29
|
+
*
|
|
30
|
+
* Why this matters: the production container (node:20.10-alpine) sets no TZ,
|
|
31
|
+
* so it defaults to UTC. A bare `new Date().getDate()` at 01:00 IST would read
|
|
32
|
+
* UTC's date (still "yesterday"), causing classification jobs to target the
|
|
33
|
+
* wrong day or silently miss their trigger window.
|
|
34
|
+
*
|
|
35
|
+
* This function returns a Date whose local getFullYear()/getMonth()/getDate()
|
|
36
|
+
* always equal the IST calendar date, regardless of container timezone.
|
|
37
|
+
*/
|
|
38
|
+
function getTodayInIST() {
|
|
39
|
+
const parts = new Intl.DateTimeFormat("en-GB", {
|
|
40
|
+
timeZone: "Asia/Kolkata",
|
|
41
|
+
year: "numeric",
|
|
42
|
+
month: "numeric",
|
|
43
|
+
day: "numeric",
|
|
44
|
+
}).formatToParts(new Date());
|
|
45
|
+
const get = (type) => { var _a, _b; return Number((_b = (_a = parts.find((part) => part.type === type)) === null || _a === void 0 ? void 0 : _a.value) !== null && _b !== void 0 ? _b : 0); };
|
|
46
|
+
return new Date(get("year"), get("month") - 1, get("day"));
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "law-common",
|
|
3
|
-
"version": "14.0.3-beta.
|
|
3
|
+
"version": "14.0.3-beta.10",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"script:publish:install:patch": "node scripts/publish-and-install.js patch",
|
|
36
36
|
"script:publish:install:minor": "node scripts/publish-and-install.js minor",
|
|
37
37
|
"script:publish:install:major": "node scripts/publish-and-install.js major",
|
|
38
|
-
"build:link": "bash scripts/build-link.sh"
|
|
38
|
+
"build:link": "bash scripts/build-link.sh",
|
|
39
|
+
"script:deploy:prod": "bash scripts/deploy-prod.sh"
|
|
39
40
|
},
|
|
40
41
|
"keywords": [],
|
|
41
42
|
"author": "",
|