@platform-modules/civil-aviation-authority 2.3.295 → 2.3.297

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.
@@ -131,6 +131,58 @@
131
131
  "it_helpdesk_wf_routing_resolved": {
132
132
  "en": "Workflow routing completed - request resolved at L3",
133
133
  "ar": "تم استكمال مسار سير العمل - تم حل الطلب في المستوى الثالث (L3)"
134
+ },
135
+ "it_helpdesk_wf_routing_l3_reassign": {
136
+ "en": "Workflow routed to L3 assignment path - request reassigned by {{userName}}{{comment}}",
137
+ "ar": "تم توجيه سير العمل إلى مسار تعيين المستوى الثالث (L3) - تمت إعادة تعيين الطلب بواسطة {{userName}}{{comment}}"
138
+ },
139
+ "it_helpdesk_wf_routing_direct_approval": {
140
+ "en": "Workflow routed to direct resolution path - request approved by {{roleName}}{{comment}}",
141
+ "ar": "تم توجيه سير العمل إلى مسار الحل المباشر - تمت الموافقة على الطلب من قبل {{roleName}}{{comment}}"
142
+ },
143
+ "it_helpdesk_wf_routing_decision": {
144
+ "en": "Workflow decision made - {{status}} by {{roleName}}{{comment}}",
145
+ "ar": "تم اتخاذ قرار سير العمل - {{statusAr}} الطلب من قبل {{roleName}}{{comment}}"
146
+ },
147
+ "it_helpdesk_wf_l3_assignment_pending": {
148
+ "en": "L3 Technical Support assignment pending - request reassigned by {{roleName}}{{comment}}",
149
+ "ar": "تعيين دعم المستوى الثالث (L3) قيد الانتظار - تمت إعادة تعيين الطلب من قبل {{roleName}}{{comment}}"
150
+ },
151
+ "it_helpdesk_wf_l3_role_approved": {
152
+ "en": "{{roleName}} approved the request{{comment}}",
153
+ "ar": "تمت الموافقة على الطلب من قبل {{roleName}}{{commentAr}}"
154
+ },
155
+ "it_helpdesk_wf_l3_role_rejected": {
156
+ "en": "{{roleName}} rejected the request{{comment}}",
157
+ "ar": "تم رفض الطلب من قبل {{roleName}}{{commentAr}}"
158
+ },
159
+ "it_helpdesk_wf_role_outcome_by_user_rejected": {
160
+ "en": "{{roleName}} rejected the request by {{userName}}{{comment}}",
161
+ "ar": "تم رفض الطلب من قبل {{roleName}} بواسطة {{userName}}{{comment}}"
162
+ },
163
+ "it_helpdesk_wf_role_outcome_by_user_closed": {
164
+ "en": "{{roleName}} closed the request by {{userName}}{{comment}}",
165
+ "ar": "تم إغلاق الطلب من قبل {{roleName}} بواسطة {{userName}}{{comment}}"
166
+ },
167
+ "it_helpdesk_wf_routing_technician_rejected": {
168
+ "en": "Workflow routing completed - request rejected at technician level{{comment}}",
169
+ "ar": "تم استكمال مسار سير العمل - تم رفض الطلب على مستوى الفني{{comment}}"
170
+ },
171
+ "it_helpdesk_wf_hos_not_required_rejected": {
172
+ "en": "HOS notification not required, request rejected by {{userName}}{{comment}}",
173
+ "ar": "إشعار رئيس القسم غير مطلوب، تم رفض الطلب بواسطة {{userName}}{{comment}}"
174
+ },
175
+ "it_helpdesk_wf_hos_not_required_closed": {
176
+ "en": "HOS notification not required, request closed by {{userName}}{{comment}}",
177
+ "ar": "إشعار رئيس القسم غير مطلوب، تم إغلاق الطلب بواسطة {{userName}}{{comment}}"
178
+ },
179
+ "it_helpdesk_wf_l3_not_required_rejected": {
180
+ "en": "L3 assignment not required, request rejected by {{userName}}{{comment}}",
181
+ "ar": "تعيين المستوى الثالث (L3) غير مطلوب، تم رفض الطلب بواسطة {{userName}}{{comment}}"
182
+ },
183
+ "it_helpdesk_wf_l3_not_required_closed": {
184
+ "en": "L3 assignment not required, request closed by {{userName}}{{comment}}",
185
+ "ar": "تعيين المستوى الثالث (L3) غير مطلوب، تم إغلاق الطلب بواسطة {{userName}}{{comment}}"
134
186
  }
135
187
  },
136
188
  "chatTemplates": {
@@ -262,6 +314,10 @@
262
314
  "Pending": "قيد الانتظار",
263
315
  "Not Yet Started": "لم يبدأ بعد"
264
316
  },
317
+ "roleLabels": {
318
+ "L3 Technical Support": "دعم المستوى الثالث (L3) التقني",
319
+ "IT Technician": "فني تكنولوجيا المعلومات"
320
+ },
265
321
  "chatStatusLabels": {
266
322
  "Submitted": "تم التقديم",
267
323
  "Received": "تم الاستلام",
@@ -77,7 +77,6 @@ export type RequestApprovedAssignedWorkflowParams = {
77
77
  employeeId: string | number;
78
78
  assigneeLabel?: 'user' | 'technician';
79
79
  };
80
- /** Parse legacy English-only assignment comments used as workflow log suffixes. */
81
80
  export declare function tryParseRequestAssignedComment(comment?: string | null): Omit<RequestApprovedAssignedWorkflowParams, 'roleArabicName'> | null;
82
81
  /** Bilingual workflow content for approved assignment steps (HR, IT, Tender, etc.). */
83
82
  export declare function buildRequestApprovedAssignedWorkflowFields(params: RequestApprovedAssignedWorkflowParams): BilingualText;
@@ -265,6 +264,47 @@ export type ItHelpdeskMuscatWorkflowParams = {
265
264
  comment?: string | null;
266
265
  } | {
267
266
  variant: 'routing_resolved';
267
+ } | {
268
+ variant: 'routing_l3_reassign';
269
+ userName: string;
270
+ userArabicName?: string | null;
271
+ comment?: string | null;
272
+ } | {
273
+ variant: 'routing_direct_approval';
274
+ roleName: string;
275
+ roleArabicName?: string | null;
276
+ comment?: string | null;
277
+ } | {
278
+ variant: 'routing_decision';
279
+ status: string;
280
+ roleName: string;
281
+ roleArabicName?: string | null;
282
+ comment?: string | null;
283
+ } | {
284
+ variant: 'l3_assignment_pending';
285
+ roleName: string;
286
+ roleArabicName?: string | null;
287
+ comment?: string | null;
288
+ } | {
289
+ variant: 'l3_role_approved' | 'l3_role_rejected';
290
+ roleName: string;
291
+ roleArabicName?: string | null;
292
+ comment?: string | null;
293
+ } | {
294
+ variant: 'role_outcome_by_user_rejected' | 'role_outcome_by_user_closed';
295
+ roleName: string;
296
+ roleArabicName?: string | null;
297
+ userName: string;
298
+ userArabicName?: string | null;
299
+ comment?: string | null;
300
+ } | {
301
+ variant: 'routing_technician_rejected';
302
+ comment?: string | null;
303
+ } | {
304
+ variant: 'hos_not_required_rejected' | 'hos_not_required_closed' | 'l3_not_required_rejected' | 'l3_not_required_closed';
305
+ userName: string;
306
+ userArabicName?: string | null;
307
+ comment?: string | null;
268
308
  };
269
309
  export declare function buildItHelpdeskMuscatWorkflowLog(params: ItHelpdeskMuscatWorkflowParams): BilingualText;
270
310
  export type LogisticsActualReturnParams = {
@@ -67,6 +67,7 @@ const CHAT_TEMPLATES = workflow_chat_i18n_json_1.default.chatTemplates;
67
67
  const NOTIFICATION_TEMPLATES = workflow_chat_i18n_json_1.default.notificationTemplates ?? {};
68
68
  const WORKFLOW_STATUS_LABELS = workflow_chat_i18n_json_1.default.workflowStatusLabels;
69
69
  const CHAT_STATUS_LABELS = workflow_chat_i18n_json_1.default.chatStatusLabels;
70
+ const ROLE_LABELS = workflow_chat_i18n_json_1.default.roleLabels ?? {};
70
71
  function getWorkflowChatI18nCatalog() {
71
72
  return workflow_chat_i18n_json_1.default;
72
73
  }
@@ -178,10 +179,19 @@ function buildSynchronizedWorkflowMessage(ctx) {
178
179
  return actorEn
179
180
  ? renderSynchronizedWorkflowMessage('request_approval_pending_from', { name: actorEn }, { name: actorAr })
180
181
  : workflowTemplate('request_approval_pending');
181
- case 'approved':
182
+ case 'approved': {
183
+ const assignment = tryParseRequestAssignedComment(ctx.comment);
184
+ if (assignment) {
185
+ return buildRequestApprovedAssignedWorkflowFields({
186
+ ...assignment,
187
+ roleName: ctx.roleName?.trim() || assignment.roleName,
188
+ roleArabicName: ctx.roleArabicName,
189
+ });
190
+ }
182
191
  return actorEn
183
192
  ? renderSynchronizedWorkflowMessage('request_approved_by', { name: actorEn, comment: commentWf }, { name: actorAr, comment: commentWf })
184
193
  : renderSynchronizedWorkflowMessage('request_approved', { comment: commentWf }, { comment: commentWf });
194
+ }
185
195
  case 'rejected':
186
196
  return actorEn
187
197
  ? renderSynchronizedWorkflowMessage('request_rejected_by', { name: actorEn, comment: commentWf }, { name: actorAr, comment: commentWf })
@@ -236,11 +246,11 @@ function buildSynchronizedChatMessage(ctx) {
236
246
  }
237
247
  }
238
248
  /** Parse legacy English-only assignment comments used as workflow log suffixes. */
239
- function tryParseRequestAssignedComment(comment) {
240
- const text = comment?.trim();
241
- if (!text)
249
+ function parseRequestAssignedCommentText(text) {
250
+ const normalized = text.trim();
251
+ if (!normalized)
242
252
  return null;
243
- const technicianMatch = text.match(/^Request assigned to technician (.+?) by (.+?) \((.+?) - (.+?)\)$/);
253
+ const technicianMatch = normalized.match(/^request assigned to technician (.+?) by (.+?) \((.+?) - (.+?)\)$/i);
244
254
  if (technicianMatch) {
245
255
  return {
246
256
  assignedUserName: technicianMatch[1].trim(),
@@ -250,7 +260,7 @@ function tryParseRequestAssignedComment(comment) {
250
260
  assigneeLabel: 'technician',
251
261
  };
252
262
  }
253
- const userMatch = text.match(/^Request assigned to (.+?) by (.+?) \((.+?) - (.+?)\)$/);
263
+ const userMatch = normalized.match(/^request assigned to (.+?) by (.+?) \((.+?) - (.+?)\)$/i);
254
264
  if (userMatch) {
255
265
  return {
256
266
  assignedUserName: userMatch[1].trim(),
@@ -262,6 +272,36 @@ function tryParseRequestAssignedComment(comment) {
262
272
  }
263
273
  return null;
264
274
  }
275
+ function tryParseRequestAssignedComment(comment) {
276
+ let text = comment?.trim();
277
+ if (!text)
278
+ return null;
279
+ if (text.startsWith(':')) {
280
+ text = text.replace(/^:\s*/, '').trim();
281
+ }
282
+ const direct = parseRequestAssignedCommentText(text);
283
+ if (direct)
284
+ return direct;
285
+ const colonIdx = text.indexOf(': ');
286
+ if (colonIdx >= 0) {
287
+ const afterColon = text.slice(colonIdx + 2).trim();
288
+ const parsedAfterColon = parseRequestAssignedCommentText(afterColon);
289
+ if (parsedAfterColon)
290
+ return parsedAfterColon;
291
+ }
292
+ const assignMatch = text.match(/request assigned to/i);
293
+ if (assignMatch && assignMatch.index != null && assignMatch.index >= 0) {
294
+ return parseRequestAssignedCommentText(text.slice(assignMatch.index).trim());
295
+ }
296
+ return null;
297
+ }
298
+ function buildAssignmentWorkflowFieldsFromActors(assignment, actors) {
299
+ return buildRequestApprovedAssignedWorkflowFields({
300
+ ...assignment,
301
+ roleName: actors?.roleName?.trim() || assignment.roleName,
302
+ roleArabicName: actors?.roleArabicName ?? null,
303
+ });
304
+ }
265
305
  /** Bilingual workflow content for approved assignment steps (HR, IT, Tender, etc.). */
266
306
  function buildRequestApprovedAssignedWorkflowFields(params) {
267
307
  const assignedUserEn = params.assignedUserName?.trim() || 'User';
@@ -286,7 +326,8 @@ function buildRequestApprovedAssignedWorkflowFields(params) {
286
326
  }
287
327
  /** Standard workflow log UPDATE fields for all CAA module repositories. */
288
328
  function buildWorkflowLogUpdateFields(params) {
289
- if (params.approvalStatus === 'Approved') {
329
+ const isApprovedLike = params.approvalStatus === 'Approved' || params.approvalStatus === 'Assigned';
330
+ if (isApprovedLike) {
290
331
  const assignment = tryParseRequestAssignedComment(params.comment);
291
332
  if (assignment) {
292
333
  const bilingual = buildRequestApprovedAssignedWorkflowFields({
@@ -303,7 +344,7 @@ function buildWorkflowLogUpdateFields(params) {
303
344
  }
304
345
  }
305
346
  let action;
306
- if (params.approvalStatus === 'Approved')
347
+ if (params.approvalStatus === 'Approved' || params.approvalStatus === 'Assigned')
307
348
  action = 'approved';
308
349
  else if (params.approvalStatus === 'Rejected')
309
350
  action = 'rejected';
@@ -417,6 +458,23 @@ function formatColonCommentSuffix(comment) {
417
458
  const text = comment?.trim();
418
459
  return text ? `: ${text}` : '';
419
460
  }
461
+ function formatBilingualColonCommentSuffix(comment) {
462
+ const text = comment?.trim();
463
+ if (!text)
464
+ return { en: '', ar: '' };
465
+ const statusKey = text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
466
+ const arText = resolveChatStatusAr(statusKey) ?? text;
467
+ return { en: `: ${text}`, ar: `: ${arText}` };
468
+ }
469
+ function splitWorkflowColonComment(content) {
470
+ const idx = content.indexOf(': ');
471
+ if (idx <= 0)
472
+ return { base: content.trim() };
473
+ return {
474
+ base: content.slice(0, idx).trim(),
475
+ comment: content.slice(idx + 2).trim() || undefined,
476
+ };
477
+ }
420
478
  function formatReassignCommentSuffix(comment) {
421
479
  const text = comment?.trim();
422
480
  return text ? `:${text}` : '';
@@ -684,6 +742,65 @@ function buildItHelpdeskMuscatWorkflowLog(params) {
684
742
  return renderSynchronizedWorkflowMessage('it_helpdesk_wf_routing_rejected', { comment }, { comment });
685
743
  case 'routing_resolved':
686
744
  return workflowTemplate('it_helpdesk_wf_routing_resolved');
745
+ case 'routing_l3_reassign': {
746
+ const userEn = params.userName.trim();
747
+ const userAr = resolveBilingualName(userEn, params.userArabicName);
748
+ return renderSynchronizedWorkflowMessage('it_helpdesk_wf_routing_l3_reassign', { userName: userEn, comment }, { userName: userAr, comment });
749
+ }
750
+ case 'routing_direct_approval': {
751
+ const roleEn = params.roleName.trim();
752
+ const roleAr = resolveBilingualName(roleEn, params.roleArabicName);
753
+ return renderSynchronizedWorkflowMessage('it_helpdesk_wf_routing_direct_approval', { roleName: roleEn, comment }, { roleName: roleAr, comment });
754
+ }
755
+ case 'routing_decision': {
756
+ const roleEn = params.roleName.trim();
757
+ const roleAr = resolveBilingualName(roleEn, params.roleArabicName);
758
+ const statusEn = params.status.toLowerCase();
759
+ const statusAr = resolveItHelpdeskGenericStatusAr(params.status);
760
+ return renderSynchronizedWorkflowMessage('it_helpdesk_wf_routing_decision', { status: statusEn, statusAr, roleName: roleEn, comment }, { status: statusEn, statusAr, roleName: roleAr, comment });
761
+ }
762
+ case 'l3_assignment_pending': {
763
+ const roleEn = params.roleName.trim();
764
+ const roleAr = resolveBilingualName(roleEn, params.roleArabicName);
765
+ return renderSynchronizedWorkflowMessage('it_helpdesk_wf_l3_assignment_pending', { roleName: roleEn, comment }, { roleName: roleAr, comment });
766
+ }
767
+ case 'l3_role_approved':
768
+ case 'l3_role_rejected': {
769
+ const roleEn = params.roleName.trim();
770
+ const roleAr = resolveBilingualName(roleEn, params.roleArabicName ?? ROLE_LABELS[roleEn] ?? null);
771
+ const biComment = formatBilingualColonCommentSuffix(params.comment);
772
+ const key = params.variant === 'l3_role_approved'
773
+ ? 'it_helpdesk_wf_l3_role_approved'
774
+ : 'it_helpdesk_wf_l3_role_rejected';
775
+ return renderSynchronizedWorkflowMessage(key, { roleName: roleEn, comment: biComment.en }, { roleName: roleAr, commentAr: biComment.ar });
776
+ }
777
+ case 'role_outcome_by_user_rejected':
778
+ case 'role_outcome_by_user_closed': {
779
+ const roleEn = params.roleName.trim();
780
+ const roleAr = resolveBilingualName(roleEn, params.roleArabicName);
781
+ const userEn = params.userName.trim();
782
+ const userAr = resolveBilingualName(userEn, params.userArabicName);
783
+ const key = params.variant === 'role_outcome_by_user_rejected'
784
+ ? 'it_helpdesk_wf_role_outcome_by_user_rejected'
785
+ : 'it_helpdesk_wf_role_outcome_by_user_closed';
786
+ return renderSynchronizedWorkflowMessage(key, { roleName: roleEn, userName: userEn, comment }, { roleName: roleAr, userName: userAr, comment });
787
+ }
788
+ case 'routing_technician_rejected':
789
+ return renderSynchronizedWorkflowMessage('it_helpdesk_wf_routing_technician_rejected', { comment }, { comment });
790
+ case 'hos_not_required_rejected':
791
+ case 'hos_not_required_closed':
792
+ case 'l3_not_required_rejected':
793
+ case 'l3_not_required_closed': {
794
+ const userEn = params.userName.trim();
795
+ const userAr = resolveBilingualName(userEn, params.userArabicName);
796
+ const keyMap = {
797
+ hos_not_required_rejected: 'it_helpdesk_wf_hos_not_required_rejected',
798
+ hos_not_required_closed: 'it_helpdesk_wf_hos_not_required_closed',
799
+ l3_not_required_rejected: 'it_helpdesk_wf_l3_not_required_rejected',
800
+ l3_not_required_closed: 'it_helpdesk_wf_l3_not_required_closed',
801
+ };
802
+ return renderSynchronizedWorkflowMessage(keyMap[params.variant], { userName: userEn, comment }, { userName: userAr, comment });
803
+ }
687
804
  default:
688
805
  return workflowTemplate('request_submitted');
689
806
  }
@@ -775,19 +892,23 @@ function resolveArabicWorkflowActorName(actors, parsedEnglishName) {
775
892
  /** Arabic role/user label for chat templates. */
776
893
  function resolveArabicChatRoleName(actors, parsedEnglishRole) {
777
894
  const parsed = parsedEnglishRole?.trim();
778
- if (actors?.roleName?.trim()) {
779
- const actorRole = actors.roleName.trim();
780
- if (!parsed || parsed.toLowerCase() === actorRole.toLowerCase()) {
781
- return resolveBilingualName(actorRole, actors.roleArabicName);
895
+ if (parsed) {
896
+ if (actors?.roleName?.trim() &&
897
+ parsed.toLowerCase() === actors.roleName.trim().toLowerCase()) {
898
+ return resolveBilingualName(parsed, actors.roleArabicName);
782
899
  }
900
+ const catalogAr = ROLE_LABELS[parsed];
901
+ if (catalogAr)
902
+ return catalogAr;
903
+ return parsed;
783
904
  }
784
- if (actors?.roleArabicName?.trim() && parsed) {
785
- return resolveBilingualName(parsed, actors.roleArabicName);
905
+ if (actors?.roleName?.trim()) {
906
+ return resolveBilingualName(actors.roleName, actors.roleArabicName);
786
907
  }
787
908
  if (actors?.userName?.trim()) {
788
909
  return resolveBilingualName(actors.userName, actors.userArabicName);
789
910
  }
790
- return resolveBilingualName(parsed, actors?.roleArabicName);
911
+ return '';
791
912
  }
792
913
  function escapeRegexLiteral(value) {
793
914
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -868,6 +989,14 @@ function buildArabicWorkflowTemplateParams(params, actors) {
868
989
  const normalizedStatus = params.status.charAt(0).toUpperCase() + params.status.slice(1).toLowerCase();
869
990
  arParams.statusAr = resolveItHelpdeskGenericStatusAr(normalizedStatus);
870
991
  }
992
+ if (params.userName !== undefined) {
993
+ arParams.userName = actors?.userName?.trim()
994
+ ? resolveBilingualName(actors.userName, actors.userArabicName)
995
+ : resolveBilingualName(params.userName.trim(), actors?.userArabicName);
996
+ }
997
+ if (params.commentAr !== undefined) {
998
+ arParams.commentAr = params.commentAr;
999
+ }
871
1000
  return arParams;
872
1001
  }
873
1002
  function countTemplatePlaceholders(templateEn) {
@@ -876,10 +1005,139 @@ function countTemplatePlaceholders(templateEn) {
876
1005
  function listWorkflowTemplatesBySpecificity() {
877
1006
  return Object.entries(WORKFLOW_TEMPLATES).sort(([, a], [, b]) => countTemplatePlaceholders(b.en) - countTemplatePlaceholders(a.en));
878
1007
  }
1008
+ /** Parse legacy IT Helpdesk Muscat EN workflow strings before generic template matching. */
1009
+ function tryResolveItHelpdeskMuscatLegacyWorkflowAr(content, actors) {
1010
+ const { base, comment } = splitWorkflowColonComment(content);
1011
+ let m = base.match(/^Workflow routed to L3 assignment path - request reassigned by (.+)$/i);
1012
+ if (m) {
1013
+ return buildItHelpdeskMuscatWorkflowLog({
1014
+ variant: 'routing_l3_reassign',
1015
+ userName: m[1].trim(),
1016
+ userArabicName: actors?.userArabicName,
1017
+ comment,
1018
+ }).ar;
1019
+ }
1020
+ m = base.match(/^Workflow routed to direct resolution path - request approved by (.+)$/i);
1021
+ if (m) {
1022
+ return buildItHelpdeskMuscatWorkflowLog({
1023
+ variant: 'routing_direct_approval',
1024
+ roleName: m[1].trim(),
1025
+ roleArabicName: actors?.roleArabicName,
1026
+ comment,
1027
+ }).ar;
1028
+ }
1029
+ m = base.match(/^Workflow decision made - (\w+) by (.+)$/i);
1030
+ if (m) {
1031
+ return buildItHelpdeskMuscatWorkflowLog({
1032
+ variant: 'routing_decision',
1033
+ status: m[1],
1034
+ roleName: m[2].trim(),
1035
+ roleArabicName: actors?.roleArabicName,
1036
+ comment,
1037
+ }).ar;
1038
+ }
1039
+ m = base.match(/^L3 Technical Support assignment pending - request reassigned by (.+)$/i);
1040
+ if (m) {
1041
+ return buildItHelpdeskMuscatWorkflowLog({
1042
+ variant: 'l3_assignment_pending',
1043
+ roleName: m[1].trim(),
1044
+ roleArabicName: actors?.roleArabicName,
1045
+ comment,
1046
+ }).ar;
1047
+ }
1048
+ m = base.match(/^L3 Technical Support approved the request$/i);
1049
+ if (m) {
1050
+ return buildItHelpdeskMuscatWorkflowLog({
1051
+ variant: 'l3_role_approved',
1052
+ roleName: 'L3 Technical Support',
1053
+ comment,
1054
+ }).ar;
1055
+ }
1056
+ m = base.match(/^L3 Technical Support rejected the request$/i);
1057
+ if (m) {
1058
+ return buildItHelpdeskMuscatWorkflowLog({
1059
+ variant: 'l3_role_rejected',
1060
+ roleName: 'L3 Technical Support',
1061
+ comment,
1062
+ }).ar;
1063
+ }
1064
+ m = base.match(/^(.+?) rejected the request by (.+)$/i);
1065
+ if (m && !m[1].toLowerCase().includes('workflow')) {
1066
+ return buildItHelpdeskMuscatWorkflowLog({
1067
+ variant: 'role_outcome_by_user_rejected',
1068
+ roleName: m[1].trim(),
1069
+ roleArabicName: actors?.roleArabicName,
1070
+ userName: m[2].trim(),
1071
+ userArabicName: actors?.userArabicName,
1072
+ comment,
1073
+ }).ar;
1074
+ }
1075
+ m = base.match(/^(.+?) closed the request by (.+)$/i);
1076
+ if (m) {
1077
+ return buildItHelpdeskMuscatWorkflowLog({
1078
+ variant: 'role_outcome_by_user_closed',
1079
+ roleName: m[1].trim(),
1080
+ roleArabicName: actors?.roleArabicName,
1081
+ userName: m[2].trim(),
1082
+ userArabicName: actors?.userArabicName,
1083
+ comment,
1084
+ }).ar;
1085
+ }
1086
+ if (/^Workflow routing completed - request rejected at technician level$/i.test(base)) {
1087
+ return buildItHelpdeskMuscatWorkflowLog({
1088
+ variant: 'routing_technician_rejected',
1089
+ comment,
1090
+ }).ar;
1091
+ }
1092
+ m = base.match(/^HOS notification not required, request rejected by (.+)$/i);
1093
+ if (m) {
1094
+ return buildItHelpdeskMuscatWorkflowLog({
1095
+ variant: 'hos_not_required_rejected',
1096
+ userName: m[1].trim(),
1097
+ userArabicName: actors?.userArabicName,
1098
+ comment,
1099
+ }).ar;
1100
+ }
1101
+ m = base.match(/^HOS notification not required, request closed by (.+)$/i);
1102
+ if (m) {
1103
+ return buildItHelpdeskMuscatWorkflowLog({
1104
+ variant: 'hos_not_required_closed',
1105
+ userName: m[1].trim(),
1106
+ userArabicName: actors?.userArabicName,
1107
+ comment,
1108
+ }).ar;
1109
+ }
1110
+ m = base.match(/^L3 assignment not required, request rejected by (.+)$/i);
1111
+ if (m) {
1112
+ return buildItHelpdeskMuscatWorkflowLog({
1113
+ variant: 'l3_not_required_rejected',
1114
+ userName: m[1].trim(),
1115
+ userArabicName: actors?.userArabicName,
1116
+ comment,
1117
+ }).ar;
1118
+ }
1119
+ m = base.match(/^L3 assignment not required, request closed by (.+)$/i);
1120
+ if (m) {
1121
+ return buildItHelpdeskMuscatWorkflowLog({
1122
+ variant: 'l3_not_required_closed',
1123
+ userName: m[1].trim(),
1124
+ userArabicName: actors?.userArabicName,
1125
+ comment,
1126
+ }).ar;
1127
+ }
1128
+ return null;
1129
+ }
879
1130
  function resolveWorkflowContentAr(content, actors) {
880
1131
  if (content == null || content.trim() === '')
881
1132
  return null;
882
1133
  const normalized = content.trim();
1134
+ const embeddedAssignment = tryParseRequestAssignedComment(normalized);
1135
+ if (embeddedAssignment) {
1136
+ return buildAssignmentWorkflowFieldsFromActors(embeddedAssignment, actors).ar;
1137
+ }
1138
+ const itHelpdeskLegacyAr = tryResolveItHelpdeskMuscatLegacyWorkflowAr(normalized, actors);
1139
+ if (itHelpdeskLegacyAr)
1140
+ return itHelpdeskLegacyAr;
883
1141
  for (const tpl of Object.values(WORKFLOW_TEMPLATES)) {
884
1142
  if (tpl.en === normalized || tpl.en.toLowerCase() === normalized.toLowerCase())
885
1143
  return tpl.ar;
@@ -896,6 +1154,10 @@ function resolveWorkflowContentAr(content, actors) {
896
1154
  if (commentIdx > 0) {
897
1155
  const baseEn = normalized.slice(0, commentIdx).trim();
898
1156
  const commentText = normalized.slice(commentIdx + 2).trim();
1157
+ const assignment = tryParseRequestAssignedComment(commentText);
1158
+ if (assignment) {
1159
+ return buildAssignmentWorkflowFieldsFromActors(assignment, actors).ar;
1160
+ }
899
1161
  const commentSuffix = formatWorkflowCommentSuffix(commentText);
900
1162
  for (const tpl of Object.values(WORKFLOW_TEMPLATES)) {
901
1163
  if (!tpl.en.includes('{{comment}}'))
@@ -999,7 +1261,7 @@ function enrichWorkflowLog(log) {
999
1261
  existingAr &&
1000
1262
  resolvedAr &&
1001
1263
  existingAr !== resolvedAr &&
1002
- /Request assigned to/i.test(existingAr)) {
1264
+ (/request assigned to/i.test(content) || /request assigned to/i.test(existingAr))) {
1003
1265
  content_ar = resolvedAr;
1004
1266
  }
1005
1267
  if (content &&
@@ -1011,6 +1273,15 @@ function enrichWorkflowLog(log) {
1011
1273
  existingAr.includes(actors.roleName.trim())) {
1012
1274
  content_ar = resolvedAr;
1013
1275
  }
1276
+ const itHelpdeskMixedEnglish = /routed to L3|assignment path|Workflow decision made|L3 Technical Support approved|L3 assignment not required|HOS notification not required|routing completed/i;
1277
+ if (content &&
1278
+ existingAr &&
1279
+ resolvedAr &&
1280
+ existingAr !== resolvedAr &&
1281
+ (itHelpdeskMixedEnglish.test(content) ||
1282
+ /routed to|routing completed|assignment path|\sby\s/i.test(existingAr))) {
1283
+ content_ar = resolvedAr;
1284
+ }
1014
1285
  if ((content_ar == null || content_ar === '') && content) {
1015
1286
  const notificationAr = resolveWorkflowContentAr(content, actors);
1016
1287
  if (notificationAr)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platform-modules/civil-aviation-authority",
3
- "version": "2.3.295",
3
+ "version": "2.3.297",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {
@@ -131,6 +131,58 @@
131
131
  "it_helpdesk_wf_routing_resolved": {
132
132
  "en": "Workflow routing completed - request resolved at L3",
133
133
  "ar": "تم استكمال مسار سير العمل - تم حل الطلب في المستوى الثالث (L3)"
134
+ },
135
+ "it_helpdesk_wf_routing_l3_reassign": {
136
+ "en": "Workflow routed to L3 assignment path - request reassigned by {{userName}}{{comment}}",
137
+ "ar": "تم توجيه سير العمل إلى مسار تعيين المستوى الثالث (L3) - تمت إعادة تعيين الطلب بواسطة {{userName}}{{comment}}"
138
+ },
139
+ "it_helpdesk_wf_routing_direct_approval": {
140
+ "en": "Workflow routed to direct resolution path - request approved by {{roleName}}{{comment}}",
141
+ "ar": "تم توجيه سير العمل إلى مسار الحل المباشر - تمت الموافقة على الطلب من قبل {{roleName}}{{comment}}"
142
+ },
143
+ "it_helpdesk_wf_routing_decision": {
144
+ "en": "Workflow decision made - {{status}} by {{roleName}}{{comment}}",
145
+ "ar": "تم اتخاذ قرار سير العمل - {{statusAr}} الطلب من قبل {{roleName}}{{comment}}"
146
+ },
147
+ "it_helpdesk_wf_l3_assignment_pending": {
148
+ "en": "L3 Technical Support assignment pending - request reassigned by {{roleName}}{{comment}}",
149
+ "ar": "تعيين دعم المستوى الثالث (L3) قيد الانتظار - تمت إعادة تعيين الطلب من قبل {{roleName}}{{comment}}"
150
+ },
151
+ "it_helpdesk_wf_l3_role_approved": {
152
+ "en": "{{roleName}} approved the request{{comment}}",
153
+ "ar": "تمت الموافقة على الطلب من قبل {{roleName}}{{commentAr}}"
154
+ },
155
+ "it_helpdesk_wf_l3_role_rejected": {
156
+ "en": "{{roleName}} rejected the request{{comment}}",
157
+ "ar": "تم رفض الطلب من قبل {{roleName}}{{commentAr}}"
158
+ },
159
+ "it_helpdesk_wf_role_outcome_by_user_rejected": {
160
+ "en": "{{roleName}} rejected the request by {{userName}}{{comment}}",
161
+ "ar": "تم رفض الطلب من قبل {{roleName}} بواسطة {{userName}}{{comment}}"
162
+ },
163
+ "it_helpdesk_wf_role_outcome_by_user_closed": {
164
+ "en": "{{roleName}} closed the request by {{userName}}{{comment}}",
165
+ "ar": "تم إغلاق الطلب من قبل {{roleName}} بواسطة {{userName}}{{comment}}"
166
+ },
167
+ "it_helpdesk_wf_routing_technician_rejected": {
168
+ "en": "Workflow routing completed - request rejected at technician level{{comment}}",
169
+ "ar": "تم استكمال مسار سير العمل - تم رفض الطلب على مستوى الفني{{comment}}"
170
+ },
171
+ "it_helpdesk_wf_hos_not_required_rejected": {
172
+ "en": "HOS notification not required, request rejected by {{userName}}{{comment}}",
173
+ "ar": "إشعار رئيس القسم غير مطلوب، تم رفض الطلب بواسطة {{userName}}{{comment}}"
174
+ },
175
+ "it_helpdesk_wf_hos_not_required_closed": {
176
+ "en": "HOS notification not required, request closed by {{userName}}{{comment}}",
177
+ "ar": "إشعار رئيس القسم غير مطلوب، تم إغلاق الطلب بواسطة {{userName}}{{comment}}"
178
+ },
179
+ "it_helpdesk_wf_l3_not_required_rejected": {
180
+ "en": "L3 assignment not required, request rejected by {{userName}}{{comment}}",
181
+ "ar": "تعيين المستوى الثالث (L3) غير مطلوب، تم رفض الطلب بواسطة {{userName}}{{comment}}"
182
+ },
183
+ "it_helpdesk_wf_l3_not_required_closed": {
184
+ "en": "L3 assignment not required, request closed by {{userName}}{{comment}}",
185
+ "ar": "تعيين المستوى الثالث (L3) غير مطلوب، تم إغلاق الطلب بواسطة {{userName}}{{comment}}"
134
186
  }
135
187
  },
136
188
  "chatTemplates": {
@@ -262,6 +314,10 @@
262
314
  "Pending": "قيد الانتظار",
263
315
  "Not Yet Started": "لم يبدأ بعد"
264
316
  },
317
+ "roleLabels": {
318
+ "L3 Technical Support": "دعم المستوى الثالث (L3) التقني",
319
+ "IT Technician": "فني تكنولوجيا المعلومات"
320
+ },
265
321
  "chatStatusLabels": {
266
322
  "Submitted": "تم التقديم",
267
323
  "Received": "تم الاستلام",
@@ -65,6 +65,7 @@ const CHAT_TEMPLATES = catalog.chatTemplates as Record<string, BilingualText>;
65
65
  const NOTIFICATION_TEMPLATES = (catalog as { notificationTemplates?: Record<string, BilingualText> }).notificationTemplates ?? {};
66
66
  const WORKFLOW_STATUS_LABELS = catalog.workflowStatusLabels as Record<string, string>;
67
67
  const CHAT_STATUS_LABELS = catalog.chatStatusLabels as Record<string, string>;
68
+ const ROLE_LABELS = (catalog as { roleLabels?: Record<string, string> }).roleLabels ?? {};
68
69
 
69
70
  export function getWorkflowChatI18nCatalog(): WorkflowChatI18nCatalog {
70
71
  return catalog as WorkflowChatI18nCatalog;
@@ -237,7 +238,15 @@ export function buildSynchronizedWorkflowMessage(ctx: WorkflowMessageContext): B
237
238
  { name: actorAr },
238
239
  )
239
240
  : workflowTemplate('request_approval_pending');
240
- case 'approved':
241
+ case 'approved': {
242
+ const assignment = tryParseRequestAssignedComment(ctx.comment);
243
+ if (assignment) {
244
+ return buildRequestApprovedAssignedWorkflowFields({
245
+ ...assignment,
246
+ roleName: ctx.roleName?.trim() || assignment.roleName,
247
+ roleArabicName: ctx.roleArabicName,
248
+ });
249
+ }
241
250
  return actorEn
242
251
  ? renderSynchronizedWorkflowMessage(
243
252
  'request_approved_by',
@@ -249,6 +258,7 @@ export function buildSynchronizedWorkflowMessage(ctx: WorkflowMessageContext): B
249
258
  { comment: commentWf },
250
259
  { comment: commentWf },
251
260
  );
261
+ }
252
262
  case 'rejected':
253
263
  return actorEn
254
264
  ? renderSynchronizedWorkflowMessage(
@@ -354,14 +364,14 @@ export type RequestApprovedAssignedWorkflowParams = {
354
364
  };
355
365
 
356
366
  /** Parse legacy English-only assignment comments used as workflow log suffixes. */
357
- export function tryParseRequestAssignedComment(
358
- comment?: string | null,
367
+ function parseRequestAssignedCommentText(
368
+ text: string,
359
369
  ): Omit<RequestApprovedAssignedWorkflowParams, 'roleArabicName'> | null {
360
- const text = comment?.trim();
361
- if (!text) return null;
370
+ const normalized = text.trim();
371
+ if (!normalized) return null;
362
372
 
363
- const technicianMatch = text.match(
364
- /^Request assigned to technician (.+?) by (.+?) \((.+?) - (.+?)\)$/,
373
+ const technicianMatch = normalized.match(
374
+ /^request assigned to technician (.+?) by (.+?) \((.+?) - (.+?)\)$/i,
365
375
  );
366
376
  if (technicianMatch) {
367
377
  return {
@@ -373,7 +383,7 @@ export function tryParseRequestAssignedComment(
373
383
  };
374
384
  }
375
385
 
376
- const userMatch = text.match(/^Request assigned to (.+?) by (.+?) \((.+?) - (.+?)\)$/);
386
+ const userMatch = normalized.match(/^request assigned to (.+?) by (.+?) \((.+?) - (.+?)\)$/i);
377
387
  if (userMatch) {
378
388
  return {
379
389
  assignedUserName: userMatch[1].trim(),
@@ -387,6 +397,45 @@ export function tryParseRequestAssignedComment(
387
397
  return null;
388
398
  }
389
399
 
400
+ export function tryParseRequestAssignedComment(
401
+ comment?: string | null,
402
+ ): Omit<RequestApprovedAssignedWorkflowParams, 'roleArabicName'> | null {
403
+ let text = comment?.trim();
404
+ if (!text) return null;
405
+
406
+ if (text.startsWith(':')) {
407
+ text = text.replace(/^:\s*/, '').trim();
408
+ }
409
+
410
+ const direct = parseRequestAssignedCommentText(text);
411
+ if (direct) return direct;
412
+
413
+ const colonIdx = text.indexOf(': ');
414
+ if (colonIdx >= 0) {
415
+ const afterColon = text.slice(colonIdx + 2).trim();
416
+ const parsedAfterColon = parseRequestAssignedCommentText(afterColon);
417
+ if (parsedAfterColon) return parsedAfterColon;
418
+ }
419
+
420
+ const assignMatch = text.match(/request assigned to/i);
421
+ if (assignMatch && assignMatch.index != null && assignMatch.index >= 0) {
422
+ return parseRequestAssignedCommentText(text.slice(assignMatch.index).trim());
423
+ }
424
+
425
+ return null;
426
+ }
427
+
428
+ function buildAssignmentWorkflowFieldsFromActors(
429
+ assignment: Omit<RequestApprovedAssignedWorkflowParams, 'roleArabicName'>,
430
+ actors?: BilingualActorNames,
431
+ ): BilingualText {
432
+ return buildRequestApprovedAssignedWorkflowFields({
433
+ ...assignment,
434
+ roleName: actors?.roleName?.trim() || assignment.roleName,
435
+ roleArabicName: actors?.roleArabicName ?? null,
436
+ });
437
+ }
438
+
390
439
  /** Bilingual workflow content for approved assignment steps (HR, IT, Tender, etc.). */
391
440
  export function buildRequestApprovedAssignedWorkflowFields(
392
441
  params: RequestApprovedAssignedWorkflowParams,
@@ -427,7 +476,10 @@ export function buildWorkflowLogUpdateFields(params: {
427
476
  roleArabicName?: string | null;
428
477
  comment?: string | null;
429
478
  }): { content: string; content_ar: string; status: string; status_ar: string | null } {
430
- if (params.approvalStatus === 'Approved') {
479
+ const isApprovedLike =
480
+ params.approvalStatus === 'Approved' || params.approvalStatus === 'Assigned';
481
+
482
+ if (isApprovedLike) {
431
483
  const assignment = tryParseRequestAssignedComment(params.comment);
432
484
  if (assignment) {
433
485
  const bilingual = buildRequestApprovedAssignedWorkflowFields({
@@ -445,7 +497,7 @@ export function buildWorkflowLogUpdateFields(params: {
445
497
  }
446
498
 
447
499
  let action: WorkflowMessageAction;
448
- if (params.approvalStatus === 'Approved') action = 'approved';
500
+ if (params.approvalStatus === 'Approved' || params.approvalStatus === 'Assigned') action = 'approved';
449
501
  else if (params.approvalStatus === 'Rejected') action = 'rejected';
450
502
  else if (params.approvalStatus === 'Returned for Modification') action = 'returned_for_modification';
451
503
  else action = 'approval_pending_update';
@@ -591,6 +643,23 @@ function formatColonCommentSuffix(comment?: string | null): string {
591
643
  return text ? `: ${text}` : '';
592
644
  }
593
645
 
646
+ function formatBilingualColonCommentSuffix(comment?: string | null): { en: string; ar: string } {
647
+ const text = comment?.trim();
648
+ if (!text) return { en: '', ar: '' };
649
+ const statusKey = text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
650
+ const arText = resolveChatStatusAr(statusKey) ?? text;
651
+ return { en: `: ${text}`, ar: `: ${arText}` };
652
+ }
653
+
654
+ function splitWorkflowColonComment(content: string): { base: string; comment?: string } {
655
+ const idx = content.indexOf(': ');
656
+ if (idx <= 0) return { base: content.trim() };
657
+ return {
658
+ base: content.slice(0, idx).trim(),
659
+ comment: content.slice(idx + 2).trim() || undefined,
660
+ };
661
+ }
662
+
594
663
  function formatReassignCommentSuffix(comment?: string | null): string {
595
664
  const text = comment?.trim();
596
665
  return text ? `:${text}` : '';
@@ -935,7 +1004,53 @@ export type ItHelpdeskMuscatWorkflowParams =
935
1004
  roleArabicName?: string | null;
936
1005
  comment?: string | null;
937
1006
  }
938
- | { variant: 'routing_resolved' };
1007
+ | { variant: 'routing_resolved' }
1008
+ | {
1009
+ variant: 'routing_l3_reassign';
1010
+ userName: string;
1011
+ userArabicName?: string | null;
1012
+ comment?: string | null;
1013
+ }
1014
+ | {
1015
+ variant: 'routing_direct_approval';
1016
+ roleName: string;
1017
+ roleArabicName?: string | null;
1018
+ comment?: string | null;
1019
+ }
1020
+ | {
1021
+ variant: 'routing_decision';
1022
+ status: string;
1023
+ roleName: string;
1024
+ roleArabicName?: string | null;
1025
+ comment?: string | null;
1026
+ }
1027
+ | {
1028
+ variant: 'l3_assignment_pending';
1029
+ roleName: string;
1030
+ roleArabicName?: string | null;
1031
+ comment?: string | null;
1032
+ }
1033
+ | {
1034
+ variant: 'l3_role_approved' | 'l3_role_rejected';
1035
+ roleName: string;
1036
+ roleArabicName?: string | null;
1037
+ comment?: string | null;
1038
+ }
1039
+ | {
1040
+ variant: 'role_outcome_by_user_rejected' | 'role_outcome_by_user_closed';
1041
+ roleName: string;
1042
+ roleArabicName?: string | null;
1043
+ userName: string;
1044
+ userArabicName?: string | null;
1045
+ comment?: string | null;
1046
+ }
1047
+ | { variant: 'routing_technician_rejected'; comment?: string | null }
1048
+ | {
1049
+ variant: 'hos_not_required_rejected' | 'hos_not_required_closed' | 'l3_not_required_rejected' | 'l3_not_required_closed';
1050
+ userName: string;
1051
+ userArabicName?: string | null;
1052
+ comment?: string | null;
1053
+ };
939
1054
 
940
1055
  function resolveItHelpdeskGenericStatusAr(status: string): string {
941
1056
  switch (status.trim()) {
@@ -1100,6 +1215,102 @@ export function buildItHelpdeskMuscatWorkflowLog(params: ItHelpdeskMuscatWorkflo
1100
1215
  );
1101
1216
  case 'routing_resolved':
1102
1217
  return workflowTemplate('it_helpdesk_wf_routing_resolved');
1218
+ case 'routing_l3_reassign': {
1219
+ const userEn = params.userName.trim();
1220
+ const userAr = resolveBilingualName(userEn, params.userArabicName);
1221
+ return renderSynchronizedWorkflowMessage(
1222
+ 'it_helpdesk_wf_routing_l3_reassign',
1223
+ { userName: userEn, comment },
1224
+ { userName: userAr, comment },
1225
+ );
1226
+ }
1227
+ case 'routing_direct_approval': {
1228
+ const roleEn = params.roleName.trim();
1229
+ const roleAr = resolveBilingualName(roleEn, params.roleArabicName);
1230
+ return renderSynchronizedWorkflowMessage(
1231
+ 'it_helpdesk_wf_routing_direct_approval',
1232
+ { roleName: roleEn, comment },
1233
+ { roleName: roleAr, comment },
1234
+ );
1235
+ }
1236
+ case 'routing_decision': {
1237
+ const roleEn = params.roleName.trim();
1238
+ const roleAr = resolveBilingualName(roleEn, params.roleArabicName);
1239
+ const statusEn = params.status.toLowerCase();
1240
+ const statusAr = resolveItHelpdeskGenericStatusAr(params.status);
1241
+ return renderSynchronizedWorkflowMessage(
1242
+ 'it_helpdesk_wf_routing_decision',
1243
+ { status: statusEn, statusAr, roleName: roleEn, comment },
1244
+ { status: statusEn, statusAr, roleName: roleAr, comment },
1245
+ );
1246
+ }
1247
+ case 'l3_assignment_pending': {
1248
+ const roleEn = params.roleName.trim();
1249
+ const roleAr = resolveBilingualName(roleEn, params.roleArabicName);
1250
+ return renderSynchronizedWorkflowMessage(
1251
+ 'it_helpdesk_wf_l3_assignment_pending',
1252
+ { roleName: roleEn, comment },
1253
+ { roleName: roleAr, comment },
1254
+ );
1255
+ }
1256
+ case 'l3_role_approved':
1257
+ case 'l3_role_rejected': {
1258
+ const roleEn = params.roleName.trim();
1259
+ const roleAr = resolveBilingualName(
1260
+ roleEn,
1261
+ params.roleArabicName ?? ROLE_LABELS[roleEn] ?? null,
1262
+ );
1263
+ const biComment = formatBilingualColonCommentSuffix(params.comment);
1264
+ const key =
1265
+ params.variant === 'l3_role_approved'
1266
+ ? 'it_helpdesk_wf_l3_role_approved'
1267
+ : 'it_helpdesk_wf_l3_role_rejected';
1268
+ return renderSynchronizedWorkflowMessage(
1269
+ key,
1270
+ { roleName: roleEn, comment: biComment.en },
1271
+ { roleName: roleAr, commentAr: biComment.ar },
1272
+ );
1273
+ }
1274
+ case 'role_outcome_by_user_rejected':
1275
+ case 'role_outcome_by_user_closed': {
1276
+ const roleEn = params.roleName.trim();
1277
+ const roleAr = resolveBilingualName(roleEn, params.roleArabicName);
1278
+ const userEn = params.userName.trim();
1279
+ const userAr = resolveBilingualName(userEn, params.userArabicName);
1280
+ const key =
1281
+ params.variant === 'role_outcome_by_user_rejected'
1282
+ ? 'it_helpdesk_wf_role_outcome_by_user_rejected'
1283
+ : 'it_helpdesk_wf_role_outcome_by_user_closed';
1284
+ return renderSynchronizedWorkflowMessage(
1285
+ key,
1286
+ { roleName: roleEn, userName: userEn, comment },
1287
+ { roleName: roleAr, userName: userAr, comment },
1288
+ );
1289
+ }
1290
+ case 'routing_technician_rejected':
1291
+ return renderSynchronizedWorkflowMessage(
1292
+ 'it_helpdesk_wf_routing_technician_rejected',
1293
+ { comment },
1294
+ { comment },
1295
+ );
1296
+ case 'hos_not_required_rejected':
1297
+ case 'hos_not_required_closed':
1298
+ case 'l3_not_required_rejected':
1299
+ case 'l3_not_required_closed': {
1300
+ const userEn = params.userName.trim();
1301
+ const userAr = resolveBilingualName(userEn, params.userArabicName);
1302
+ const keyMap = {
1303
+ hos_not_required_rejected: 'it_helpdesk_wf_hos_not_required_rejected',
1304
+ hos_not_required_closed: 'it_helpdesk_wf_hos_not_required_closed',
1305
+ l3_not_required_rejected: 'it_helpdesk_wf_l3_not_required_rejected',
1306
+ l3_not_required_closed: 'it_helpdesk_wf_l3_not_required_closed',
1307
+ } as const;
1308
+ return renderSynchronizedWorkflowMessage(
1309
+ keyMap[params.variant],
1310
+ { userName: userEn, comment },
1311
+ { userName: userAr, comment },
1312
+ );
1313
+ }
1103
1314
  default:
1104
1315
  return workflowTemplate('request_submitted');
1105
1316
  }
@@ -1236,19 +1447,24 @@ function resolveArabicChatRoleName(
1236
1447
  parsedEnglishRole?: string,
1237
1448
  ): string {
1238
1449
  const parsed = parsedEnglishRole?.trim();
1239
- if (actors?.roleName?.trim()) {
1240
- const actorRole = actors.roleName.trim();
1241
- if (!parsed || parsed.toLowerCase() === actorRole.toLowerCase()) {
1242
- return resolveBilingualName(actorRole, actors.roleArabicName);
1450
+ if (parsed) {
1451
+ if (
1452
+ actors?.roleName?.trim() &&
1453
+ parsed.toLowerCase() === actors.roleName.trim().toLowerCase()
1454
+ ) {
1455
+ return resolveBilingualName(parsed, actors.roleArabicName);
1243
1456
  }
1457
+ const catalogAr = ROLE_LABELS[parsed];
1458
+ if (catalogAr) return catalogAr;
1459
+ return parsed;
1244
1460
  }
1245
- if (actors?.roleArabicName?.trim() && parsed) {
1246
- return resolveBilingualName(parsed, actors.roleArabicName);
1461
+ if (actors?.roleName?.trim()) {
1462
+ return resolveBilingualName(actors.roleName, actors.roleArabicName);
1247
1463
  }
1248
1464
  if (actors?.userName?.trim()) {
1249
1465
  return resolveBilingualName(actors.userName, actors.userArabicName);
1250
1466
  }
1251
- return resolveBilingualName(parsed, actors?.roleArabicName);
1467
+ return '';
1252
1468
  }
1253
1469
 
1254
1470
  function escapeRegexLiteral(value: string): string {
@@ -1348,6 +1564,14 @@ function buildArabicWorkflowTemplateParams(
1348
1564
  params.status.charAt(0).toUpperCase() + params.status.slice(1).toLowerCase();
1349
1565
  arParams.statusAr = resolveItHelpdeskGenericStatusAr(normalizedStatus);
1350
1566
  }
1567
+ if (params.userName !== undefined) {
1568
+ arParams.userName = actors?.userName?.trim()
1569
+ ? resolveBilingualName(actors.userName, actors.userArabicName)
1570
+ : resolveBilingualName(params.userName.trim(), actors?.userArabicName);
1571
+ }
1572
+ if (params.commentAr !== undefined) {
1573
+ arParams.commentAr = params.commentAr;
1574
+ }
1351
1575
  return arParams;
1352
1576
  }
1353
1577
 
@@ -1361,6 +1585,146 @@ function listWorkflowTemplatesBySpecificity(): Array<[string, BilingualText]> {
1361
1585
  );
1362
1586
  }
1363
1587
 
1588
+ /** Parse legacy IT Helpdesk Muscat EN workflow strings before generic template matching. */
1589
+ function tryResolveItHelpdeskMuscatLegacyWorkflowAr(
1590
+ content: string,
1591
+ actors?: BilingualActorNames,
1592
+ ): string | null {
1593
+ const { base, comment } = splitWorkflowColonComment(content);
1594
+
1595
+ let m = base.match(/^Workflow routed to L3 assignment path - request reassigned by (.+)$/i);
1596
+ if (m) {
1597
+ return buildItHelpdeskMuscatWorkflowLog({
1598
+ variant: 'routing_l3_reassign',
1599
+ userName: m[1].trim(),
1600
+ userArabicName: actors?.userArabicName,
1601
+ comment,
1602
+ }).ar;
1603
+ }
1604
+
1605
+ m = base.match(/^Workflow routed to direct resolution path - request approved by (.+)$/i);
1606
+ if (m) {
1607
+ return buildItHelpdeskMuscatWorkflowLog({
1608
+ variant: 'routing_direct_approval',
1609
+ roleName: m[1].trim(),
1610
+ roleArabicName: actors?.roleArabicName,
1611
+ comment,
1612
+ }).ar;
1613
+ }
1614
+
1615
+ m = base.match(/^Workflow decision made - (\w+) by (.+)$/i);
1616
+ if (m) {
1617
+ return buildItHelpdeskMuscatWorkflowLog({
1618
+ variant: 'routing_decision',
1619
+ status: m[1],
1620
+ roleName: m[2].trim(),
1621
+ roleArabicName: actors?.roleArabicName,
1622
+ comment,
1623
+ }).ar;
1624
+ }
1625
+
1626
+ m = base.match(/^L3 Technical Support assignment pending - request reassigned by (.+)$/i);
1627
+ if (m) {
1628
+ return buildItHelpdeskMuscatWorkflowLog({
1629
+ variant: 'l3_assignment_pending',
1630
+ roleName: m[1].trim(),
1631
+ roleArabicName: actors?.roleArabicName,
1632
+ comment,
1633
+ }).ar;
1634
+ }
1635
+
1636
+ m = base.match(/^L3 Technical Support approved the request$/i);
1637
+ if (m) {
1638
+ return buildItHelpdeskMuscatWorkflowLog({
1639
+ variant: 'l3_role_approved',
1640
+ roleName: 'L3 Technical Support',
1641
+ comment,
1642
+ }).ar;
1643
+ }
1644
+
1645
+ m = base.match(/^L3 Technical Support rejected the request$/i);
1646
+ if (m) {
1647
+ return buildItHelpdeskMuscatWorkflowLog({
1648
+ variant: 'l3_role_rejected',
1649
+ roleName: 'L3 Technical Support',
1650
+ comment,
1651
+ }).ar;
1652
+ }
1653
+
1654
+ m = base.match(/^(.+?) rejected the request by (.+)$/i);
1655
+ if (m && !m[1].toLowerCase().includes('workflow')) {
1656
+ return buildItHelpdeskMuscatWorkflowLog({
1657
+ variant: 'role_outcome_by_user_rejected',
1658
+ roleName: m[1].trim(),
1659
+ roleArabicName: actors?.roleArabicName,
1660
+ userName: m[2].trim(),
1661
+ userArabicName: actors?.userArabicName,
1662
+ comment,
1663
+ }).ar;
1664
+ }
1665
+
1666
+ m = base.match(/^(.+?) closed the request by (.+)$/i);
1667
+ if (m) {
1668
+ return buildItHelpdeskMuscatWorkflowLog({
1669
+ variant: 'role_outcome_by_user_closed',
1670
+ roleName: m[1].trim(),
1671
+ roleArabicName: actors?.roleArabicName,
1672
+ userName: m[2].trim(),
1673
+ userArabicName: actors?.userArabicName,
1674
+ comment,
1675
+ }).ar;
1676
+ }
1677
+
1678
+ if (/^Workflow routing completed - request rejected at technician level$/i.test(base)) {
1679
+ return buildItHelpdeskMuscatWorkflowLog({
1680
+ variant: 'routing_technician_rejected',
1681
+ comment,
1682
+ }).ar;
1683
+ }
1684
+
1685
+ m = base.match(/^HOS notification not required, request rejected by (.+)$/i);
1686
+ if (m) {
1687
+ return buildItHelpdeskMuscatWorkflowLog({
1688
+ variant: 'hos_not_required_rejected',
1689
+ userName: m[1].trim(),
1690
+ userArabicName: actors?.userArabicName,
1691
+ comment,
1692
+ }).ar;
1693
+ }
1694
+
1695
+ m = base.match(/^HOS notification not required, request closed by (.+)$/i);
1696
+ if (m) {
1697
+ return buildItHelpdeskMuscatWorkflowLog({
1698
+ variant: 'hos_not_required_closed',
1699
+ userName: m[1].trim(),
1700
+ userArabicName: actors?.userArabicName,
1701
+ comment,
1702
+ }).ar;
1703
+ }
1704
+
1705
+ m = base.match(/^L3 assignment not required, request rejected by (.+)$/i);
1706
+ if (m) {
1707
+ return buildItHelpdeskMuscatWorkflowLog({
1708
+ variant: 'l3_not_required_rejected',
1709
+ userName: m[1].trim(),
1710
+ userArabicName: actors?.userArabicName,
1711
+ comment,
1712
+ }).ar;
1713
+ }
1714
+
1715
+ m = base.match(/^L3 assignment not required, request closed by (.+)$/i);
1716
+ if (m) {
1717
+ return buildItHelpdeskMuscatWorkflowLog({
1718
+ variant: 'l3_not_required_closed',
1719
+ userName: m[1].trim(),
1720
+ userArabicName: actors?.userArabicName,
1721
+ comment,
1722
+ }).ar;
1723
+ }
1724
+
1725
+ return null;
1726
+ }
1727
+
1364
1728
  export function resolveWorkflowContentAr(
1365
1729
  content: string | null | undefined,
1366
1730
  actors?: BilingualActorNames,
@@ -1368,6 +1732,14 @@ export function resolveWorkflowContentAr(
1368
1732
  if (content == null || content.trim() === '') return null;
1369
1733
  const normalized = content.trim();
1370
1734
 
1735
+ const embeddedAssignment = tryParseRequestAssignedComment(normalized);
1736
+ if (embeddedAssignment) {
1737
+ return buildAssignmentWorkflowFieldsFromActors(embeddedAssignment, actors).ar;
1738
+ }
1739
+
1740
+ const itHelpdeskLegacyAr = tryResolveItHelpdeskMuscatLegacyWorkflowAr(normalized, actors);
1741
+ if (itHelpdeskLegacyAr) return itHelpdeskLegacyAr;
1742
+
1371
1743
  for (const tpl of Object.values(WORKFLOW_TEMPLATES)) {
1372
1744
  if (tpl.en === normalized || tpl.en.toLowerCase() === normalized.toLowerCase()) return tpl.ar;
1373
1745
  }
@@ -1383,6 +1755,10 @@ export function resolveWorkflowContentAr(
1383
1755
  if (commentIdx > 0) {
1384
1756
  const baseEn = normalized.slice(0, commentIdx).trim();
1385
1757
  const commentText = normalized.slice(commentIdx + 2).trim();
1758
+ const assignment = tryParseRequestAssignedComment(commentText);
1759
+ if (assignment) {
1760
+ return buildAssignmentWorkflowFieldsFromActors(assignment, actors).ar;
1761
+ }
1386
1762
  const commentSuffix = formatWorkflowCommentSuffix(commentText);
1387
1763
 
1388
1764
  for (const tpl of Object.values(WORKFLOW_TEMPLATES)) {
@@ -1502,7 +1878,7 @@ export function enrichWorkflowLog(log: Record<string, unknown>): Record<string,
1502
1878
  existingAr &&
1503
1879
  resolvedAr &&
1504
1880
  existingAr !== resolvedAr &&
1505
- /Request assigned to/i.test(existingAr)
1881
+ (/request assigned to/i.test(content) || /request assigned to/i.test(existingAr))
1506
1882
  ) {
1507
1883
  content_ar = resolvedAr;
1508
1884
  }
@@ -1517,6 +1893,18 @@ export function enrichWorkflowLog(log: Record<string, unknown>): Record<string,
1517
1893
  ) {
1518
1894
  content_ar = resolvedAr;
1519
1895
  }
1896
+ const itHelpdeskMixedEnglish =
1897
+ /routed to L3|assignment path|Workflow decision made|L3 Technical Support approved|L3 assignment not required|HOS notification not required|routing completed/i;
1898
+ if (
1899
+ content &&
1900
+ existingAr &&
1901
+ resolvedAr &&
1902
+ existingAr !== resolvedAr &&
1903
+ (itHelpdeskMixedEnglish.test(content) ||
1904
+ /routed to|routing completed|assignment path|\sby\s/i.test(existingAr))
1905
+ ) {
1906
+ content_ar = resolvedAr;
1907
+ }
1520
1908
  if ((content_ar == null || content_ar === '') && content) {
1521
1909
  const notificationAr = resolveWorkflowContentAr(content, actors);
1522
1910
  if (notificationAr) content_ar = notificationAr;