@platform-modules/foreign-ministry 1.3.327 → 1.3.333

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.
@@ -16,8 +16,8 @@ export declare function normalizeEmployeeSubmissions(raw: unknown): {
16
16
  employees: NormalizedEmployeeSubmission[];
17
17
  error?: string;
18
18
  };
19
- /** Resolve max employees from active eligibility setting for dept/section/month. */
20
- export declare function resolveMaxEmployeesPerRequest(manager: EntityManager, department_id: number, section_id: number, month: number): Promise<number>;
19
+ /** Resolve max employees from active exception cycle for the evaluation month. */
20
+ export declare function resolveMaxEmployeesPerRequest(manager: EntityManager, month: number): Promise<number>;
21
21
  export declare function persistEmployeeEvaluationScores(manager: EntityManager, opts: {
22
22
  requestId: number;
23
23
  formId: number;
@@ -9,6 +9,7 @@ const EmployeeEvaluationPersonScoreModel_1 = require("../models/EmployeeEvaluati
9
9
  const EmployeeEvaluationRequestModel_1 = require("../models/EmployeeEvaluationRequestModel");
10
10
  const EvaluationEligibilitySettingModel_1 = require("../models/EvaluationEligibilitySettingModel");
11
11
  const EvaluationFormQuestionModel_1 = require("../models/EvaluationFormQuestionModel");
12
+ const evaluation_eligibility_utils_1 = require("./evaluation-eligibility.utils");
12
13
  exports.DEFAULT_MAX_EMPLOYEES_PER_REQUEST = 50;
13
14
  function normalizeEmployeeSubmissions(raw) {
14
15
  if (!Array.isArray(raw) || raw.length === 0) {
@@ -68,19 +69,16 @@ function normalizeEmployeeSubmissions(raw) {
68
69
  }
69
70
  return { employees };
70
71
  }
71
- /** Resolve max employees from active eligibility setting for dept/section/month. */
72
- async function resolveMaxEmployeesPerRequest(manager, department_id, section_id, month) {
73
- const setting = await manager
72
+ /** Resolve max employees from active exception cycle for the evaluation month. */
73
+ async function resolveMaxEmployeesPerRequest(manager, month) {
74
+ const rows = await manager
74
75
  .getRepository(EvaluationEligibilitySettingModel_1.EvaluationEligibilitySetting)
75
76
  .createQueryBuilder('s')
76
77
  .where('s.is_deleted = false')
77
78
  .andWhere('s.is_active = true')
78
- .andWhere('s.department_id = :department_id', { department_id })
79
- .andWhere('s.section_id = :section_id', { section_id })
80
- .andWhere('s.from_month <= :month', { month })
81
- .andWhere('s.to_month >= :month', { month })
82
79
  .orderBy('s.id', 'DESC')
83
- .getOne();
80
+ .getMany();
81
+ const setting = rows.find((s) => (0, evaluation_eligibility_utils_1.isCalendarMonthInEligibilityRange)(s.from_month, s.to_month, month)) ?? null;
84
82
  const max = setting?.max_employees_per_request;
85
83
  if (max != null && Number.isFinite(Number(max)) && Number(max) > 0) {
86
84
  return Math.floor(Number(max));
@@ -1,9 +1,30 @@
1
1
  /**
2
- * Whether evaluations are still open for a setting's month range.
3
- * `evaluation_end_date` is the last calendar day (1–31) of each month in the range.
2
+ * Evaluation exception cycle dates are stored as MM-DD strings (e.g. 01-01, 05-31, 04-12).
4
3
  */
5
- export declare function isEvaluationEligibilityWindowOpen(fromMonth: number, toMonth: number, evaluationEndDay: number, referenceDate?: Date): boolean;
4
+ export type MmDdInterval = {
5
+ start: number;
6
+ end: number;
7
+ };
8
+ /** Validate and normalize MM-DD (e.g. 1-1 → 01-01). */
9
+ export declare function parseMonthDay(value: unknown): string | null;
10
+ /** Sortable key for MM-DD overlap checks (month * 100 + day). */
11
+ export declare function mmDdSortKey(mmDd: string): number;
12
+ /** True when two MM-DD windows share any calendar period. */
13
+ export declare function mmDdWindowsOverlap(startA: string, endA: string, startB: string, endB: string): boolean;
14
+ /** Whether calendar month (1–12) falls within the inclusive MM-DD window. */
15
+ export declare function isCalendarMonthInEligibilityRange(fromMmDd: string, toMmDd: string, calendarMonth: number): boolean;
16
+ /**
17
+ * Whether HOD submissions are still open for the active cycle on the reference date.
18
+ * Uses the day component of `evaluationEndMmDd` as the last submission day of each month in range.
19
+ */
20
+ export declare function isEvaluationEligibilityWindowOpen(fromMmDd: string, toMmDd: string, evaluationEndMmDd: string, referenceDate?: Date): boolean;
21
+ export declare function parseEligibilityDateRange(fromInput: unknown, toInput: unknown): {
22
+ from_month: string;
23
+ to_month: string;
24
+ } | null;
25
+ /** @deprecated Use parseMonthDay for MM-DD strings. */
6
26
  export declare function parseEvaluationEndDay(input: unknown): number | null;
27
+ /** @deprecated Use parseEligibilityDateRange for MM-DD strings. */
7
28
  export declare function parseMonthRange(fromInput: unknown, toInput: unknown): {
8
29
  from_month: number;
9
30
  to_month: number;
@@ -1,39 +1,118 @@
1
1
  "use strict";
2
+ /**
3
+ * Evaluation exception cycle dates are stored as MM-DD strings (e.g. 01-01, 05-31, 04-12).
4
+ */
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseMonthDay = parseMonthDay;
7
+ exports.mmDdSortKey = mmDdSortKey;
8
+ exports.mmDdWindowsOverlap = mmDdWindowsOverlap;
9
+ exports.isCalendarMonthInEligibilityRange = isCalendarMonthInEligibilityRange;
3
10
  exports.isEvaluationEligibilityWindowOpen = isEvaluationEligibilityWindowOpen;
11
+ exports.parseEligibilityDateRange = parseEligibilityDateRange;
4
12
  exports.parseEvaluationEndDay = parseEvaluationEndDay;
5
13
  exports.parseMonthRange = parseMonthRange;
14
+ /** Validate and normalize MM-DD (e.g. 1-1 → 01-01). */
15
+ function parseMonthDay(value) {
16
+ if (value == null || value === '')
17
+ return null;
18
+ const s = String(value).trim();
19
+ const m = /^(\d{1,2})-(\d{1,2})$/.exec(s);
20
+ if (!m)
21
+ return null;
22
+ const month = parseInt(m[1], 10);
23
+ const day = parseInt(m[2], 10);
24
+ if (month < 1 || month > 12 || day < 1 || day > 31)
25
+ return null;
26
+ return `${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
27
+ }
28
+ function parseMmDdParts(mmDd) {
29
+ const normalized = parseMonthDay(mmDd);
30
+ if (!normalized)
31
+ return null;
32
+ const [month, day] = normalized.split('-').map(Number);
33
+ return { month, day };
34
+ }
35
+ /** Sortable key for MM-DD overlap checks (month * 100 + day). */
36
+ function mmDdSortKey(mmDd) {
37
+ const parts = parseMmDdParts(mmDd);
38
+ if (!parts)
39
+ return 0;
40
+ return parts.month * 100 + parts.day;
41
+ }
42
+ function expandMmDdWindow(startMmDd, endMmDd) {
43
+ const start = mmDdSortKey(startMmDd);
44
+ const end = mmDdSortKey(endMmDd);
45
+ if (start <= end)
46
+ return [{ start, end }];
47
+ return [
48
+ { start, end: 1231 },
49
+ { start: 101, end },
50
+ ];
51
+ }
52
+ function linearIntervalsOverlap(a, b) {
53
+ return a.start <= b.end && b.start <= a.end;
54
+ }
55
+ /** True when two MM-DD windows share any calendar period. */
56
+ function mmDdWindowsOverlap(startA, endA, startB, endB) {
57
+ const intervalsA = expandMmDdWindow(startA, endA);
58
+ const intervalsB = expandMmDdWindow(startB, endB);
59
+ for (const ia of intervalsA) {
60
+ for (const ib of intervalsB) {
61
+ if (linearIntervalsOverlap(ia, ib))
62
+ return true;
63
+ }
64
+ }
65
+ return false;
66
+ }
67
+ /** Whether calendar month (1–12) falls within the inclusive MM-DD window. */
68
+ function isCalendarMonthInEligibilityRange(fromMmDd, toMmDd, calendarMonth) {
69
+ const from = parseMmDdParts(fromMmDd);
70
+ const to = parseMmDdParts(toMmDd);
71
+ if (!from || !to || calendarMonth < 1 || calendarMonth > 12)
72
+ return false;
73
+ const fm = from.month;
74
+ const tm = to.month;
75
+ if (fm <= tm)
76
+ return calendarMonth >= fm && calendarMonth <= tm;
77
+ return calendarMonth >= fm || calendarMonth <= tm;
78
+ }
6
79
  /**
7
- * Whether evaluations are still open for a setting's month range.
8
- * `evaluation_end_date` is the last calendar day (1–31) of each month in the range.
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.
9
82
  */
10
- function isEvaluationEligibilityWindowOpen(fromMonth, toMonth, evaluationEndDay, referenceDate = new Date()) {
11
- const month = referenceDate.getUTCMonth() + 1;
12
- const day = referenceDate.getUTCDate();
13
- if (month < fromMonth || month > toMonth)
83
+ function isEvaluationEligibilityWindowOpen(fromMmDd, toMmDd, evaluationEndMmDd, referenceDate = new Date()) {
84
+ const month = referenceDate.getMonth() + 1;
85
+ const day = referenceDate.getDate();
86
+ if (!isCalendarMonthInEligibilityRange(fromMmDd, toMmDd, month))
14
87
  return false;
15
- if (!Number.isFinite(evaluationEndDay) || evaluationEndDay < 1 || evaluationEndDay > 31)
88
+ const endParts = parseMmDdParts(evaluationEndMmDd);
89
+ if (!endParts)
16
90
  return false;
17
- return day <= evaluationEndDay;
91
+ return day <= endParts.day;
18
92
  }
19
- function parseEvaluationEndDay(input) {
20
- if (input == null || input === '')
93
+ function parseEligibilityDateRange(fromInput, toInput) {
94
+ const from_month = parseMonthDay(fromInput);
95
+ const to_month = parseMonthDay(toInput);
96
+ if (!from_month || !to_month)
21
97
  return null;
22
- const n = typeof input === 'number' ? input : parseInt(String(input).trim(), 10);
23
- if (!Number.isFinite(n) || n < 1 || n > 31)
98
+ return { from_month, to_month };
99
+ }
100
+ /** @deprecated Use parseMonthDay for MM-DD strings. */
101
+ function parseEvaluationEndDay(input) {
102
+ const mmDd = parseMonthDay(input);
103
+ if (!mmDd)
24
104
  return null;
25
- return Math.floor(n);
105
+ const parts = parseMmDdParts(mmDd);
106
+ return parts?.day ?? null;
26
107
  }
108
+ /** @deprecated Use parseEligibilityDateRange for MM-DD strings. */
27
109
  function parseMonthRange(fromInput, toInput) {
28
- const from_month = typeof fromInput === 'number' ? fromInput : parseInt(String(fromInput).trim(), 10);
29
- const to_month = typeof toInput === 'number' ? toInput : parseInt(String(toInput).trim(), 10);
30
- if (!Number.isFinite(from_month) || !Number.isFinite(to_month))
31
- return null;
32
- if (!Number.isInteger(from_month) || !Number.isInteger(to_month))
110
+ const range = parseEligibilityDateRange(fromInput, toInput);
111
+ if (!range)
33
112
  return null;
34
- if (from_month < 1 || from_month > 12 || to_month < 1 || to_month > 12)
113
+ const from = parseMmDdParts(range.from_month);
114
+ const to = parseMmDdParts(range.to_month);
115
+ if (!from || !to)
35
116
  return null;
36
- if (from_month > to_month)
37
- return null;
38
- return { from_month, to_month };
117
+ return { from_month: from.month, to_month: to.month };
39
118
  }
package/dist/index.d.ts CHANGED
@@ -409,7 +409,7 @@ export { parsePortalUserIdFromRequest, userHasPortalAdminRole, isPortalAdminFrom
409
409
  export type { FmServicesNotificationConfigRecipient, CollectFmServicesNotificationConfigRecipientsParams, SendFmServicesNotificationConfigNotificationsParams, } from './helpers/services-notification-config.helper';
410
410
  export * from './models/MoodleUsersModel';
411
411
  export { EvaluationEligibilitySetting, EvaluationEligibilitySettingEmployee, } from './models/EvaluationEligibilitySettingModel';
412
- export { isEvaluationEligibilityWindowOpen, parseEvaluationEndDay, parseMonthRange, } from './helpers/evaluation-eligibility.utils';
412
+ export { isEvaluationEligibilityWindowOpen, isCalendarMonthInEligibilityRange, mmDdWindowsOverlap, mmDdSortKey, parseMonthDay, parseEligibilityDateRange, parseEvaluationEndDay, parseMonthRange, } from './helpers/evaluation-eligibility.utils';
413
413
  export { DEFAULT_MAX_EMPLOYEES_PER_REQUEST, normalizeEmployeeSubmissions, resolveMaxEmployeesPerRequest, persistEmployeeEvaluationScores, } from './helpers/employee-evaluation-request.utils';
414
414
  export type { EmployeeEvaluationSubmitAnswer, EmployeeEvaluationSubmitEmployee, NormalizedEmployeeSubmission, } from './helpers/employee-evaluation-request.utils';
415
415
  export { EmployeeEvaluationRequests, EmployeeEvaluationRequestStatus, EmployeeEvaluationRoutingBucket, } from './models/EmployeeEvaluationRequestModel';
package/dist/index.js CHANGED
@@ -15,8 +15,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.collectFmServicesNotificationConfigRecipients = exports.ServicesNotificationTriggerType = exports.AppointmentWorkFlowStatus = exports.AppointmentMessageType = exports.AppointmentAttendeeStatus = exports.AppointmentApprovalStatus = exports.AppointmentAttendees = exports.AppointmentWorkFlow = exports.AppointmentRequestChat = exports.AppointmentRequestAttachment = exports.AppointmentApprovalDetails = exports.AppointmentRequestStatus = exports.AppointmentRequests = exports.TravelClass = exports.MissionTravelWorkFlowStatus = exports.MissionTravelApprovalStatus = exports.AllowanceRatio = exports.DecisionType = exports.MissionType = exports.MissionTravelStatus = exports.GatePassMessageType = exports.GatePassWorkFlowStatus = exports.GatePassApprovalStatus = exports.GatePassType = exports.GatePassRequestStatus = exports.SecurityDeptMessageType = exports.SecurityDeptAccessType = exports.SecurityDeptRequestStatus = exports.RetiredCardMessageType = exports.RetiredCardWorkFlowStatus = exports.RetiredCardApprovalStatus = exports.RetiredCardAccessType = exports.RetiredCardRequestStatus = exports.TitleCategory = exports.ProfileUpdateRequestStatus = exports.StayAfterHoursTransactionStatus = exports.StayAfterHoursTransaction = exports.StayAfterHoursBalance = exports.EarlyCheckoutTransactionStatus = exports.EarlyCheckoutTransaction = exports.EarlyCheckoutFrequency = exports.EarlyCheckoutConfiguration = exports.EarlyCheckoutBalance = exports.LeaveTransactionStatus = exports.LeaveTransaction = exports.LeaveConfigurationGrades = exports.enumFrequency = exports.LeaveConfiguration = exports.ParcelDepartmentCategory = exports.RegisterCandidateExperienceActivity = void 0;
18
- exports.InitiatorEmployeeNominationRequestAttachment = exports.InitiatorEmployeeNominationMessageType = exports.InitiatorEmployeeNominationRequestChat = exports.InitiatorEmployeeNominationWorkFlowStatus = exports.InitiatorEmployeeNominationWorkFlow = exports.InitiatorEmployeeNominationApprovalStatus = exports.InitiatorEmployeeNominationApprovalDetails = exports.InitiatorInitiativeType = exports.InitiatorEmployeeNominationRequestStatus = exports.InitiatorEmployeeNominationRequests = exports.IpeGrievanceRequestAttachment = exports.IpeGrievanceMessageType = exports.IpeGrievanceRequestChat = exports.IpeGrievanceWorkFlowStatus = exports.IpeGrievanceWorkFlow = exports.IpeGrievanceApprovalStatus = exports.IpeGrievanceApprovalDetails = exports.IpeGrievancePeriodRating = exports.IpeGrievanceExemptionReason = exports.IpeGrievancePeriodType = exports.IpeGrievanceFinalEvaluationResult = exports.IpeGrievanceRequestStatus = exports.IpeGrievanceRequests = exports.EmployeeEvaluationRequestAttachment = exports.EmployeeEvaluationMessageType = exports.EmployeeEvaluationRequestChat = exports.EmployeeEvaluationWorkFlowStatus = exports.EmployeeEvaluationWorkFlow = exports.EmployeeEvaluationApprovalStatus = exports.EmployeeEvaluationApprovalDetails = exports.EmployeeEvaluationPersonScore = exports.EmployeeEvaluationAnswers = exports.EmployeeEvaluationUsFeedback = exports.EmployeeEvaluation = exports.EmployeeEvaluationRoutingBucket = exports.EmployeeEvaluationRequestStatus = exports.EmployeeEvaluationRequests = exports.persistEmployeeEvaluationScores = exports.resolveMaxEmployeesPerRequest = exports.normalizeEmployeeSubmissions = exports.DEFAULT_MAX_EMPLOYEES_PER_REQUEST = exports.parseMonthRange = exports.parseEvaluationEndDay = exports.isEvaluationEligibilityWindowOpen = exports.EvaluationEligibilitySettingEmployee = exports.EvaluationEligibilitySetting = exports.isPortalAdminFromRequest = exports.userHasPortalAdminRole = exports.parsePortalUserIdFromRequest = exports.sendFmServicesNotificationConfigNotifications = void 0;
19
- exports.EvaluationFormQuestionType = exports.EvaluationFormQuestion = exports.EvaluationFormSection = exports.EvaluationFormType = exports.EvaluationForm = exports.EmbassyEvaluationRequestAttachment = exports.EmbassyEvaluationMessageType = exports.EmbassyEvaluationRequestChat = exports.EmbassyEvaluationWorkFlowStatus = exports.EmbassyEvaluationWorkFlow = exports.EmbassyEvaluationApprovalStatus = exports.EmbassyEvaluationApprovalDetails = exports.EmbassyEvaluationRequestStatus = exports.EmbassyEvaluationRequests = exports.EmbassyEvaluationResponse = exports.EmbassyEvaluationAssignmentStatus = exports.EmbassyEvaluationAssignment = exports.EmbassyEvaluationCycleStatus = exports.EmbassyEvaluationCycle = exports.EmployeeOfMonthSupportNominationRequestAttachment = exports.EmployeeOfMonthSupportNominationMessageType = exports.EmployeeOfMonthSupportNominationRequestChat = exports.EmployeeOfMonthSupportNominationWorkFlowStatus = exports.EmployeeOfMonthSupportNominationWorkFlow = exports.EmployeeOfMonthSupportNominationApprovalStatus = exports.EmployeeOfMonthSupportNominationApprovalDetails = exports.EmployeeOfMonthSupportNominationRequestStatus = exports.EmployeeOfMonthSupportNominationRequests = exports.EmployeeOfMonthNominationRequestAttachment = exports.EmployeeOfMonthNominationMessageType = exports.EmployeeOfMonthNominationRequestChat = exports.EmployeeOfMonthNominationWorkFlowStatus = exports.EmployeeOfMonthNominationWorkFlow = exports.EmployeeOfMonthNominationApprovalStatus = exports.EmployeeOfMonthNominationApprovalDetails = exports.EmployeeOfMonthNominationRequestStatus = exports.EmployeeOfMonthNominationRequests = exports.InnovativeEmployeeNominationRequestAttachment = exports.InnovativeEmployeeNominationMessageType = exports.InnovativeEmployeeNominationRequestChat = exports.InnovativeEmployeeNominationWorkFlowStatus = exports.InnovativeEmployeeNominationWorkFlow = exports.InnovativeEmployeeNominationApprovalStatus = exports.InnovativeEmployeeNominationApprovalDetails = exports.InnovativeEmployeeNominationRequestStatus = exports.InnovativeEmployeeNominationRequests = void 0;
18
+ exports.InitiatorEmployeeNominationApprovalStatus = exports.InitiatorEmployeeNominationApprovalDetails = exports.InitiatorInitiativeType = exports.InitiatorEmployeeNominationRequestStatus = exports.InitiatorEmployeeNominationRequests = exports.IpeGrievanceRequestAttachment = exports.IpeGrievanceMessageType = exports.IpeGrievanceRequestChat = exports.IpeGrievanceWorkFlowStatus = exports.IpeGrievanceWorkFlow = exports.IpeGrievanceApprovalStatus = exports.IpeGrievanceApprovalDetails = exports.IpeGrievancePeriodRating = exports.IpeGrievanceExemptionReason = exports.IpeGrievancePeriodType = exports.IpeGrievanceFinalEvaluationResult = exports.IpeGrievanceRequestStatus = exports.IpeGrievanceRequests = exports.EmployeeEvaluationRequestAttachment = exports.EmployeeEvaluationMessageType = exports.EmployeeEvaluationRequestChat = exports.EmployeeEvaluationWorkFlowStatus = exports.EmployeeEvaluationWorkFlow = exports.EmployeeEvaluationApprovalStatus = exports.EmployeeEvaluationApprovalDetails = exports.EmployeeEvaluationPersonScore = exports.EmployeeEvaluationAnswers = exports.EmployeeEvaluationUsFeedback = exports.EmployeeEvaluation = exports.EmployeeEvaluationRoutingBucket = exports.EmployeeEvaluationRequestStatus = exports.EmployeeEvaluationRequests = exports.persistEmployeeEvaluationScores = exports.resolveMaxEmployeesPerRequest = exports.normalizeEmployeeSubmissions = exports.DEFAULT_MAX_EMPLOYEES_PER_REQUEST = exports.parseMonthRange = exports.parseEvaluationEndDay = exports.parseEligibilityDateRange = exports.parseMonthDay = exports.mmDdSortKey = exports.mmDdWindowsOverlap = exports.isCalendarMonthInEligibilityRange = exports.isEvaluationEligibilityWindowOpen = exports.EvaluationEligibilitySettingEmployee = exports.EvaluationEligibilitySetting = exports.isPortalAdminFromRequest = exports.userHasPortalAdminRole = exports.parsePortalUserIdFromRequest = exports.sendFmServicesNotificationConfigNotifications = void 0;
19
+ exports.EvaluationFormQuestion = exports.EvaluationFormSection = exports.EvaluationFormType = exports.EvaluationForm = exports.EmbassyEvaluationRequestAttachment = exports.EmbassyEvaluationMessageType = exports.EmbassyEvaluationRequestChat = exports.EmbassyEvaluationWorkFlowStatus = exports.EmbassyEvaluationWorkFlow = exports.EmbassyEvaluationApprovalStatus = exports.EmbassyEvaluationApprovalDetails = exports.EmbassyEvaluationRequestStatus = exports.EmbassyEvaluationRequests = exports.EmbassyEvaluationResponse = exports.EmbassyEvaluationAssignmentStatus = exports.EmbassyEvaluationAssignment = exports.EmbassyEvaluationCycleStatus = exports.EmbassyEvaluationCycle = exports.EmployeeOfMonthSupportNominationRequestAttachment = exports.EmployeeOfMonthSupportNominationMessageType = exports.EmployeeOfMonthSupportNominationRequestChat = exports.EmployeeOfMonthSupportNominationWorkFlowStatus = exports.EmployeeOfMonthSupportNominationWorkFlow = exports.EmployeeOfMonthSupportNominationApprovalStatus = exports.EmployeeOfMonthSupportNominationApprovalDetails = exports.EmployeeOfMonthSupportNominationRequestStatus = exports.EmployeeOfMonthSupportNominationRequests = exports.EmployeeOfMonthNominationRequestAttachment = exports.EmployeeOfMonthNominationMessageType = exports.EmployeeOfMonthNominationRequestChat = exports.EmployeeOfMonthNominationWorkFlowStatus = exports.EmployeeOfMonthNominationWorkFlow = exports.EmployeeOfMonthNominationApprovalStatus = exports.EmployeeOfMonthNominationApprovalDetails = exports.EmployeeOfMonthNominationRequestStatus = exports.EmployeeOfMonthNominationRequests = exports.InnovativeEmployeeNominationRequestAttachment = exports.InnovativeEmployeeNominationMessageType = exports.InnovativeEmployeeNominationRequestChat = exports.InnovativeEmployeeNominationWorkFlowStatus = exports.InnovativeEmployeeNominationWorkFlow = exports.InnovativeEmployeeNominationApprovalStatus = exports.InnovativeEmployeeNominationApprovalDetails = exports.InnovativeEmployeeNominationRequestStatus = exports.InnovativeEmployeeNominationRequests = exports.InitiatorEmployeeNominationRequestAttachment = exports.InitiatorEmployeeNominationMessageType = exports.InitiatorEmployeeNominationRequestChat = exports.InitiatorEmployeeNominationWorkFlowStatus = exports.InitiatorEmployeeNominationWorkFlow = void 0;
20
+ exports.EvaluationFormQuestionType = void 0;
20
21
  __exportStar(require("./models/user"), exports);
21
22
  __exportStar(require("./models/role"), exports);
22
23
  __exportStar(require("./models/user-sessions"), exports);
@@ -505,6 +506,11 @@ Object.defineProperty(exports, "EvaluationEligibilitySetting", { enumerable: tru
505
506
  Object.defineProperty(exports, "EvaluationEligibilitySettingEmployee", { enumerable: true, get: function () { return EvaluationEligibilitySettingModel_1.EvaluationEligibilitySettingEmployee; } });
506
507
  var evaluation_eligibility_utils_1 = require("./helpers/evaluation-eligibility.utils");
507
508
  Object.defineProperty(exports, "isEvaluationEligibilityWindowOpen", { enumerable: true, get: function () { return evaluation_eligibility_utils_1.isEvaluationEligibilityWindowOpen; } });
509
+ Object.defineProperty(exports, "isCalendarMonthInEligibilityRange", { enumerable: true, get: function () { return evaluation_eligibility_utils_1.isCalendarMonthInEligibilityRange; } });
510
+ Object.defineProperty(exports, "mmDdWindowsOverlap", { enumerable: true, get: function () { return evaluation_eligibility_utils_1.mmDdWindowsOverlap; } });
511
+ Object.defineProperty(exports, "mmDdSortKey", { enumerable: true, get: function () { return evaluation_eligibility_utils_1.mmDdSortKey; } });
512
+ Object.defineProperty(exports, "parseMonthDay", { enumerable: true, get: function () { return evaluation_eligibility_utils_1.parseMonthDay; } });
513
+ Object.defineProperty(exports, "parseEligibilityDateRange", { enumerable: true, get: function () { return evaluation_eligibility_utils_1.parseEligibilityDateRange; } });
508
514
  Object.defineProperty(exports, "parseEvaluationEndDay", { enumerable: true, get: function () { return evaluation_eligibility_utils_1.parseEvaluationEndDay; } });
509
515
  Object.defineProperty(exports, "parseMonthRange", { enumerable: true, get: function () { return evaluation_eligibility_utils_1.parseMonthRange; } });
510
516
  var employee_evaluation_request_utils_1 = require("./helpers/employee-evaluation-request.utils");
@@ -11,6 +11,11 @@ export declare class EmbassyEvaluationCycle extends BaseModel {
11
11
  start_date: string;
12
12
  /** End of evaluation window, format MM-DD. Submissions locked after this date in the active year. */
13
13
  end_date: string;
14
+ /**
15
+ * Last day submissions are allowed (MM-DD). Closing reminders use this date.
16
+ * When null, `end_date` is used for submission and reminders.
17
+ */
18
+ last_evaluation_date: string | null;
14
19
  status: EmbassyEvaluationCycleStatus;
15
20
  service_id: number | null;
16
21
  sub_service_id: number | null;
@@ -34,6 +34,10 @@ __decorate([
34
34
  (0, typeorm_1.Column)({ type: 'varchar', length: 10, nullable: false }),
35
35
  __metadata("design:type", String)
36
36
  ], EmbassyEvaluationCycle.prototype, "end_date", void 0);
37
+ __decorate([
38
+ (0, typeorm_1.Column)({ type: 'varchar', length: 10, nullable: true }),
39
+ __metadata("design:type", Object)
40
+ ], EmbassyEvaluationCycle.prototype, "last_evaluation_date", void 0);
37
41
  __decorate([
38
42
  (0, typeorm_1.Column)({
39
43
  type: 'enum',
@@ -1,25 +1,25 @@
1
1
  import { BaseModel } from './BaseModel';
2
2
  /**
3
- * Department/section/month evaluation window: which employees are excluded or re-included
4
- * for that cycle (e.g. excluded in January, eligible again in May via a different month row).
3
+ * Cycle-based evaluation exception list: defines the evaluation window (MM-DD dates),
4
+ * last submission day, and employees excluded from evaluation for that cycle.
5
5
  */
6
6
  export declare class EvaluationEligibilitySetting extends BaseModel {
7
- department_id: number;
8
- section_id: number;
9
- /** Evaluation window start month (1–12). */
10
- from_month: number;
11
- /** Evaluation window end month (1–12), inclusive. */
12
- to_month: number;
13
- /** Last day of the evaluation month (1–31) when submissions close. */
14
- evaluation_end_date: number;
7
+ cycle_name: string;
8
+ cycle_code: string;
9
+ /** Evaluation window start date as MM-DD (e.g. 01-01). */
10
+ from_month: string;
11
+ /** Evaluation window end date as MM-DD (e.g. 05-31), inclusive. */
12
+ to_month: string;
13
+ /** Last submission date as MM-DD; day applies each month in the window (e.g. 04-12 through the 12th). */
14
+ evaluation_end_date: string;
15
15
  is_active: boolean;
16
- /** Maximum employees allowed in one evaluation request for this dept/section window. */
16
+ /** Maximum employees allowed in one evaluation request for this cycle window. */
17
17
  max_employees_per_request: number;
18
18
  employees?: EvaluationEligibilitySettingEmployee[];
19
19
  }
20
20
  export declare class EvaluationEligibilitySettingEmployee extends BaseModel {
21
21
  evaluation_eligibility_setting: EvaluationEligibilitySetting;
22
22
  employee_id: number;
23
- /** When true, employee is excluded from evaluation for this setting’s month/department/section. */
23
+ /** When true, employee is excluded from evaluation for this cycle window. */
24
24
  is_excluded_from_evaluation: boolean;
25
25
  }
@@ -13,31 +13,31 @@ exports.EvaluationEligibilitySettingEmployee = exports.EvaluationEligibilitySett
13
13
  const typeorm_1 = require("typeorm");
14
14
  const BaseModel_1 = require("./BaseModel");
15
15
  /**
16
- * Department/section/month evaluation window: which employees are excluded or re-included
17
- * for that cycle (e.g. excluded in January, eligible again in May via a different month row).
16
+ * Cycle-based evaluation exception list: defines the evaluation window (MM-DD dates),
17
+ * last submission day, and employees excluded from evaluation for that cycle.
18
18
  */
19
19
  let EvaluationEligibilitySetting = class EvaluationEligibilitySetting extends BaseModel_1.BaseModel {
20
20
  };
21
21
  exports.EvaluationEligibilitySetting = EvaluationEligibilitySetting;
22
22
  __decorate([
23
- (0, typeorm_1.Column)({ type: 'int' }),
24
- __metadata("design:type", Number)
25
- ], EvaluationEligibilitySetting.prototype, "department_id", void 0);
23
+ (0, typeorm_1.Column)({ type: 'varchar', length: 255 }),
24
+ __metadata("design:type", String)
25
+ ], EvaluationEligibilitySetting.prototype, "cycle_name", void 0);
26
26
  __decorate([
27
- (0, typeorm_1.Column)({ type: 'int' }),
28
- __metadata("design:type", Number)
29
- ], EvaluationEligibilitySetting.prototype, "section_id", void 0);
27
+ (0, typeorm_1.Column)({ type: 'varchar', length: 100 }),
28
+ __metadata("design:type", String)
29
+ ], EvaluationEligibilitySetting.prototype, "cycle_code", void 0);
30
30
  __decorate([
31
- (0, typeorm_1.Column)({ type: 'int' }),
32
- __metadata("design:type", Number)
31
+ (0, typeorm_1.Column)({ type: 'varchar', length: 5 }),
32
+ __metadata("design:type", String)
33
33
  ], EvaluationEligibilitySetting.prototype, "from_month", void 0);
34
34
  __decorate([
35
- (0, typeorm_1.Column)({ type: 'int' }),
36
- __metadata("design:type", Number)
35
+ (0, typeorm_1.Column)({ type: 'varchar', length: 5 }),
36
+ __metadata("design:type", String)
37
37
  ], EvaluationEligibilitySetting.prototype, "to_month", void 0);
38
38
  __decorate([
39
- (0, typeorm_1.Column)({ type: 'int' }),
40
- __metadata("design:type", Number)
39
+ (0, typeorm_1.Column)({ type: 'varchar', length: 5 }),
40
+ __metadata("design:type", String)
41
41
  ], EvaluationEligibilitySetting.prototype, "evaluation_end_date", void 0);
42
42
  __decorate([
43
43
  (0, typeorm_1.Column)({ type: 'boolean', default: true }),
package/package.json CHANGED
@@ -1,35 +1,35 @@
1
- {
2
- "name": "@platform-modules/foreign-ministry",
3
- "version": "1.3.327",
4
- "main": "dist/index.js",
5
- "types": "dist/index.d.ts",
6
- "exports": {
7
- ".": {
8
- "types": "./dist/index.d.ts",
9
- "default": "./dist/index.js"
10
- },
11
- "./sla-report-views": {
12
- "types": "./dist/sla-report-views.d.ts",
13
- "default": "./dist/sla-report-views.js"
14
- }
15
- },
16
- "scripts": {
17
- "build": "tsc",
18
- "dev": "ts-node src/scripts.ts",
19
- "sync:sla-sql": "node scripts/sync-sla-reports-sql.js"
20
- },
21
- "publishConfig": {
22
- "access": "public"
23
- },
24
- "dependencies": {
25
- "moment-timezone": "^0.6.0",
26
- "pg": "^8.16.0",
27
- "typeorm": "^0.3.17"
28
- },
29
- "devDependencies": {
30
- "@types/moment-timezone": "^0.5.30",
31
- "dotenv": "^16.5.0",
32
- "ts-node": "^10.9.2",
33
- "typescript": "^5.2.0"
34
- }
35
- }
1
+ {
2
+ "name": "@platform-modules/foreign-ministry",
3
+ "version": "1.3.333",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ },
11
+ "./sla-report-views": {
12
+ "types": "./dist/sla-report-views.d.ts",
13
+ "default": "./dist/sla-report-views.js"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "ts-node src/scripts.ts",
19
+ "sync:sla-sql": "node scripts/sync-sla-reports-sql.js"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "dependencies": {
25
+ "moment-timezone": "^0.6.0",
26
+ "pg": "^8.16.0",
27
+ "typeorm": "^0.3.17"
28
+ },
29
+ "devDependencies": {
30
+ "@types/moment-timezone": "^0.5.30",
31
+ "dotenv": "^16.5.0",
32
+ "ts-node": "^10.9.2",
33
+ "typescript": "^5.2.0"
34
+ }
35
+ }
@@ -1,181 +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
-
8
- export const DEFAULT_MAX_EMPLOYEES_PER_REQUEST = 50;
9
-
10
- export type EmployeeEvaluationSubmitAnswer = {
11
- section_id: number;
12
- question_id: number;
13
- score: number;
14
- remarks?: string | null;
15
- };
16
-
17
- export type EmployeeEvaluationSubmitEmployee = {
18
- user_id: number;
19
- is_rca?: boolean;
20
- answers: EmployeeEvaluationSubmitAnswer[];
21
- };
22
-
23
- export type NormalizedEmployeeSubmission = EmployeeEvaluationSubmitEmployee;
24
-
25
- export function normalizeEmployeeSubmissions(raw: unknown): {
26
- employees: NormalizedEmployeeSubmission[];
27
- error?: string;
28
- } {
29
- if (!Array.isArray(raw) || raw.length === 0) {
30
- return { employees: [], error: 'employees must be a non-empty array' };
31
- }
32
- const seen = new Set<number>();
33
- const employees: NormalizedEmployeeSubmission[] = [];
34
- for (const item of raw) {
35
- if (item == null || typeof item !== 'object') {
36
- return { employees: [], error: 'Each employee entry must be an object' };
37
- }
38
- const o = item as Record<string, unknown>;
39
- const user_id = Number(o.user_id ?? o.employee_id);
40
- if (!Number.isFinite(user_id) || user_id <= 0) {
41
- return { employees: [], error: 'Each employee must have a valid user_id' };
42
- }
43
- if (seen.has(user_id)) {
44
- return { employees: [], error: `Duplicate user_id ${user_id} in employees list` };
45
- }
46
- seen.add(user_id);
47
- const answersRaw = o.answers;
48
- if (!Array.isArray(answersRaw) || answersRaw.length === 0) {
49
- return { employees: [], error: `Employee ${user_id} must include a non-empty answers array` };
50
- }
51
- const answers: EmployeeEvaluationSubmitAnswer[] = [];
52
- const qSeen = new Set<number>();
53
- for (const a of answersRaw) {
54
- if (a == null || typeof a !== 'object') {
55
- return { employees: [], error: `Invalid answer for employee ${user_id}` };
56
- }
57
- const ar = a as Record<string, unknown>;
58
- const section_id = Number(ar.section_id);
59
- const question_id = Number(ar.question_id);
60
- const score = Number(ar.score);
61
- if (!Number.isFinite(section_id) || !Number.isFinite(question_id)) {
62
- return { employees: [], error: `section_id and question_id required for employee ${user_id}` };
63
- }
64
- if (!Number.isFinite(score) || score < 0) {
65
- return { employees: [], error: `score must be a non-negative number for employee ${user_id}, question ${question_id}` };
66
- }
67
- if (qSeen.has(question_id)) {
68
- return { employees: [], error: `Duplicate question_id ${question_id} for employee ${user_id}` };
69
- }
70
- qSeen.add(question_id);
71
- answers.push({
72
- section_id,
73
- question_id,
74
- score,
75
- remarks: ar.remarks != null ? String(ar.remarks) : null,
76
- });
77
- }
78
- employees.push({
79
- user_id,
80
- is_rca: o.is_rca !== undefined ? Boolean(o.is_rca) : false,
81
- answers,
82
- });
83
- }
84
- return { employees };
85
- }
86
-
87
- /** Resolve max employees from active eligibility setting for dept/section/month. */
88
- export async function resolveMaxEmployeesPerRequest(
89
- manager: EntityManager,
90
- department_id: number,
91
- section_id: number,
92
- month: number
93
- ): Promise<number> {
94
- const setting = await manager
95
- .getRepository(EvaluationEligibilitySetting)
96
- .createQueryBuilder('s')
97
- .where('s.is_deleted = false')
98
- .andWhere('s.is_active = true')
99
- .andWhere('s.department_id = :department_id', { department_id })
100
- .andWhere('s.section_id = :section_id', { section_id })
101
- .andWhere('s.from_month <= :month', { month })
102
- .andWhere('s.to_month >= :month', { month })
103
- .orderBy('s.id', 'DESC')
104
- .getOne();
105
- const max = setting?.max_employees_per_request;
106
- if (max != null && Number.isFinite(Number(max)) && Number(max) > 0) {
107
- return Math.floor(Number(max));
108
- }
109
- return DEFAULT_MAX_EMPLOYEES_PER_REQUEST;
110
- }
111
-
112
- export async function persistEmployeeEvaluationScores(
113
- manager: EntityManager,
114
- opts: {
115
- requestId: number;
116
- formId: number;
117
- createdBy: string;
118
- employees: NormalizedEmployeeSubmission[];
119
- }
120
- ): Promise<{ totalScore: number; averageScore: number; employeeCount: number }> {
121
- const { requestId, formId, createdBy, employees } = opts;
122
- let requestTotal = 0;
123
-
124
- for (const emp of employees) {
125
- let personTotal = 0;
126
- for (const ans of emp.answers) {
127
- const qMeta = await manager.findOne(EvaluationFormQuestion, {
128
- where: { id: ans.question_id, is_deleted: false },
129
- });
130
- const questionSectionId = qMeta?.form_section_id;
131
- if (!qMeta || questionSectionId == null || questionSectionId !== ans.section_id) {
132
- throw new Error(`Invalid question ${ans.question_id} for section ${ans.section_id}`);
133
- }
134
- if (qMeta.max_score != null && ans.score > qMeta.max_score) {
135
- throw new Error(
136
- `score ${ans.score} exceeds max_score ${qMeta.max_score} for question ${ans.question_id}`
137
- );
138
- }
139
- personTotal += ans.score;
140
- await manager.save(
141
- EmployeeEvaluationAnswers,
142
- manager.create(EmployeeEvaluationAnswers, {
143
- request_id: requestId,
144
- user_id: emp.user_id,
145
- form_id: formId,
146
- section_id: ans.section_id,
147
- question_id: ans.question_id,
148
- score: ans.score,
149
- remarks: ans.remarks ?? null,
150
- created_by: createdBy,
151
- is_deleted: false,
152
- })
153
- );
154
- }
155
-
156
- await manager.save(
157
- EmployeeEvaluationPersonScore,
158
- manager.create(EmployeeEvaluationPersonScore, {
159
- request_id: requestId,
160
- user_id: emp.user_id,
161
- is_rca: Boolean(emp.is_rca),
162
- us_feedback: null,
163
- total_score: personTotal,
164
- created_by: createdBy,
165
- is_deleted: false,
166
- })
167
- );
168
- requestTotal += personTotal;
169
- }
170
-
171
- const employeeCount = employees.length;
172
- const averageScore = employeeCount ? requestTotal / employeeCount : 0;
173
- await manager.update(EmployeeEvaluationRequests, { id: requestId }, {
174
- total_score: requestTotal,
175
- average_score: averageScore,
176
- employee_count: employeeCount,
177
- updated_by: createdBy,
178
- });
179
-
180
- return { totalScore: requestTotal, averageScore, employeeCount };
181
- }
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,36 +1,126 @@
1
- /**
2
- * Whether evaluations are still open for a setting's month range.
3
- * `evaluation_end_date` is the last calendar day (1–31) of each month in the range.
4
- */
5
- export function isEvaluationEligibilityWindowOpen(
6
- fromMonth: number,
7
- toMonth: number,
8
- evaluationEndDay: number,
9
- referenceDate: Date = new Date()
10
- ): boolean {
11
- const month = referenceDate.getUTCMonth() + 1;
12
- const day = referenceDate.getUTCDate();
13
- if (month < fromMonth || month > toMonth) return false;
14
- if (!Number.isFinite(evaluationEndDay) || evaluationEndDay < 1 || evaluationEndDay > 31) return false;
15
- return day <= evaluationEndDay;
16
- }
17
-
18
- export function parseEvaluationEndDay(input: unknown): number | null {
19
- if (input == null || input === '') return null;
20
- const n = typeof input === 'number' ? input : parseInt(String(input).trim(), 10);
21
- if (!Number.isFinite(n) || n < 1 || n > 31) return null;
22
- return Math.floor(n);
23
- }
24
-
25
- export function parseMonthRange(
26
- fromInput: unknown,
27
- toInput: unknown
28
- ): { from_month: number; to_month: number } | null {
29
- const from_month = typeof fromInput === 'number' ? fromInput : parseInt(String(fromInput).trim(), 10);
30
- const to_month = typeof toInput === 'number' ? toInput : parseInt(String(toInput).trim(), 10);
31
- if (!Number.isFinite(from_month) || !Number.isFinite(to_month)) return null;
32
- if (!Number.isInteger(from_month) || !Number.isInteger(to_month)) return null;
33
- if (from_month < 1 || from_month > 12 || to_month < 1 || to_month > 12) return null;
34
- if (from_month > to_month) return null;
35
- return { from_month, to_month };
36
- }
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
+ }
package/src/index.ts CHANGED
@@ -448,6 +448,11 @@ export {
448
448
  } from './models/EvaluationEligibilitySettingModel';
449
449
  export {
450
450
  isEvaluationEligibilityWindowOpen,
451
+ isCalendarMonthInEligibilityRange,
452
+ mmDdWindowsOverlap,
453
+ mmDdSortKey,
454
+ parseMonthDay,
455
+ parseEligibilityDateRange,
451
456
  parseEvaluationEndDay,
452
457
  parseMonthRange,
453
458
  } from './helpers/evaluation-eligibility.utils';
@@ -21,6 +21,13 @@ export class EmbassyEvaluationCycle extends BaseModel {
21
21
  @Column({ type: 'varchar', length: 10, nullable: false })
22
22
  end_date: string;
23
23
 
24
+ /**
25
+ * Last day submissions are allowed (MM-DD). Closing reminders use this date.
26
+ * When null, `end_date` is used for submission and reminders.
27
+ */
28
+ @Column({ type: 'varchar', length: 10, nullable: true })
29
+ last_evaluation_date: string | null;
30
+
24
31
  @Column({
25
32
  type: 'enum',
26
33
  enum: EmbassyEvaluationCycleStatus,
@@ -2,33 +2,33 @@ import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
2
2
  import { BaseModel } from './BaseModel';
3
3
 
4
4
  /**
5
- * Department/section/month evaluation window: which employees are excluded or re-included
6
- * for that cycle (e.g. excluded in January, eligible again in May via a different month row).
5
+ * Cycle-based evaluation exception list: defines the evaluation window (MM-DD dates),
6
+ * last submission day, and employees excluded from evaluation for that cycle.
7
7
  */
8
8
  @Entity({ name: 'evaluation_eligibility_settings' })
9
9
  export class EvaluationEligibilitySetting extends BaseModel {
10
- @Column({ type: 'int' })
11
- department_id: number;
10
+ @Column({ type: 'varchar', length: 255 })
11
+ cycle_name: string;
12
12
 
13
- @Column({ type: 'int' })
14
- section_id: number;
13
+ @Column({ type: 'varchar', length: 100 })
14
+ cycle_code: string;
15
15
 
16
- /** Evaluation window start month (1–12). */
17
- @Column({ type: 'int' })
18
- from_month: number;
16
+ /** Evaluation window start date as MM-DD (e.g. 01-01). */
17
+ @Column({ type: 'varchar', length: 5 })
18
+ from_month: string;
19
19
 
20
- /** Evaluation window end month (1–12), inclusive. */
21
- @Column({ type: 'int' })
22
- to_month: number;
20
+ /** Evaluation window end date as MM-DD (e.g. 05-31), inclusive. */
21
+ @Column({ type: 'varchar', length: 5 })
22
+ to_month: string;
23
23
 
24
- /** Last day of the evaluation month (1–31) when submissions close. */
25
- @Column({ type: 'int' })
26
- evaluation_end_date: number;
24
+ /** Last submission date as MM-DD; day applies each month in the window (e.g. 04-12 through the 12th). */
25
+ @Column({ type: 'varchar', length: 5 })
26
+ evaluation_end_date: string;
27
27
 
28
28
  @Column({ type: 'boolean', default: true })
29
29
  is_active: boolean;
30
30
 
31
- /** Maximum employees allowed in one evaluation request for this dept/section window. */
31
+ /** Maximum employees allowed in one evaluation request for this cycle window. */
32
32
  @Column({ type: 'int', default: 50 })
33
33
  max_employees_per_request: number;
34
34
 
@@ -45,7 +45,7 @@ export class EvaluationEligibilitySettingEmployee extends BaseModel {
45
45
  @Column({ type: 'int' })
46
46
  employee_id: number;
47
47
 
48
- /** When true, employee is excluded from evaluation for this setting’s month/department/section. */
48
+ /** When true, employee is excluded from evaluation for this cycle window. */
49
49
  @Column({ type: 'boolean', default: true })
50
50
  is_excluded_from_evaluation: boolean;
51
51
  }