@platform-modules/civil-aviation-authority 2.3.292 → 2.3.294

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 (28) hide show
  1. package/dist/data-source.js +2 -0
  2. package/dist/i18n/workflow-chat-i18n.json +9 -1
  3. package/dist/i18n/workflow-chat-message.builder.d.ts +12 -0
  4. package/dist/i18n/workflow-chat-message.builder.js +215 -11
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +1 -0
  7. package/dist/models/ContractServiceRequestDocumentModel.d.ts +13 -0
  8. package/dist/models/ContractServiceRequestDocumentModel.js +60 -0
  9. package/dist/models/ContractServiceRequestModel.d.ts +2 -0
  10. package/dist/models/ContractServiceRequestModel.js +8 -0
  11. package/dist/models/FollowUpReportItemModel.d.ts +5 -2
  12. package/dist/models/FollowUpReportItemModel.js +17 -7
  13. package/dist/models/FollowUpReportRequestModel.d.ts +7 -1
  14. package/dist/models/FollowUpReportRequestModel.js +7 -1
  15. package/dist/models/RespondToEnquiriesAttachmentModel.d.ts +2 -1
  16. package/dist/models/RespondToEnquiriesAttachmentModel.js +7 -2
  17. package/package.json +1 -1
  18. package/sql/caa_api_payload_changes_2026_07.sql +79 -0
  19. package/sql/fix_maintenance_subject_classification_enum_2026_07.sql +48 -0
  20. package/src/data-source.ts +2 -0
  21. package/src/i18n/workflow-chat-i18n.json +9 -1
  22. package/src/i18n/workflow-chat-message.builder.ts +261 -10
  23. package/src/index.ts +3 -1
  24. package/src/models/ContractServiceRequestDocumentModel.ts +35 -0
  25. package/src/models/ContractServiceRequestModel.ts +6 -0
  26. package/src/models/FollowUpReportItemModel.ts +14 -8
  27. package/src/models/FollowUpReportRequestModel.ts +9 -0
  28. package/src/models/RespondToEnquiriesAttachmentModel.ts +6 -1
@@ -30,3 +30,82 @@ ALTER TABLE request_a_tender_requests
30
30
  ALTER TABLE cyber_security_risk_management_risks
31
31
  ADD COLUMN IF NOT EXISTS treatment TEXT,
32
32
  ADD COLUMN IF NOT EXISTS action TEXT;
33
+
34
+ -- 7. Follow-up report: subject_classification enum values
35
+ ALTER TYPE maintenance_subject_classification_en ADD VALUE IF NOT EXISTS 'Important';
36
+ ALTER TYPE maintenance_subject_classification_en ADD VALUE IF NOT EXISTS 'Highly Confidential';
37
+ ALTER TYPE maintenance_subject_classification_en ADD VALUE IF NOT EXISTS 'Restricted';
38
+ ALTER TYPE maintenance_subject_classification_en ADD VALUE IF NOT EXISTS 'Limited';
39
+
40
+ -- 7b. Repair failed TypeORM enum swap (items column can remain on _old type)
41
+ DO $$
42
+ BEGIN
43
+ IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'maintenance_subject_classification_en_old') THEN
44
+ IF EXISTS (
45
+ SELECT 1
46
+ FROM information_schema.columns
47
+ WHERE table_schema = 'public'
48
+ AND table_name = 'followup_report_items'
49
+ AND column_name = 'subject_classification'
50
+ AND udt_name = 'maintenance_subject_classification_en_old'
51
+ ) THEN
52
+ ALTER TABLE followup_report_items
53
+ ALTER COLUMN subject_classification TYPE maintenance_subject_classification_en
54
+ USING subject_classification::text::maintenance_subject_classification_en;
55
+ END IF;
56
+
57
+ IF EXISTS (
58
+ SELECT 1
59
+ FROM information_schema.columns
60
+ WHERE table_schema = 'public'
61
+ AND table_name = 'followup_report_requests'
62
+ AND column_name = 'subject_classification'
63
+ AND udt_name = 'maintenance_subject_classification_en_old'
64
+ ) THEN
65
+ ALTER TABLE followup_report_requests
66
+ ALTER COLUMN subject_classification TYPE maintenance_subject_classification_en
67
+ USING subject_classification::text::maintenance_subject_classification_en;
68
+ END IF;
69
+
70
+ DROP TYPE maintenance_subject_classification_en_old;
71
+ END IF;
72
+ END $$;
73
+
74
+ -- 7c. Report items use varchar (nullable) so TypeORM sync does not fight shared PG enum on two tables
75
+ ALTER TABLE followup_report_items
76
+ ALTER COLUMN subject_classification TYPE VARCHAR(100)
77
+ USING subject_classification::text;
78
+
79
+ -- 8. Contract service request: notes, company phone, contract documents table
80
+ ALTER TABLE contract_service_requests
81
+ ADD COLUMN IF NOT EXISTS notes TEXT,
82
+ ADD COLUMN IF NOT EXISTS company_phone_number VARCHAR(30);
83
+
84
+ CREATE TABLE IF NOT EXISTS contract_service_request_documents (
85
+ id SERIAL PRIMARY KEY,
86
+ created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
87
+ updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
88
+ created_by INT,
89
+ updated_by INT,
90
+ is_deleted BOOLEAN DEFAULT FALSE,
91
+ request_id INT NOT NULL,
92
+ service_id INT,
93
+ sub_service_id INT,
94
+ document_name VARCHAR(500) NOT NULL,
95
+ description TEXT,
96
+ file_url VARCHAR(500) NOT NULL,
97
+ file_name VARCHAR(255),
98
+ file_type VARCHAR(100),
99
+ file_size BIGINT,
100
+ uploaded_by INT NOT NULL
101
+ );
102
+
103
+ ALTER TABLE respond_to_enquiries_request_attachments
104
+ ADD COLUMN IF NOT EXISTS document_name VARCHAR(500);
105
+
106
+ -- 9. Follow-up report items: per-item attachment fields
107
+ ALTER TABLE followup_report_items
108
+ ADD COLUMN IF NOT EXISTS file_url VARCHAR(500),
109
+ ADD COLUMN IF NOT EXISTS file_name VARCHAR(255),
110
+ ADD COLUMN IF NOT EXISTS file_type VARCHAR(100),
111
+ ADD COLUMN IF NOT EXISTS file_size BIGINT;
@@ -0,0 +1,48 @@
1
+ -- Run this BEFORE shared_models synchronize if you see:
2
+ -- cannot drop type maintenance_subject_classification_en_old because other objects depend on it
3
+ --
4
+ -- Safe to re-run.
5
+
6
+ -- Add new enum values on the live type (no-op if already present)
7
+ ALTER TYPE maintenance_subject_classification_en ADD VALUE IF NOT EXISTS 'Important';
8
+ ALTER TYPE maintenance_subject_classification_en ADD VALUE IF NOT EXISTS 'Highly Confidential';
9
+ ALTER TYPE maintenance_subject_classification_en ADD VALUE IF NOT EXISTS 'Restricted';
10
+ ALTER TYPE maintenance_subject_classification_en ADD VALUE IF NOT EXISTS 'Limited';
11
+
12
+ DO $$
13
+ BEGIN
14
+ IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'maintenance_subject_classification_en_old') THEN
15
+ IF EXISTS (
16
+ SELECT 1
17
+ FROM information_schema.columns
18
+ WHERE table_schema = 'public'
19
+ AND table_name = 'followup_report_items'
20
+ AND column_name = 'subject_classification'
21
+ AND udt_name = 'maintenance_subject_classification_en_old'
22
+ ) THEN
23
+ ALTER TABLE followup_report_items
24
+ ALTER COLUMN subject_classification TYPE maintenance_subject_classification_en
25
+ USING subject_classification::text::maintenance_subject_classification_en;
26
+ END IF;
27
+
28
+ IF EXISTS (
29
+ SELECT 1
30
+ FROM information_schema.columns
31
+ WHERE table_schema = 'public'
32
+ AND table_name = 'followup_report_requests'
33
+ AND column_name = 'subject_classification'
34
+ AND udt_name = 'maintenance_subject_classification_en_old'
35
+ ) THEN
36
+ ALTER TABLE followup_report_requests
37
+ ALTER COLUMN subject_classification TYPE maintenance_subject_classification_en
38
+ USING subject_classification::text::maintenance_subject_classification_en;
39
+ END IF;
40
+
41
+ DROP TYPE maintenance_subject_classification_en_old;
42
+ END IF;
43
+ END $$;
44
+
45
+ -- Items column: varchar avoids TypeORM multi-table PG enum sync failures
46
+ ALTER TABLE followup_report_items
47
+ ALTER COLUMN subject_classification TYPE VARCHAR(100)
48
+ USING subject_classification::text;
@@ -269,6 +269,7 @@ import { ComplaintLostPropertyReportChat } from './models/ComplaintLostPropertyR
269
269
  import { ComplaintLostPropertyReportRequest } from './models/ComplaintLostPropertyReportRequestModel';
270
270
  import { ComplaintLostPropertyReportWorkflow } from './models/ComplaintLostPropertyReportWorkflowModel';
271
271
  import { ContractServiceRequest } from './models/ContractServiceRequestModel';
272
+ import { ContractServiceRequestDocument } from './models/ContractServiceRequestDocumentModel';
272
273
  import { CountryMaster } from './models/CountryMasterModel';
273
274
  import { CyberSecurityAuditRequest } from './models/CyberSecurityAuditRequestModel';
274
275
  import { CyberSecurityRiskManagementRequest } from './models/CyberSecurityRiskManagementRequestModel';
@@ -593,6 +594,7 @@ export const AppDataSource = new DataSource({
593
594
  ComplaintLostPropertyReportRequest,
594
595
  ComplaintLostPropertyReportWorkflow,
595
596
  ContractServiceRequest,
597
+ ContractServiceRequestDocument,
596
598
  CountryMaster,
597
599
  CyberSecurityAuditRequest,
598
600
  CyberSecurityRiskManagementRequest,
@@ -40,6 +40,14 @@
40
40
  "en": "Request Approved{{comment}}",
41
41
  "ar": "تمت الموافقة على الطلب{{comment}}"
42
42
  },
43
+ "request_approved_assigned_to_user": {
44
+ "en": "Request Approved: Request assigned to {{assignedUserName}} by {{roleName}} ({{actorUserName}} - {{employeeId}})",
45
+ "ar": "تمت الموافقة على الطلب: تم تعيين الطلب إلى {{assignedUserName}} من قبل {{roleName}} ({{actorUserName}} - {{employeeId}})"
46
+ },
47
+ "request_approved_assigned_to_technician": {
48
+ "en": "Request Approved: Request assigned to technician {{assignedUserName}} by {{roleName}} ({{actorUserName}} - {{employeeId}})",
49
+ "ar": "تمت الموافقة على الطلب: تم تعيين الطلب إلى الفني {{assignedUserName}} من قبل {{roleName}} ({{actorUserName}} - {{employeeId}})"
50
+ },
43
51
  "request_rejected": {
44
52
  "en": "Request Rejected{{comment}}",
45
53
  "ar": "تم رفض الطلب{{comment}}"
@@ -106,7 +114,7 @@
106
114
  },
107
115
  "it_helpdesk_wf_status_by_role": {
108
116
  "en": "Request {{status}} by {{approverRoleName}}{{comment}}",
109
- "ar": "تم {{statusAr}} الطلب من قبل {{approverRoleName}}{{comment}}"
117
+ "ar": "{{statusAr}} الطلب من قبل {{approverRoleName}}{{comment}}"
110
118
  },
111
119
  "it_helpdesk_wf_l3_step_rejected": {
112
120
  "en": "{{roleName}} rejected the request{{comment}}",
@@ -344,6 +344,81 @@ export function buildSynchronizedChatMessage(ctx: ChatMessageContext): Bilingual
344
344
 
345
345
  // ─── Backward-compatible aliases used by Cancellation pilot ───────────────────
346
346
 
347
+ export type RequestApprovedAssignedWorkflowParams = {
348
+ assignedUserName: string;
349
+ roleName: string;
350
+ roleArabicName?: string | null;
351
+ actorUserName: string;
352
+ employeeId: string | number;
353
+ assigneeLabel?: 'user' | 'technician';
354
+ };
355
+
356
+ /** Parse legacy English-only assignment comments used as workflow log suffixes. */
357
+ export function tryParseRequestAssignedComment(
358
+ comment?: string | null,
359
+ ): Omit<RequestApprovedAssignedWorkflowParams, 'roleArabicName'> | null {
360
+ const text = comment?.trim();
361
+ if (!text) return null;
362
+
363
+ const technicianMatch = text.match(
364
+ /^Request assigned to technician (.+?) by (.+?) \((.+?) - (.+?)\)$/,
365
+ );
366
+ if (technicianMatch) {
367
+ return {
368
+ assignedUserName: technicianMatch[1].trim(),
369
+ roleName: technicianMatch[2].trim(),
370
+ actorUserName: technicianMatch[3].trim(),
371
+ employeeId: technicianMatch[4].trim(),
372
+ assigneeLabel: 'technician',
373
+ };
374
+ }
375
+
376
+ const userMatch = text.match(/^Request assigned to (.+?) by (.+?) \((.+?) - (.+?)\)$/);
377
+ if (userMatch) {
378
+ return {
379
+ assignedUserName: userMatch[1].trim(),
380
+ roleName: userMatch[2].trim(),
381
+ actorUserName: userMatch[3].trim(),
382
+ employeeId: userMatch[4].trim(),
383
+ assigneeLabel: 'user',
384
+ };
385
+ }
386
+
387
+ return null;
388
+ }
389
+
390
+ /** Bilingual workflow content for approved assignment steps (HR, IT, Tender, etc.). */
391
+ export function buildRequestApprovedAssignedWorkflowFields(
392
+ params: RequestApprovedAssignedWorkflowParams,
393
+ ): BilingualText {
394
+ const assignedUserEn = params.assignedUserName?.trim() || 'User';
395
+ const roleEn = params.roleName?.trim() || 'Approver';
396
+ const roleAr = resolveBilingualName(roleEn, params.roleArabicName);
397
+ const actorEn = params.actorUserName?.trim() || 'User';
398
+ const employeeId = String(params.employeeId ?? 'N/A');
399
+
400
+ const templateKey =
401
+ params.assigneeLabel === 'technician'
402
+ ? 'request_approved_assigned_to_technician'
403
+ : 'request_approved_assigned_to_user';
404
+
405
+ return renderSynchronizedWorkflowMessage(
406
+ templateKey,
407
+ {
408
+ assignedUserName: assignedUserEn,
409
+ roleName: roleEn,
410
+ actorUserName: actorEn,
411
+ employeeId,
412
+ },
413
+ {
414
+ assignedUserName: assignedUserEn,
415
+ roleName: roleAr,
416
+ actorUserName: actorEn,
417
+ employeeId,
418
+ },
419
+ );
420
+ }
421
+
347
422
  /** Standard workflow log UPDATE fields for all CAA module repositories. */
348
423
  export function buildWorkflowLogUpdateFields(params: {
349
424
  approvalStatus: string;
@@ -352,6 +427,23 @@ export function buildWorkflowLogUpdateFields(params: {
352
427
  roleArabicName?: string | null;
353
428
  comment?: string | null;
354
429
  }): { content: string; content_ar: string; status: string; status_ar: string | null } {
430
+ if (params.approvalStatus === 'Approved') {
431
+ const assignment = tryParseRequestAssignedComment(params.comment);
432
+ if (assignment) {
433
+ const bilingual = buildRequestApprovedAssignedWorkflowFields({
434
+ ...assignment,
435
+ roleName: params.roleName?.trim() || assignment.roleName,
436
+ roleArabicName: params.roleArabicName,
437
+ });
438
+ return {
439
+ content: bilingual.en,
440
+ content_ar: bilingual.ar,
441
+ status: params.workflowStatus,
442
+ status_ar: resolveWorkflowStatusAr(params.workflowStatus),
443
+ };
444
+ }
445
+ }
446
+
355
447
  let action: WorkflowMessageAction;
356
448
  if (params.approvalStatus === 'Approved') action = 'approved';
357
449
  else if (params.approvalStatus === 'Rejected') action = 'rejected';
@@ -845,6 +937,23 @@ export type ItHelpdeskMuscatWorkflowParams =
845
937
  }
846
938
  | { variant: 'routing_resolved' };
847
939
 
940
+ function resolveItHelpdeskGenericStatusAr(status: string): string {
941
+ switch (status.trim()) {
942
+ case 'Approved':
943
+ return 'تمت الموافقة على';
944
+ case 'Rejected':
945
+ return 'تم رفض';
946
+ case 'Closed':
947
+ return 'تم إغلاق';
948
+ case 'Assigned':
949
+ return 'تم تعيين';
950
+ case 'Reassigned':
951
+ return 'تمت إعادة تعيين';
952
+ default:
953
+ return resolveChatStatusAr(status) ?? status.toLowerCase();
954
+ }
955
+ }
956
+
848
957
  export function buildItHelpdeskMuscatWorkflowLog(params: ItHelpdeskMuscatWorkflowParams): BilingualText {
849
958
  const comment = formatColonCommentSuffix(
850
959
  'comment' in params ? params.comment : undefined,
@@ -967,7 +1076,7 @@ export function buildItHelpdeskMuscatWorkflowLog(params: ItHelpdeskMuscatWorkflo
967
1076
  const approverEn = params.approverRoleName.trim();
968
1077
  const approverAr = resolveBilingualName(approverEn, params.approverRoleArabicName);
969
1078
  const statusEn = params.status.toLowerCase();
970
- const statusAr = resolveChatStatusAr(params.status) ?? statusEn;
1079
+ const statusAr = resolveItHelpdeskGenericStatusAr(params.status);
971
1080
  return renderSynchronizedWorkflowMessage(
972
1081
  'it_helpdesk_wf_status_by_role',
973
1082
  { status: statusEn, statusAr, approverRoleName: approverEn, comment },
@@ -1126,13 +1235,20 @@ function resolveArabicChatRoleName(
1126
1235
  actors: BilingualActorNames | undefined,
1127
1236
  parsedEnglishRole?: string,
1128
1237
  ): string {
1238
+ const parsed = parsedEnglishRole?.trim();
1129
1239
  if (actors?.roleName?.trim()) {
1130
- return resolveBilingualName(actors.roleName, actors.roleArabicName);
1240
+ const actorRole = actors.roleName.trim();
1241
+ if (!parsed || parsed.toLowerCase() === actorRole.toLowerCase()) {
1242
+ return resolveBilingualName(actorRole, actors.roleArabicName);
1243
+ }
1244
+ }
1245
+ if (actors?.roleArabicName?.trim() && parsed) {
1246
+ return resolveBilingualName(parsed, actors.roleArabicName);
1131
1247
  }
1132
1248
  if (actors?.userName?.trim()) {
1133
1249
  return resolveBilingualName(actors.userName, actors.userArabicName);
1134
1250
  }
1135
- return resolveBilingualName(parsedEnglishRole, actors?.roleArabicName);
1251
+ return resolveBilingualName(parsed, actors?.roleArabicName);
1136
1252
  }
1137
1253
 
1138
1254
  function escapeRegexLiteral(value: string): string {
@@ -1146,17 +1262,40 @@ function matchTemplateMessage(
1146
1262
  ): Record<string, string> | null {
1147
1263
  if (!/\{\{\w+\}\}/.test(templateEn)) return null;
1148
1264
 
1149
- const placeholders: string[] = [];
1265
+ const captures: Array<{ key: string; groupIndex: number }> = [];
1150
1266
  let regexBody = '';
1151
1267
  let cursor = 0;
1268
+ let groupIndex = 0;
1152
1269
  const tokenRe = /\{\{(\w+)\}\}/g;
1153
1270
  let tokenMatch: RegExpExecArray | null;
1154
1271
 
1155
1272
  while ((tokenMatch = tokenRe.exec(templateEn)) !== null) {
1156
1273
  regexBody += escapeRegexLiteral(templateEn.slice(cursor, tokenMatch.index));
1157
1274
  const key = tokenMatch[1];
1158
- placeholders.push(key);
1159
- regexBody += key === 'comment' ? '(.*)' : '(.+?)';
1275
+ const afterToken = templateEn.slice(tokenMatch.index + tokenMatch[0].length);
1276
+
1277
+ if (key === 'comment') {
1278
+ groupIndex += 1;
1279
+ captures.push({ key, groupIndex });
1280
+ regexBody += '(.*)';
1281
+ cursor = tokenMatch.index + tokenMatch[0].length;
1282
+ continue;
1283
+ }
1284
+
1285
+ if (afterToken.startsWith('{{comment}}')) {
1286
+ groupIndex += 1;
1287
+ captures.push({ key, groupIndex });
1288
+ groupIndex += 1;
1289
+ captures.push({ key: 'comment', groupIndex });
1290
+ regexBody += '([^:]+?)(: .*)?';
1291
+ tokenRe.lastIndex = tokenMatch.index + tokenMatch[0].length + '{{comment}}'.length;
1292
+ cursor = tokenRe.lastIndex;
1293
+ continue;
1294
+ }
1295
+
1296
+ groupIndex += 1;
1297
+ captures.push({ key, groupIndex });
1298
+ regexBody += '(.+?)';
1160
1299
  cursor = tokenMatch.index + tokenMatch[0].length;
1161
1300
  }
1162
1301
  regexBody += escapeRegexLiteral(templateEn.slice(cursor));
@@ -1165,8 +1304,8 @@ function matchTemplateMessage(
1165
1304
  if (!match) return null;
1166
1305
 
1167
1306
  const params: Record<string, string> = {};
1168
- placeholders.forEach((key, index) => {
1169
- params[key] = match[index + 1] ?? '';
1307
+ captures.forEach(({ key, groupIndex: idx }) => {
1308
+ params[key] = match[idx] ?? '';
1170
1309
  });
1171
1310
  return params;
1172
1311
  }
@@ -1187,6 +1326,41 @@ function buildArabicChatTemplateParams(
1187
1326
  return arParams;
1188
1327
  }
1189
1328
 
1329
+ function buildArabicWorkflowTemplateParams(
1330
+ params: Record<string, string>,
1331
+ actors?: BilingualActorNames,
1332
+ ): Record<string, string> {
1333
+ const arParams: Record<string, string> = { ...params };
1334
+ if (params.roleName !== undefined) {
1335
+ arParams.roleName = resolveArabicChatRoleName(actors, params.roleName.trim());
1336
+ }
1337
+ if (params.approverRoleName !== undefined) {
1338
+ arParams.approverRoleName = resolveArabicChatRoleName(actors, params.approverRoleName.trim());
1339
+ }
1340
+ if (params.assignedRoleName !== undefined) {
1341
+ arParams.assignedRoleName = resolveArabicChatRoleName(actors, params.assignedRoleName.trim());
1342
+ }
1343
+ if (params.name !== undefined) {
1344
+ arParams.name = resolveArabicWorkflowActorName(actors, params.name.trim());
1345
+ }
1346
+ if (params.status !== undefined && params.statusAr === undefined) {
1347
+ const normalizedStatus =
1348
+ params.status.charAt(0).toUpperCase() + params.status.slice(1).toLowerCase();
1349
+ arParams.statusAr = resolveItHelpdeskGenericStatusAr(normalizedStatus);
1350
+ }
1351
+ return arParams;
1352
+ }
1353
+
1354
+ function countTemplatePlaceholders(templateEn: string): number {
1355
+ return (templateEn.match(/\{\{\w+\}\}/g) || []).length;
1356
+ }
1357
+
1358
+ function listWorkflowTemplatesBySpecificity(): Array<[string, BilingualText]> {
1359
+ return Object.entries(WORKFLOW_TEMPLATES).sort(
1360
+ ([, a], [, b]) => countTemplatePlaceholders(b.en) - countTemplatePlaceholders(a.en),
1361
+ );
1362
+ }
1363
+
1190
1364
  export function resolveWorkflowContentAr(
1191
1365
  content: string | null | undefined,
1192
1366
  actors?: BilingualActorNames,
@@ -1195,7 +1369,14 @@ export function resolveWorkflowContentAr(
1195
1369
  const normalized = content.trim();
1196
1370
 
1197
1371
  for (const tpl of Object.values(WORKFLOW_TEMPLATES)) {
1198
- if (tpl.en === normalized) return tpl.ar;
1372
+ if (tpl.en === normalized || tpl.en.toLowerCase() === normalized.toLowerCase()) return tpl.ar;
1373
+ }
1374
+
1375
+ for (const [, tpl] of listWorkflowTemplatesBySpecificity()) {
1376
+ if (!/\{\{\w+\}\}/.test(tpl.en)) continue;
1377
+ const params = matchTemplateMessage(tpl.en, normalized);
1378
+ if (!params) continue;
1379
+ return interpolate(tpl.ar, buildArabicWorkflowTemplateParams(params, actors));
1199
1380
  }
1200
1381
 
1201
1382
  const commentIdx = normalized.indexOf(': ');
@@ -1233,6 +1414,31 @@ export function resolveWorkflowContentAr(
1233
1414
  return interpolate(WORKFLOW_TEMPLATES.request_approval_pending_from.ar, { name: nameAr });
1234
1415
  }
1235
1416
 
1417
+ // Legacy HR worker strings (pre-i18n catalog)
1418
+ if (normalized === 'Send Notification') {
1419
+ return WORKFLOW_TEMPLATES.notification_sent_pending?.ar ?? null;
1420
+ }
1421
+ const notificationSuccessVariants = [
1422
+ 'notification sent successfully',
1423
+ 'notification sent sucessfully',
1424
+ 'notification sent',
1425
+ ];
1426
+ if (notificationSuccessVariants.includes(normalized.toLowerCase())) {
1427
+ return WORKFLOW_TEMPLATES.notification_sent_successfully?.ar ?? null;
1428
+ }
1429
+ const legacyPendingPrefix = 'Approval pending from ';
1430
+ if (normalized.startsWith(legacyPendingPrefix)) {
1431
+ const namePartEn = normalized.slice(legacyPendingPrefix.length).trim();
1432
+ const nameAr = resolveArabicWorkflowActorName(actors, namePartEn);
1433
+ return interpolate(WORKFLOW_TEMPLATES.request_approval_pending_from.ar, { name: nameAr });
1434
+ }
1435
+ if (
1436
+ normalized === 'Approval Pending from HR Section' ||
1437
+ normalized === 'Approval Pending from Records Department'
1438
+ ) {
1439
+ return WORKFLOW_TEMPLATES.request_approval_pending?.ar ?? null;
1440
+ }
1441
+
1236
1442
  return null;
1237
1443
  }
1238
1444
 
@@ -1270,9 +1476,54 @@ export function enrichWorkflowLog(log: Record<string, unknown>): Record<string,
1270
1476
  const status = log.status != null ? String(log.status) : null;
1271
1477
  const content = log.content != null ? String(log.content) : null;
1272
1478
  const actors = extractBilingualActorsFromRecord(log);
1479
+ const resolvedAr = resolveWorkflowContentAr(content, actors);
1480
+ const existingAr =
1481
+ log.content_ar != null && String(log.content_ar).trim() !== ''
1482
+ ? String(log.content_ar)
1483
+ : null;
1484
+ // Re-resolve when stored AR still embeds the English actor parsed from `content`.
1485
+ const pendingPrefix = WORKFLOW_TEMPLATES.request_approval_pending_from?.en.split('{{name}}')[0].trim();
1486
+ let content_ar = existingAr ?? resolvedAr;
1487
+ if (
1488
+ content &&
1489
+ pendingPrefix &&
1490
+ content.startsWith(pendingPrefix) &&
1491
+ existingAr &&
1492
+ resolvedAr &&
1493
+ existingAr !== resolvedAr
1494
+ ) {
1495
+ const namePartEn = content.slice(pendingPrefix.length).trim();
1496
+ if (namePartEn && existingAr.includes(namePartEn)) {
1497
+ content_ar = resolvedAr;
1498
+ }
1499
+ }
1500
+ if (
1501
+ content &&
1502
+ existingAr &&
1503
+ resolvedAr &&
1504
+ existingAr !== resolvedAr &&
1505
+ /Request assigned to/i.test(existingAr)
1506
+ ) {
1507
+ content_ar = resolvedAr;
1508
+ }
1509
+ if (
1510
+ content &&
1511
+ existingAr &&
1512
+ resolvedAr &&
1513
+ existingAr !== resolvedAr &&
1514
+ actors?.roleName?.trim() &&
1515
+ actors?.roleArabicName?.trim() &&
1516
+ existingAr.includes(actors.roleName.trim())
1517
+ ) {
1518
+ content_ar = resolvedAr;
1519
+ }
1520
+ if ((content_ar == null || content_ar === '') && content) {
1521
+ const notificationAr = resolveWorkflowContentAr(content, actors);
1522
+ if (notificationAr) content_ar = notificationAr;
1523
+ }
1273
1524
  return {
1274
1525
  ...log,
1275
- content_ar: log.content_ar ?? resolveWorkflowContentAr(content, actors),
1526
+ content_ar,
1276
1527
  status_ar: log.status_ar ?? resolveWorkflowStatusAr(status),
1277
1528
  };
1278
1529
  }
package/src/index.ts CHANGED
@@ -498,7 +498,9 @@ export * from './models/RespondToEnquiriesAttachmentModel';
498
498
  export {RespondToEnquiriesRequestAttachment} from './models/RespondToEnquiriesAttachmentModel';
499
499
  export * from './models/RequestATenderRequestModel';
500
500
  export * from './models/RequestTenderAnalysisRequestModel';
501
- export * from './models/ContractServiceRequestModel';export * from './models/SlaConfigModel';
501
+ export * from './models/ContractServiceRequestModel';
502
+ export * from './models/ContractServiceRequestDocumentModel';
503
+ export * from './models/SlaConfigModel';
502
504
  export * from './models/SlaRequestModel';
503
505
  export * from './models/SlaMyRequestsViewModel';
504
506
  export * from './models/SlaApprovalsViewModel';
@@ -0,0 +1,35 @@
1
+ import { Column, Entity } from 'typeorm';
2
+ import { BaseModel } from './BaseModel';
3
+
4
+ @Entity({ name: 'contract_service_request_documents' })
5
+ export class ContractServiceRequestDocument extends BaseModel {
6
+ @Column({ type: 'integer', nullable: false })
7
+ request_id: number;
8
+
9
+ @Column({ type: 'integer', nullable: true })
10
+ service_id: number | null;
11
+
12
+ @Column({ type: 'integer', nullable: true })
13
+ sub_service_id: number | null;
14
+
15
+ @Column({ type: 'varchar', length: 500, nullable: false })
16
+ document_name: string;
17
+
18
+ @Column({ type: 'text', nullable: true })
19
+ description: string | null;
20
+
21
+ @Column({ type: 'varchar', length: 500, nullable: false })
22
+ file_url: string;
23
+
24
+ @Column({ type: 'varchar', length: 255, nullable: true })
25
+ file_name: string | null;
26
+
27
+ @Column({ type: 'varchar', length: 100, nullable: true })
28
+ file_type: string | null;
29
+
30
+ @Column({ type: 'bigint', nullable: true })
31
+ file_size: number | null;
32
+
33
+ @Column({ type: 'integer', nullable: false })
34
+ uploaded_by: number;
35
+ }
@@ -43,6 +43,12 @@ export class ContractServiceRequest extends BaseModel {
43
43
  @Column({ type: "varchar", length: 255, nullable: false })
44
44
  company_name: string;
45
45
 
46
+ @Column({ type: "varchar", length: 30, nullable: true })
47
+ company_phone_number: string | null;
48
+
49
+ @Column({ type: "text", nullable: true })
50
+ notes: string | null;
51
+
46
52
  @Column({
47
53
  type: "enum",
48
54
  enum: RespondToEnquiriesRequestStatus,
@@ -1,6 +1,5 @@
1
1
  import { Column, Entity } from 'typeorm';
2
2
  import { BaseModel } from './BaseModel';
3
- import { FollowUpReportSubjectClassification } from './FollowUpReportRequestModel';
4
3
 
5
4
  @Entity({ name: 'followup_report_items' })
6
5
  export class FollowUpReportItem extends BaseModel {
@@ -22,13 +21,8 @@ export class FollowUpReportItem extends BaseModel {
22
21
  @Column({ type: 'varchar', length: 500, nullable: true })
23
22
  subject: string | null;
24
23
 
25
- @Column({
26
- type: 'enum',
27
- enum: FollowUpReportSubjectClassification,
28
- enumName: 'maintenance_subject_classification_en',
29
- nullable: true,
30
- })
31
- subject_classification: FollowUpReportSubjectClassification | null;
24
+ @Column({ type: 'varchar', length: 100, nullable: true })
25
+ subject_classification: string | null;
32
26
 
33
27
  @Column({ type: 'varchar', length: 500, nullable: true })
34
28
  general_manager_comment: string | null;
@@ -44,4 +38,16 @@ export class FollowUpReportItem extends BaseModel {
44
38
 
45
39
  @Column({ type: 'varchar', length: 100, nullable: true })
46
40
  delay_period: string | null;
41
+
42
+ @Column({ type: 'varchar', length: 500, nullable: true })
43
+ file_url: string | null;
44
+
45
+ @Column({ type: 'varchar', length: 255, nullable: true })
46
+ file_name: string | null;
47
+
48
+ @Column({ type: 'varchar', length: 100, nullable: true })
49
+ file_type: string | null;
50
+
51
+ @Column({ type: 'bigint', nullable: true })
52
+ file_size: number | null;
47
53
  }
@@ -8,8 +8,17 @@ export enum FollowUpReportSubjectClassification {
8
8
  CONFIDENTIAL = 'Confidential',
9
9
  INFORMATIONAL = 'Informational',
10
10
  NORMAL = 'Normal',
11
+ IMPORTANT = 'Important',
12
+ HIGHLY_CONFIDENTIAL = 'Highly Confidential',
13
+ RESTRICTED = 'Restricted',
14
+ LIMITED = 'Limited',
11
15
  }
12
16
 
17
+ /** Allowed API/DB values for follow-up report subject classification. */
18
+ export const FOLLOW_UP_REPORT_SUBJECT_CLASSIFICATION_VALUES = Object.values(
19
+ FollowUpReportSubjectClassification,
20
+ ) as FollowUpReportSubjectClassification[];
21
+
13
22
  /** Concerned department under General Manager of Support Services */
14
23
  export enum FollowUpReportConcernedDepartment {
15
24
  IT = 'IT',
@@ -30,6 +30,9 @@ export class RespondToEnquiriesRequestAttachment extends BaseModel {
30
30
  @Column({ type: "text", nullable: true })
31
31
  description: string;
32
32
 
33
+ @Column({ type: "varchar", length: 500, nullable: true })
34
+ document_name: string | null;
35
+
33
36
  constructor(
34
37
  request_id: number,
35
38
  uploaded_by: number,
@@ -39,7 +42,8 @@ export class RespondToEnquiriesRequestAttachment extends BaseModel {
39
42
  file_name?: string,
40
43
  file_type?: string,
41
44
  file_size?: number,
42
- description?: string
45
+ description?: string,
46
+ document_name?: string
43
47
  ) {
44
48
  super();
45
49
  this.request_id = request_id;
@@ -51,5 +55,6 @@ export class RespondToEnquiriesRequestAttachment extends BaseModel {
51
55
  this.file_type = file_type || "";
52
56
  this.file_size = file_size || null;
53
57
  this.description = description || "";
58
+ this.document_name = document_name || null;
54
59
  }
55
60
  }