@platform-modules/foreign-ministry 1.3.333 → 1.3.334

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.
Files changed (84) hide show
  1. package/.env +5 -10
  2. package/dist/helpers/services-notification-config.helper.js +2 -6
  3. package/dist/models/CityMasterModel.d.ts +1 -0
  4. package/dist/models/CityMasterModel.js +4 -0
  5. package/dist/models/DepartmentsModel.d.ts +1 -0
  6. package/dist/models/DepartmentsModel.js +4 -0
  7. package/dist/models/DiplomaticTitlesMasterModel.d.ts +1 -0
  8. package/dist/models/DiplomaticTitlesMasterModel.js +4 -0
  9. package/dist/models/GroupNamesModel.d.ts +1 -0
  10. package/dist/models/GroupNamesModel.js +4 -0
  11. package/dist/models/LocationModel.d.ts +1 -0
  12. package/dist/models/LocationModel.js +4 -0
  13. package/dist/models/SectionModel.d.ts +1 -0
  14. package/dist/models/SectionModel.js +4 -0
  15. package/dist/models/SlaApprovalsViewModel.js +47 -47
  16. package/dist/models/SlaMyRequestsViewModel.js +52 -52
  17. package/dist/models/role.d.ts +2 -1
  18. package/dist/models/role.js +7 -2
  19. package/package.json +35 -35
  20. package/scripts/check-column-types.js +38 -38
  21. package/scripts/check-service-tables.js +42 -42
  22. package/scripts/sync-sla-reports-sql.js +95 -95
  23. package/sql/sla-reports-sync.manifest.json +7 -7
  24. package/sql/sla_reports_admin_procedures.sql +283 -283
  25. package/sql/sla_reports_approvals_workbook.sql +383 -383
  26. package/sql/sla_reports_procedures.sql +874 -874
  27. package/sql/vw_sla_approvals.sql +108 -108
  28. package/sql/vw_sla_my_requests.sql +55 -55
  29. package/src/data-source.ts +686 -686
  30. package/src/helpers/employee-evaluation-request.utils.ts +178 -178
  31. package/src/helpers/evaluation-eligibility.utils.ts +126 -126
  32. package/src/helpers/services-notification-config.helper.ts +1 -6
  33. package/src/index.ts +617 -617
  34. package/src/models/CityMasterModel.ts +3 -0
  35. package/src/models/DepartmentsModel.ts +3 -0
  36. package/src/models/DiplomaticAcademyRequestModel.ts +80 -80
  37. package/src/models/DiplomaticTitlesMasterModel.ts +3 -0
  38. package/src/models/DocumentationFileModel.ts +40 -40
  39. package/src/models/EmbassyEvaluationCycleModel.ts +45 -45
  40. package/src/models/EmbassyEvaluationRequestModel.ts +59 -59
  41. package/src/models/EmployeeEvaluationAnswerModel.ts +26 -26
  42. package/src/models/EvaluationEligibilitySettingModel.ts +51 -51
  43. package/src/models/EvaluationFormModel.ts +44 -44
  44. package/src/models/GroupNamesModel.ts +3 -0
  45. package/src/models/LMSExternalEntityTrainedPersonModel.ts +45 -45
  46. package/src/models/LanguageCourseRequestModel.ts +67 -67
  47. package/src/models/LeaveApprovalsModel.ts +66 -66
  48. package/src/models/LeaveConfigModel.ts +71 -71
  49. package/src/models/LocationModel.ts +3 -0
  50. package/src/models/MissionTravelApprovalModel.ts +101 -101
  51. package/src/models/MissionTravelAttachmentModel.ts +56 -56
  52. package/src/models/MissionTravelChatModel.ts +52 -52
  53. package/src/models/MissionTravelPersonModel.ts +105 -105
  54. package/src/models/MissionTravelWorkflowModel.ts +54 -54
  55. package/src/models/ProjectContactsModel.ts +51 -51
  56. package/src/models/ProjectFaqModel.ts +36 -36
  57. package/src/models/ProjectInvoicesModel.ts +41 -41
  58. package/src/models/ProjectModel.ts +75 -75
  59. package/src/models/ProjectTasksModel.ts +75 -75
  60. package/src/models/SectionModel.ts +38 -35
  61. package/src/models/ServiceSlaApprovalModel.ts +63 -63
  62. package/src/models/ServicesNotificationConfigsModel.ts +55 -55
  63. package/src/models/SlaApprovalsViewModel.ts +135 -135
  64. package/src/models/SlaMyRequestsViewModel.ts +172 -172
  65. package/src/models/role.ts +7 -3
  66. package/src/sla-report-views.ts +3 -3
  67. package/dist/models/EmbassyEvaluationDepartmentSettingModel.d.ts +0 -14
  68. package/dist/models/EmbassyEvaluationDepartmentSettingModel.js +0 -56
  69. package/dist/models/EvaluationMastersModel.d.ts +0 -16
  70. package/dist/models/EvaluationMastersModel.js +0 -67
  71. package/dist/models/EvaluationQuestionMasterModel.d.ts +0 -10
  72. package/dist/models/EvaluationQuestionMasterModel.js +0 -42
  73. package/dist/models/EvaluationSectionMasterModel.d.ts +0 -7
  74. package/dist/models/EvaluationSectionMasterModel.js +0 -36
  75. package/dist/models/EvaluationWorkflowModel.d.ts +0 -60
  76. package/dist/models/EvaluationWorkflowModel.js +0 -215
  77. package/dist/models/IneligibleForEvaluationModel.d.ts +0 -8
  78. package/dist/models/IneligibleForEvaluationModel.js +0 -44
  79. package/dist/models/MissionTravelClassConfigModel.d.ts +0 -10
  80. package/dist/models/MissionTravelClassConfigModel.js +0 -50
  81. package/dist/models/MissionTravelPerdiemModel.d.ts +0 -10
  82. package/dist/models/MissionTravelPerdiemModel.js +0 -54
  83. package/dist/models/UuidAuditModel.d.ts +0 -9
  84. package/dist/models/UuidAuditModel.js +0 -41
@@ -1,178 +1,178 @@
1
- import type { EntityManager } from 'typeorm';
2
- import { EmployeeEvaluationAnswers } from '../models/EmployeeEvaluationAnswerModel';
3
- import { EmployeeEvaluationPersonScore } from '../models/EmployeeEvaluationPersonScoreModel';
4
- import { EmployeeEvaluationRequests } from '../models/EmployeeEvaluationRequestModel';
5
- import { EvaluationEligibilitySetting } from '../models/EvaluationEligibilitySettingModel';
6
- import { EvaluationFormQuestion } from '../models/EvaluationFormQuestionModel';
7
- import { isCalendarMonthInEligibilityRange } from './evaluation-eligibility.utils';
8
-
9
- export const DEFAULT_MAX_EMPLOYEES_PER_REQUEST = 50;
10
-
11
- export type EmployeeEvaluationSubmitAnswer = {
12
- section_id: number;
13
- question_id: number;
14
- score: number;
15
- remarks?: string | null;
16
- };
17
-
18
- export type EmployeeEvaluationSubmitEmployee = {
19
- user_id: number;
20
- is_rca?: boolean;
21
- answers: EmployeeEvaluationSubmitAnswer[];
22
- };
23
-
24
- export type NormalizedEmployeeSubmission = EmployeeEvaluationSubmitEmployee;
25
-
26
- export function normalizeEmployeeSubmissions(raw: unknown): {
27
- employees: NormalizedEmployeeSubmission[];
28
- error?: string;
29
- } {
30
- if (!Array.isArray(raw) || raw.length === 0) {
31
- return { employees: [], error: 'employees must be a non-empty array' };
32
- }
33
- const seen = new Set<number>();
34
- const employees: NormalizedEmployeeSubmission[] = [];
35
- for (const item of raw) {
36
- if (item == null || typeof item !== 'object') {
37
- return { employees: [], error: 'Each employee entry must be an object' };
38
- }
39
- const o = item as Record<string, unknown>;
40
- const user_id = Number(o.user_id ?? o.employee_id);
41
- if (!Number.isFinite(user_id) || user_id <= 0) {
42
- return { employees: [], error: 'Each employee must have a valid user_id' };
43
- }
44
- if (seen.has(user_id)) {
45
- return { employees: [], error: `Duplicate user_id ${user_id} in employees list` };
46
- }
47
- seen.add(user_id);
48
- const answersRaw = o.answers;
49
- if (!Array.isArray(answersRaw) || answersRaw.length === 0) {
50
- return { employees: [], error: `Employee ${user_id} must include a non-empty answers array` };
51
- }
52
- const answers: EmployeeEvaluationSubmitAnswer[] = [];
53
- const qSeen = new Set<number>();
54
- for (const a of answersRaw) {
55
- if (a == null || typeof a !== 'object') {
56
- return { employees: [], error: `Invalid answer for employee ${user_id}` };
57
- }
58
- const ar = a as Record<string, unknown>;
59
- const section_id = Number(ar.section_id);
60
- const question_id = Number(ar.question_id);
61
- const score = Number(ar.score);
62
- if (!Number.isFinite(section_id) || !Number.isFinite(question_id)) {
63
- return { employees: [], error: `section_id and question_id required for employee ${user_id}` };
64
- }
65
- if (!Number.isFinite(score) || score < 0) {
66
- return { employees: [], error: `score must be a non-negative number for employee ${user_id}, question ${question_id}` };
67
- }
68
- if (qSeen.has(question_id)) {
69
- return { employees: [], error: `Duplicate question_id ${question_id} for employee ${user_id}` };
70
- }
71
- qSeen.add(question_id);
72
- answers.push({
73
- section_id,
74
- question_id,
75
- score,
76
- remarks: ar.remarks != null ? String(ar.remarks) : null,
77
- });
78
- }
79
- employees.push({
80
- user_id,
81
- is_rca: o.is_rca !== undefined ? Boolean(o.is_rca) : false,
82
- answers,
83
- });
84
- }
85
- return { employees };
86
- }
87
-
88
- /** Resolve max employees from active exception cycle for the evaluation month. */
89
- export async function resolveMaxEmployeesPerRequest(
90
- manager: EntityManager,
91
- month: number
92
- ): Promise<number> {
93
- const rows = await manager
94
- .getRepository(EvaluationEligibilitySetting)
95
- .createQueryBuilder('s')
96
- .where('s.is_deleted = false')
97
- .andWhere('s.is_active = true')
98
- .orderBy('s.id', 'DESC')
99
- .getMany();
100
- const setting =
101
- rows.find((s) => isCalendarMonthInEligibilityRange(s.from_month, s.to_month, month)) ?? null;
102
- const max = setting?.max_employees_per_request;
103
- if (max != null && Number.isFinite(Number(max)) && Number(max) > 0) {
104
- return Math.floor(Number(max));
105
- }
106
- return DEFAULT_MAX_EMPLOYEES_PER_REQUEST;
107
- }
108
-
109
- export async function persistEmployeeEvaluationScores(
110
- manager: EntityManager,
111
- opts: {
112
- requestId: number;
113
- formId: number;
114
- createdBy: string;
115
- employees: NormalizedEmployeeSubmission[];
116
- }
117
- ): Promise<{ totalScore: number; averageScore: number; employeeCount: number }> {
118
- const { requestId, formId, createdBy, employees } = opts;
119
- let requestTotal = 0;
120
-
121
- for (const emp of employees) {
122
- let personTotal = 0;
123
- for (const ans of emp.answers) {
124
- const qMeta = await manager.findOne(EvaluationFormQuestion, {
125
- where: { id: ans.question_id, is_deleted: false },
126
- });
127
- const questionSectionId = qMeta?.form_section_id;
128
- if (!qMeta || questionSectionId == null || questionSectionId !== ans.section_id) {
129
- throw new Error(`Invalid question ${ans.question_id} for section ${ans.section_id}`);
130
- }
131
- if (qMeta.max_score != null && ans.score > qMeta.max_score) {
132
- throw new Error(
133
- `score ${ans.score} exceeds max_score ${qMeta.max_score} for question ${ans.question_id}`
134
- );
135
- }
136
- personTotal += ans.score;
137
- await manager.save(
138
- EmployeeEvaluationAnswers,
139
- manager.create(EmployeeEvaluationAnswers, {
140
- request_id: requestId,
141
- user_id: emp.user_id,
142
- form_id: formId,
143
- section_id: ans.section_id,
144
- question_id: ans.question_id,
145
- score: ans.score,
146
- remarks: ans.remarks ?? null,
147
- created_by: createdBy,
148
- is_deleted: false,
149
- })
150
- );
151
- }
152
-
153
- await manager.save(
154
- EmployeeEvaluationPersonScore,
155
- manager.create(EmployeeEvaluationPersonScore, {
156
- request_id: requestId,
157
- user_id: emp.user_id,
158
- is_rca: Boolean(emp.is_rca),
159
- us_feedback: null,
160
- total_score: personTotal,
161
- created_by: createdBy,
162
- is_deleted: false,
163
- })
164
- );
165
- requestTotal += personTotal;
166
- }
167
-
168
- const employeeCount = employees.length;
169
- const averageScore = employeeCount ? requestTotal / employeeCount : 0;
170
- await manager.update(EmployeeEvaluationRequests, { id: requestId }, {
171
- total_score: requestTotal,
172
- average_score: averageScore,
173
- employee_count: employeeCount,
174
- updated_by: createdBy,
175
- });
176
-
177
- return { totalScore: requestTotal, averageScore, employeeCount };
178
- }
1
+ import type { EntityManager } from 'typeorm';
2
+ import { EmployeeEvaluationAnswers } from '../models/EmployeeEvaluationAnswerModel';
3
+ import { EmployeeEvaluationPersonScore } from '../models/EmployeeEvaluationPersonScoreModel';
4
+ import { EmployeeEvaluationRequests } from '../models/EmployeeEvaluationRequestModel';
5
+ import { EvaluationEligibilitySetting } from '../models/EvaluationEligibilitySettingModel';
6
+ import { EvaluationFormQuestion } from '../models/EvaluationFormQuestionModel';
7
+ import { isCalendarMonthInEligibilityRange } from './evaluation-eligibility.utils';
8
+
9
+ export const DEFAULT_MAX_EMPLOYEES_PER_REQUEST = 50;
10
+
11
+ export type EmployeeEvaluationSubmitAnswer = {
12
+ section_id: number;
13
+ question_id: number;
14
+ score: number;
15
+ remarks?: string | null;
16
+ };
17
+
18
+ export type EmployeeEvaluationSubmitEmployee = {
19
+ user_id: number;
20
+ is_rca?: boolean;
21
+ answers: EmployeeEvaluationSubmitAnswer[];
22
+ };
23
+
24
+ export type NormalizedEmployeeSubmission = EmployeeEvaluationSubmitEmployee;
25
+
26
+ export function normalizeEmployeeSubmissions(raw: unknown): {
27
+ employees: NormalizedEmployeeSubmission[];
28
+ error?: string;
29
+ } {
30
+ if (!Array.isArray(raw) || raw.length === 0) {
31
+ return { employees: [], error: 'employees must be a non-empty array' };
32
+ }
33
+ const seen = new Set<number>();
34
+ const employees: NormalizedEmployeeSubmission[] = [];
35
+ for (const item of raw) {
36
+ if (item == null || typeof item !== 'object') {
37
+ return { employees: [], error: 'Each employee entry must be an object' };
38
+ }
39
+ const o = item as Record<string, unknown>;
40
+ const user_id = Number(o.user_id ?? o.employee_id);
41
+ if (!Number.isFinite(user_id) || user_id <= 0) {
42
+ return { employees: [], error: 'Each employee must have a valid user_id' };
43
+ }
44
+ if (seen.has(user_id)) {
45
+ return { employees: [], error: `Duplicate user_id ${user_id} in employees list` };
46
+ }
47
+ seen.add(user_id);
48
+ const answersRaw = o.answers;
49
+ if (!Array.isArray(answersRaw) || answersRaw.length === 0) {
50
+ return { employees: [], error: `Employee ${user_id} must include a non-empty answers array` };
51
+ }
52
+ const answers: EmployeeEvaluationSubmitAnswer[] = [];
53
+ const qSeen = new Set<number>();
54
+ for (const a of answersRaw) {
55
+ if (a == null || typeof a !== 'object') {
56
+ return { employees: [], error: `Invalid answer for employee ${user_id}` };
57
+ }
58
+ const ar = a as Record<string, unknown>;
59
+ const section_id = Number(ar.section_id);
60
+ const question_id = Number(ar.question_id);
61
+ const score = Number(ar.score);
62
+ if (!Number.isFinite(section_id) || !Number.isFinite(question_id)) {
63
+ return { employees: [], error: `section_id and question_id required for employee ${user_id}` };
64
+ }
65
+ if (!Number.isFinite(score) || score < 0) {
66
+ return { employees: [], error: `score must be a non-negative number for employee ${user_id}, question ${question_id}` };
67
+ }
68
+ if (qSeen.has(question_id)) {
69
+ return { employees: [], error: `Duplicate question_id ${question_id} for employee ${user_id}` };
70
+ }
71
+ qSeen.add(question_id);
72
+ answers.push({
73
+ section_id,
74
+ question_id,
75
+ score,
76
+ remarks: ar.remarks != null ? String(ar.remarks) : null,
77
+ });
78
+ }
79
+ employees.push({
80
+ user_id,
81
+ is_rca: o.is_rca !== undefined ? Boolean(o.is_rca) : false,
82
+ answers,
83
+ });
84
+ }
85
+ return { employees };
86
+ }
87
+
88
+ /** Resolve max employees from active exception cycle for the evaluation month. */
89
+ export async function resolveMaxEmployeesPerRequest(
90
+ manager: EntityManager,
91
+ month: number
92
+ ): Promise<number> {
93
+ const rows = await manager
94
+ .getRepository(EvaluationEligibilitySetting)
95
+ .createQueryBuilder('s')
96
+ .where('s.is_deleted = false')
97
+ .andWhere('s.is_active = true')
98
+ .orderBy('s.id', 'DESC')
99
+ .getMany();
100
+ const setting =
101
+ rows.find((s) => isCalendarMonthInEligibilityRange(s.from_month, s.to_month, month)) ?? null;
102
+ const max = setting?.max_employees_per_request;
103
+ if (max != null && Number.isFinite(Number(max)) && Number(max) > 0) {
104
+ return Math.floor(Number(max));
105
+ }
106
+ return DEFAULT_MAX_EMPLOYEES_PER_REQUEST;
107
+ }
108
+
109
+ export async function persistEmployeeEvaluationScores(
110
+ manager: EntityManager,
111
+ opts: {
112
+ requestId: number;
113
+ formId: number;
114
+ createdBy: string;
115
+ employees: NormalizedEmployeeSubmission[];
116
+ }
117
+ ): Promise<{ totalScore: number; averageScore: number; employeeCount: number }> {
118
+ const { requestId, formId, createdBy, employees } = opts;
119
+ let requestTotal = 0;
120
+
121
+ for (const emp of employees) {
122
+ let personTotal = 0;
123
+ for (const ans of emp.answers) {
124
+ const qMeta = await manager.findOne(EvaluationFormQuestion, {
125
+ where: { id: ans.question_id, is_deleted: false },
126
+ });
127
+ const questionSectionId = qMeta?.form_section_id;
128
+ if (!qMeta || questionSectionId == null || questionSectionId !== ans.section_id) {
129
+ throw new Error(`Invalid question ${ans.question_id} for section ${ans.section_id}`);
130
+ }
131
+ if (qMeta.max_score != null && ans.score > qMeta.max_score) {
132
+ throw new Error(
133
+ `score ${ans.score} exceeds max_score ${qMeta.max_score} for question ${ans.question_id}`
134
+ );
135
+ }
136
+ personTotal += ans.score;
137
+ await manager.save(
138
+ EmployeeEvaluationAnswers,
139
+ manager.create(EmployeeEvaluationAnswers, {
140
+ request_id: requestId,
141
+ user_id: emp.user_id,
142
+ form_id: formId,
143
+ section_id: ans.section_id,
144
+ question_id: ans.question_id,
145
+ score: ans.score,
146
+ remarks: ans.remarks ?? null,
147
+ created_by: createdBy,
148
+ is_deleted: false,
149
+ })
150
+ );
151
+ }
152
+
153
+ await manager.save(
154
+ EmployeeEvaluationPersonScore,
155
+ manager.create(EmployeeEvaluationPersonScore, {
156
+ request_id: requestId,
157
+ user_id: emp.user_id,
158
+ is_rca: Boolean(emp.is_rca),
159
+ us_feedback: null,
160
+ total_score: personTotal,
161
+ created_by: createdBy,
162
+ is_deleted: false,
163
+ })
164
+ );
165
+ requestTotal += personTotal;
166
+ }
167
+
168
+ const employeeCount = employees.length;
169
+ const averageScore = employeeCount ? requestTotal / employeeCount : 0;
170
+ await manager.update(EmployeeEvaluationRequests, { id: requestId }, {
171
+ total_score: requestTotal,
172
+ average_score: averageScore,
173
+ employee_count: employeeCount,
174
+ updated_by: createdBy,
175
+ });
176
+
177
+ return { totalScore: requestTotal, averageScore, employeeCount };
178
+ }
@@ -1,126 +1,126 @@
1
- /**
2
- * Evaluation exception cycle dates are stored as MM-DD strings (e.g. 01-01, 05-31, 04-12).
3
- */
4
-
5
- export type MmDdInterval = { start: number; end: number };
6
-
7
- /** Validate and normalize MM-DD (e.g. 1-1 → 01-01). */
8
- export function parseMonthDay(value: unknown): string | null {
9
- if (value == null || value === '') return null;
10
- const s = String(value).trim();
11
- const m = /^(\d{1,2})-(\d{1,2})$/.exec(s);
12
- if (!m) return null;
13
- const month = parseInt(m[1], 10);
14
- const day = parseInt(m[2], 10);
15
- if (month < 1 || month > 12 || day < 1 || day > 31) return null;
16
- return `${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
17
- }
18
-
19
- function parseMmDdParts(mmDd: string): { month: number; day: number } | null {
20
- const normalized = parseMonthDay(mmDd);
21
- if (!normalized) return null;
22
- const [month, day] = normalized.split('-').map(Number);
23
- return { month, day };
24
- }
25
-
26
- /** Sortable key for MM-DD overlap checks (month * 100 + day). */
27
- export function mmDdSortKey(mmDd: string): number {
28
- const parts = parseMmDdParts(mmDd);
29
- if (!parts) return 0;
30
- return parts.month * 100 + parts.day;
31
- }
32
-
33
- function expandMmDdWindow(startMmDd: string, endMmDd: string): MmDdInterval[] {
34
- const start = mmDdSortKey(startMmDd);
35
- const end = mmDdSortKey(endMmDd);
36
- if (start <= end) return [{ start, end }];
37
- return [
38
- { start, end: 1231 },
39
- { start: 101, end },
40
- ];
41
- }
42
-
43
- function linearIntervalsOverlap(a: MmDdInterval, b: MmDdInterval): boolean {
44
- return a.start <= b.end && b.start <= a.end;
45
- }
46
-
47
- /** True when two MM-DD windows share any calendar period. */
48
- export function mmDdWindowsOverlap(
49
- startA: string,
50
- endA: string,
51
- startB: string,
52
- endB: string
53
- ): boolean {
54
- const intervalsA = expandMmDdWindow(startA, endA);
55
- const intervalsB = expandMmDdWindow(startB, endB);
56
- for (const ia of intervalsA) {
57
- for (const ib of intervalsB) {
58
- if (linearIntervalsOverlap(ia, ib)) return true;
59
- }
60
- }
61
- return false;
62
- }
63
-
64
- /** Whether calendar month (1–12) falls within the inclusive MM-DD window. */
65
- export function isCalendarMonthInEligibilityRange(
66
- fromMmDd: string,
67
- toMmDd: string,
68
- calendarMonth: number
69
- ): boolean {
70
- const from = parseMmDdParts(fromMmDd);
71
- const to = parseMmDdParts(toMmDd);
72
- if (!from || !to || calendarMonth < 1 || calendarMonth > 12) return false;
73
- const fm = from.month;
74
- const tm = to.month;
75
- if (fm <= tm) return calendarMonth >= fm && calendarMonth <= tm;
76
- return calendarMonth >= fm || calendarMonth <= tm;
77
- }
78
-
79
- /**
80
- * Whether HOD submissions are still open for the active cycle on the reference date.
81
- * Uses the day component of `evaluationEndMmDd` as the last submission day of each month in range.
82
- */
83
- export function isEvaluationEligibilityWindowOpen(
84
- fromMmDd: string,
85
- toMmDd: string,
86
- evaluationEndMmDd: string,
87
- referenceDate: Date = new Date()
88
- ): boolean {
89
- const month = referenceDate.getMonth() + 1;
90
- const day = referenceDate.getDate();
91
- if (!isCalendarMonthInEligibilityRange(fromMmDd, toMmDd, month)) return false;
92
- const endParts = parseMmDdParts(evaluationEndMmDd);
93
- if (!endParts) return false;
94
- return day <= endParts.day;
95
- }
96
-
97
- export function parseEligibilityDateRange(
98
- fromInput: unknown,
99
- toInput: unknown
100
- ): { from_month: string; to_month: string } | null {
101
- const from_month = parseMonthDay(fromInput);
102
- const to_month = parseMonthDay(toInput);
103
- if (!from_month || !to_month) return null;
104
- return { from_month, to_month };
105
- }
106
-
107
- /** @deprecated Use parseMonthDay for MM-DD strings. */
108
- export function parseEvaluationEndDay(input: unknown): number | null {
109
- const mmDd = parseMonthDay(input);
110
- if (!mmDd) return null;
111
- const parts = parseMmDdParts(mmDd);
112
- return parts?.day ?? null;
113
- }
114
-
115
- /** @deprecated Use parseEligibilityDateRange for MM-DD strings. */
116
- export function parseMonthRange(
117
- fromInput: unknown,
118
- toInput: unknown
119
- ): { from_month: number; to_month: number } | null {
120
- const range = parseEligibilityDateRange(fromInput, toInput);
121
- if (!range) return null;
122
- const from = parseMmDdParts(range.from_month);
123
- const to = parseMmDdParts(range.to_month);
124
- if (!from || !to) return null;
125
- return { from_month: from.month, to_month: to.month };
126
- }
1
+ /**
2
+ * Evaluation exception cycle dates are stored as MM-DD strings (e.g. 01-01, 05-31, 04-12).
3
+ */
4
+
5
+ export type MmDdInterval = { start: number; end: number };
6
+
7
+ /** Validate and normalize MM-DD (e.g. 1-1 → 01-01). */
8
+ export function parseMonthDay(value: unknown): string | null {
9
+ if (value == null || value === '') return null;
10
+ const s = String(value).trim();
11
+ const m = /^(\d{1,2})-(\d{1,2})$/.exec(s);
12
+ if (!m) return null;
13
+ const month = parseInt(m[1], 10);
14
+ const day = parseInt(m[2], 10);
15
+ if (month < 1 || month > 12 || day < 1 || day > 31) return null;
16
+ return `${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
17
+ }
18
+
19
+ function parseMmDdParts(mmDd: string): { month: number; day: number } | null {
20
+ const normalized = parseMonthDay(mmDd);
21
+ if (!normalized) return null;
22
+ const [month, day] = normalized.split('-').map(Number);
23
+ return { month, day };
24
+ }
25
+
26
+ /** Sortable key for MM-DD overlap checks (month * 100 + day). */
27
+ export function mmDdSortKey(mmDd: string): number {
28
+ const parts = parseMmDdParts(mmDd);
29
+ if (!parts) return 0;
30
+ return parts.month * 100 + parts.day;
31
+ }
32
+
33
+ function expandMmDdWindow(startMmDd: string, endMmDd: string): MmDdInterval[] {
34
+ const start = mmDdSortKey(startMmDd);
35
+ const end = mmDdSortKey(endMmDd);
36
+ if (start <= end) return [{ start, end }];
37
+ return [
38
+ { start, end: 1231 },
39
+ { start: 101, end },
40
+ ];
41
+ }
42
+
43
+ function linearIntervalsOverlap(a: MmDdInterval, b: MmDdInterval): boolean {
44
+ return a.start <= b.end && b.start <= a.end;
45
+ }
46
+
47
+ /** True when two MM-DD windows share any calendar period. */
48
+ export function mmDdWindowsOverlap(
49
+ startA: string,
50
+ endA: string,
51
+ startB: string,
52
+ endB: string
53
+ ): boolean {
54
+ const intervalsA = expandMmDdWindow(startA, endA);
55
+ const intervalsB = expandMmDdWindow(startB, endB);
56
+ for (const ia of intervalsA) {
57
+ for (const ib of intervalsB) {
58
+ if (linearIntervalsOverlap(ia, ib)) return true;
59
+ }
60
+ }
61
+ return false;
62
+ }
63
+
64
+ /** Whether calendar month (1–12) falls within the inclusive MM-DD window. */
65
+ export function isCalendarMonthInEligibilityRange(
66
+ fromMmDd: string,
67
+ toMmDd: string,
68
+ calendarMonth: number
69
+ ): boolean {
70
+ const from = parseMmDdParts(fromMmDd);
71
+ const to = parseMmDdParts(toMmDd);
72
+ if (!from || !to || calendarMonth < 1 || calendarMonth > 12) return false;
73
+ const fm = from.month;
74
+ const tm = to.month;
75
+ if (fm <= tm) return calendarMonth >= fm && calendarMonth <= tm;
76
+ return calendarMonth >= fm || calendarMonth <= tm;
77
+ }
78
+
79
+ /**
80
+ * Whether HOD submissions are still open for the active cycle on the reference date.
81
+ * Uses the day component of `evaluationEndMmDd` as the last submission day of each month in range.
82
+ */
83
+ export function isEvaluationEligibilityWindowOpen(
84
+ fromMmDd: string,
85
+ toMmDd: string,
86
+ evaluationEndMmDd: string,
87
+ referenceDate: Date = new Date()
88
+ ): boolean {
89
+ const month = referenceDate.getMonth() + 1;
90
+ const day = referenceDate.getDate();
91
+ if (!isCalendarMonthInEligibilityRange(fromMmDd, toMmDd, month)) return false;
92
+ const endParts = parseMmDdParts(evaluationEndMmDd);
93
+ if (!endParts) return false;
94
+ return day <= endParts.day;
95
+ }
96
+
97
+ export function parseEligibilityDateRange(
98
+ fromInput: unknown,
99
+ toInput: unknown
100
+ ): { from_month: string; to_month: string } | null {
101
+ const from_month = parseMonthDay(fromInput);
102
+ const to_month = parseMonthDay(toInput);
103
+ if (!from_month || !to_month) return null;
104
+ return { from_month, to_month };
105
+ }
106
+
107
+ /** @deprecated Use parseMonthDay for MM-DD strings. */
108
+ export function parseEvaluationEndDay(input: unknown): number | null {
109
+ const mmDd = parseMonthDay(input);
110
+ if (!mmDd) return null;
111
+ const parts = parseMmDdParts(mmDd);
112
+ return parts?.day ?? null;
113
+ }
114
+
115
+ /** @deprecated Use parseEligibilityDateRange for MM-DD strings. */
116
+ export function parseMonthRange(
117
+ fromInput: unknown,
118
+ toInput: unknown
119
+ ): { from_month: number; to_month: number } | null {
120
+ const range = parseEligibilityDateRange(fromInput, toInput);
121
+ if (!range) return null;
122
+ const from = parseMmDdParts(range.from_month);
123
+ const to = parseMmDdParts(range.to_month);
124
+ if (!from || !to) return null;
125
+ return { from_month: from.month, to_month: to.month };
126
+ }
@@ -133,7 +133,6 @@ export async function sendFmServicesNotificationConfigNotifications(
133
133
  pendingCount,
134
134
  requestId,
135
135
  status,
136
- approvalLevel,
137
136
  shortProductName,
138
137
  requestTypeKey,
139
138
  routePath,
@@ -194,10 +193,7 @@ export async function sendFmServicesNotificationConfigNotifications(
194
193
  continue;
195
194
  }
196
195
 
197
- const isEveryApproval = notificationConfig.trigger === ServicesNotificationTriggerType.EVERY_APPROVAL;
198
- const notificationTitle = isEveryApproval
199
- ? `${shortProductName} Request ${status} at Level ${approvalLevel}`
200
- : `${shortProductName} Request ${status}`;
196
+ const notificationTitle = `${shortProductName} Request ${status}`;
201
197
  const notificationData = `${shortProductName} request #${requestId} has been ${status}.`;
202
198
 
203
199
  for (const ur of userRoles) {
@@ -230,7 +226,6 @@ export async function sendFmServicesNotificationConfigNotifications(
230
226
  requestId,
231
227
  requestType: requestTypeKey,
232
228
  status,
233
- level: approvalLevel,
234
229
  timestamp: new Date().toISOString(),
235
230
  },
236
231
  is_read: false,