@platform-modules/civil-aviation-authority 2.3.310 → 2.3.311

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.
@@ -256,6 +256,7 @@ const SlaMyRequestsViewModel_1 = require("./models/SlaMyRequestsViewModel");
256
256
  const SlaApprovalsViewModel_1 = require("./models/SlaApprovalsViewModel");
257
257
  const ServiceSlaApprovalModel_1 = require("./models/ServiceSlaApprovalModel");
258
258
  const SlaConfigModel_1 = require("./models/SlaConfigModel");
259
+ const ChatbotQnsAnsMappingModel_1 = require("./models/ChatbotQnsAnsMappingModel");
259
260
  const CSRMBusinessImpactRatingMasterModel_1 = require("./models/CSRMBusinessImpactRatingMasterModel");
260
261
  const CSRMLikelihoodMasterModel_1 = require("./models/CSRMLikelihoodMasterModel");
261
262
  const LocationModel_1 = require("./models/LocationModel");
@@ -579,6 +580,7 @@ exports.AppDataSource = new typeorm_1.DataSource({
579
580
  SlaApprovalsViewModel_1.SlaApprovalsView,
580
581
  ServiceSlaApprovalModel_1.ServiceSlaApproval,
581
582
  SlaConfigModel_1.SlaConfig,
583
+ ChatbotQnsAnsMappingModel_1.ChatbotQnsAnsMapping,
582
584
  CSRMBusinessImpactRatingMasterModel_1.CSRMBusinessImpactRatingMaster,
583
585
  CSRMLikelihoodMasterModel_1.CSRMLikelihoodMaster,
584
586
  LocationModel_1.Location,
@@ -309,6 +309,14 @@
309
309
  "logistics_actual_return_updated": {
310
310
  "en": "{{roleName}} updated the Vehicle Actual Return Date, Time, and Reason. Actual Return Date: {{date}}, Actual Return Time: {{time}}, Reason: {{reason}}",
311
311
  "ar": "قام {{roleName}} بتحديث تاريخ الإرجاع الفعلي للمركبة والوقت والسبب. تاريخ الإرجاع الفعلي: {{date}}، وقت الإرجاع الفعلي: {{time}}، السبب: {{reason}}"
312
+ },
313
+ "pending_with_user": {
314
+ "en": "Pending with {{userName}} ({{employeeId}})",
315
+ "ar": "قيد الانتظار لدى {{userName}} ({{employeeId}})"
316
+ },
317
+ "pending_with_role": {
318
+ "en": "Pending with {{roleName}}",
319
+ "ar": "قيد الانتظار لدى {{roleName}}"
312
320
  }
313
321
  },
314
322
  "notificationTemplates": {
@@ -348,7 +348,7 @@ export declare function buildItHelpdeskCloseNotificationMessage(params: {
348
348
  status: 'Approved' | 'Rejected';
349
349
  }): BilingualText;
350
350
  export declare function resolveWorkflowStatusAr(status: string | null | undefined): string | null;
351
- export declare function resolveChatStatusAr(status: string | null | undefined): string | null;
351
+ export declare function resolveChatStatusAr(status: string | null | undefined, actors?: BilingualActorNames): string | null;
352
352
  /** Pull bilingual actor fields from joined relations on workflow/chat rows. */
353
353
  export declare function extractBilingualActorsFromRecord(record: Record<string, unknown>): BilingualActorNames;
354
354
  export declare function resolveWorkflowContentAr(content: string | null | undefined, actors?: BilingualActorNames): string | null;
@@ -951,10 +951,28 @@ function resolveWorkflowStatusAr(status) {
951
951
  return null;
952
952
  return WORKFLOW_STATUS_LABELS[status] ?? status;
953
953
  }
954
- function resolveChatStatusAr(status) {
954
+ function resolveChatStatusAr(status, actors) {
955
955
  if (status == null || status === '')
956
956
  return null;
957
- return CHAT_STATUS_LABELS[status] ?? status;
957
+ const labeled = CHAT_STATUS_LABELS[status];
958
+ if (labeled)
959
+ return labeled;
960
+ const normalized = status.trim();
961
+ const pendingWithUserTpl = CHAT_TEMPLATES.pending_with_user;
962
+ if (pendingWithUserTpl) {
963
+ const params = matchTemplateMessage(pendingWithUserTpl.en, normalized);
964
+ if (params) {
965
+ return interpolate(pendingWithUserTpl.ar, buildArabicChatTemplateParams(params, actors));
966
+ }
967
+ }
968
+ const pendingWithRoleTpl = CHAT_TEMPLATES.pending_with_role;
969
+ if (pendingWithRoleTpl) {
970
+ const params = matchTemplateMessage(pendingWithRoleTpl.en, normalized);
971
+ if (params) {
972
+ return interpolate(pendingWithRoleTpl.ar, buildArabicChatTemplateParams(params, actors));
973
+ }
974
+ }
975
+ return status;
958
976
  }
959
977
  // ─── Legacy row enrichment (read API) ────────────────────────────────────────
960
978
  /** Pull bilingual actor fields from joined relations on workflow/chat rows. */
@@ -1599,7 +1617,7 @@ function enrichChatMessage(chat) {
1599
1617
  return {
1600
1618
  ...chat,
1601
1619
  message_ar: chat.message_ar ?? resolveChatMessageAr(message, actors),
1602
- status_ar: chat.status_ar ?? resolveChatStatusAr(status),
1620
+ status_ar: chat.status_ar ?? resolveChatStatusAr(status, actors),
1603
1621
  };
1604
1622
  }
1605
1623
  /** @deprecated Use enrichWorkflowLog */
package/dist/index.d.ts CHANGED
@@ -414,6 +414,7 @@ export * from './models/RequestTenderAnalysisRequestModel';
414
414
  export * from './models/ContractServiceRequestModel';
415
415
  export * from './models/ContractServiceRequestDocumentModel';
416
416
  export * from './models/SlaConfigModel';
417
+ export * from './models/ChatbotQnsAnsMappingModel';
417
418
  export * from './models/SlaRequestModel';
418
419
  export * from './models/SlaMyRequestsViewModel';
419
420
  export * from './models/SlaApprovalsViewModel';
package/dist/index.js CHANGED
@@ -603,6 +603,7 @@ __exportStar(require("./models/RequestTenderAnalysisRequestModel"), exports);
603
603
  __exportStar(require("./models/ContractServiceRequestModel"), exports);
604
604
  __exportStar(require("./models/ContractServiceRequestDocumentModel"), exports);
605
605
  __exportStar(require("./models/SlaConfigModel"), exports);
606
+ __exportStar(require("./models/ChatbotQnsAnsMappingModel"), exports);
606
607
  __exportStar(require("./models/SlaRequestModel"), exports);
607
608
  __exportStar(require("./models/SlaMyRequestsViewModel"), exports);
608
609
  __exportStar(require("./models/SlaApprovalsViewModel"), exports);
@@ -0,0 +1,8 @@
1
+ import { BaseModel } from "./BaseModel";
2
+ export declare class ChatbotQnsAnsMapping extends BaseModel {
3
+ service_id: number;
4
+ sub_service_id: number;
5
+ question: Record<string, unknown> | unknown[];
6
+ answer: Record<string, unknown> | unknown[];
7
+ constructor();
8
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.ChatbotQnsAnsMapping = void 0;
13
+ const typeorm_1 = require("typeorm");
14
+ const BaseModel_1 = require("./BaseModel");
15
+ let ChatbotQnsAnsMapping = class ChatbotQnsAnsMapping extends BaseModel_1.BaseModel {
16
+ constructor() {
17
+ super();
18
+ }
19
+ };
20
+ exports.ChatbotQnsAnsMapping = ChatbotQnsAnsMapping;
21
+ __decorate([
22
+ (0, typeorm_1.Column)({ type: "int", nullable: false }),
23
+ __metadata("design:type", Number)
24
+ ], ChatbotQnsAnsMapping.prototype, "service_id", void 0);
25
+ __decorate([
26
+ (0, typeorm_1.Column)({ type: "int", nullable: false }),
27
+ __metadata("design:type", Number)
28
+ ], ChatbotQnsAnsMapping.prototype, "sub_service_id", void 0);
29
+ __decorate([
30
+ (0, typeorm_1.Column)({ type: "jsonb", nullable: false }),
31
+ __metadata("design:type", Object)
32
+ ], ChatbotQnsAnsMapping.prototype, "question", void 0);
33
+ __decorate([
34
+ (0, typeorm_1.Column)({ type: "jsonb", nullable: false }),
35
+ __metadata("design:type", Object)
36
+ ], ChatbotQnsAnsMapping.prototype, "answer", void 0);
37
+ exports.ChatbotQnsAnsMapping = ChatbotQnsAnsMapping = __decorate([
38
+ (0, typeorm_1.Entity)({ name: "chatbot_qns_ans_mapping" }),
39
+ __metadata("design:paramtypes", [])
40
+ ], ChatbotQnsAnsMapping);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platform-modules/civil-aviation-authority",
3
- "version": "2.3.310",
3
+ "version": "2.3.311",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {
@@ -0,0 +1,25 @@
1
+ -- Chatbot Q&A mapping master table (User Service)
2
+ -- PostgreSQL
3
+
4
+ CREATE TABLE IF NOT EXISTS chatbot_qns_ans_mapping (
5
+ id SERIAL PRIMARY KEY,
6
+ service_id INTEGER NOT NULL,
7
+ sub_service_id INTEGER NOT NULL,
8
+ question JSONB NOT NULL,
9
+ answer JSONB NOT NULL,
10
+ created_by INTEGER NULL,
11
+ created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
12
+ updated_by INTEGER NULL,
13
+ updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
14
+ is_deleted BOOLEAN DEFAULT FALSE
15
+ );
16
+
17
+ CREATE INDEX IF NOT EXISTS idx_chatbot_qns_ans_mapping_service_sub_service
18
+ ON chatbot_qns_ans_mapping (service_id, sub_service_id)
19
+ WHERE is_deleted = FALSE;
20
+
21
+ CREATE INDEX IF NOT EXISTS idx_chatbot_qns_ans_mapping_question_gin
22
+ ON chatbot_qns_ans_mapping USING GIN (question);
23
+
24
+ CREATE INDEX IF NOT EXISTS idx_chatbot_qns_ans_mapping_answer_gin
25
+ ON chatbot_qns_ans_mapping USING GIN (answer);
@@ -19,13 +19,15 @@ $$;
19
19
  -- Admin list views: served by Reports_Service TypeORM (sla-my-requests-view.query.ts, sla-approvals-view.query.ts)
20
20
 
21
21
  -- ---------------------------------------------------------------------------
22
- -- ADMIN — my requests Excel (optional p_target_user_id)
22
+ -- ADMIN — my requests Excel (optional p_target_user_ids INT[])
23
23
  -- Per sub-service Requests + Approvals sheets (same as user my-requests download)
24
24
  -- ---------------------------------------------------------------------------
25
25
  DROP FUNCTION IF EXISTS sp_sla_admin_my_requests_excel_export(INT, INT[], INT[], TEXT[], DATE, DATE, TEXT, TEXT, TEXT);
26
+ DROP FUNCTION IF EXISTS sp_sla_admin_my_requests_excel_export(INT, INT[], INT[], TEXT[], DATE, DATE, TEXT, TEXT, TEXT, TEXT);
27
+ DROP FUNCTION IF EXISTS sp_sla_admin_my_requests_excel_export(INT[], INT[], INT[], TEXT[], DATE, DATE, TEXT, TEXT, TEXT, TEXT);
26
28
 
27
29
  CREATE OR REPLACE FUNCTION sp_sla_admin_my_requests_excel_export(
28
- p_target_user_id INT DEFAULT NULL,
30
+ p_target_user_ids INT[] DEFAULT NULL,
29
31
  p_service_ids INT[] DEFAULT NULL,
30
32
  p_sub_service_ids INT[] DEFAULT NULL,
31
33
  p_statuses TEXT[] DEFAULT ARRAY['Pending','In Progress','Approved','Rejected'],
@@ -65,10 +67,21 @@ DECLARE
65
67
  v_k TEXT;
66
68
  v_sheet_order INT := 0;
67
69
  v_user_filter TEXT;
70
+ v_user_scope TEXT;
68
71
  BEGIN
72
+ IF p_target_user_ids IS NULL OR cardinality(p_target_user_ids) = 0 THEN
73
+ v_user_scope := 'TRUE';
74
+ ELSE
75
+ -- Validated positive ints from app; format as int array literal for ANY()
76
+ v_user_scope := format(
77
+ 'v.user_id = ANY (ARRAY[%s]::int[])',
78
+ array_to_string(p_target_user_ids, ',')
79
+ );
80
+ END IF;
81
+
69
82
  v_user_filter := format(
70
83
  '(%s) AND (%s)',
71
- CASE WHEN p_target_user_id IS NULL THEN 'TRUE' ELSE format('v.user_id = %s', p_target_user_id) END,
84
+ v_user_scope,
72
85
  COALESCE(NULLIF(trim(p_extra_filter_sql), ''), 'TRUE')
73
86
  );
74
87
 
@@ -256,15 +269,21 @@ $$;
256
269
 
257
270
  -- ---------------------------------------------------------------------------
258
271
  -- ADMIN — approvals Excel
259
- -- Per sub-service Requests + Approvals; optional approver_user_id (NULL = all queues)
272
+ -- Per sub-service Requests + Approvals; optional approver user id(s) (NULL/empty = all queues)
260
273
  -- Does not exclude own requests (unlike user approvals download)
261
274
  -- ---------------------------------------------------------------------------
262
275
  DROP FUNCTION IF EXISTS sp_sla_admin_approvals_excel_export(
263
276
  INT, INT[], INT[], TEXT[], TEXT[], DATE, DATE, TEXT, TEXT, TEXT
264
277
  );
278
+ DROP FUNCTION IF EXISTS sp_sla_admin_approvals_excel_export(
279
+ INT, INT[], INT[], TEXT[], TEXT[], DATE, DATE, TEXT, TEXT, TEXT, TEXT
280
+ );
281
+ DROP FUNCTION IF EXISTS sp_sla_admin_approvals_excel_export(
282
+ INT[], INT[], INT[], TEXT[], TEXT[], DATE, DATE, TEXT, TEXT, TEXT, TEXT
283
+ );
265
284
 
266
285
  CREATE OR REPLACE FUNCTION sp_sla_admin_approvals_excel_export(
267
- p_target_approver_user_id INT DEFAULT NULL,
286
+ p_target_approver_user_ids INT[] DEFAULT NULL,
268
287
  p_service_ids INT[] DEFAULT NULL,
269
288
  p_sub_service_ids INT[] DEFAULT NULL,
270
289
  p_request_statuses TEXT[] DEFAULT ARRAY['Pending','In Progress','Approved','Rejected'],
@@ -283,7 +302,7 @@ LANGUAGE plpgsql
283
302
  STABLE
284
303
  AS $$
285
304
  DECLARE
286
- v_exists TEXT := sla_approval_queue_exists_sql_optional(p_target_approver_user_id);
305
+ v_exists TEXT := sla_approval_queue_exists_sql_any(p_target_approver_user_ids);
287
306
  v_own TEXT := COALESCE(NULLIF(trim(p_extra_filter_sql), ''), 'TRUE');
288
307
  BEGIN
289
308
  RETURN QUERY
@@ -96,6 +96,53 @@ AS $$
96
96
  END;
97
97
  $$;
98
98
 
99
+ -- Admin: any of multiple approver user ids (NULL/empty = all queues)
100
+ CREATE OR REPLACE FUNCTION sla_approval_queue_exists_sql_any(p_user_ids INT[])
101
+ RETURNS TEXT
102
+ LANGUAGE plpgsql
103
+ STABLE
104
+ AS $$
105
+ DECLARE
106
+ v_id INT;
107
+ v_parts TEXT[] := ARRAY[]::TEXT[];
108
+ v_match TEXT;
109
+ BEGIN
110
+ IF p_user_ids IS NULL OR cardinality(p_user_ids) = 0 THEN
111
+ RETURN sla_approval_queue_exists_sql_optional(NULL);
112
+ END IF;
113
+
114
+ IF cardinality(p_user_ids) = 1 THEN
115
+ RETURN sla_approval_queue_exists_sql(p_user_ids[1]);
116
+ END IF;
117
+
118
+ FOREACH v_id IN ARRAY p_user_ids LOOP
119
+ IF v_id IS NOT NULL AND v_id > 0 THEN
120
+ v_parts := array_append(v_parts, '(' || sla_approval_matches_user('appr', v_id) || ')');
121
+ END IF;
122
+ END LOOP;
123
+
124
+ IF cardinality(v_parts) = 0 THEN
125
+ RETURN sla_approval_queue_exists_sql_optional(NULL);
126
+ END IF;
127
+
128
+ v_match := array_to_string(v_parts, ' OR ');
129
+
130
+ RETURN format(
131
+ 'EXISTS (
132
+ SELECT 1
133
+ FROM sla_approval appr
134
+ WHERE appr.request_id = sr.request_id
135
+ AND COALESCE(appr.is_deleted, false) = false
136
+ AND (appr.service_id = sr.service_id OR appr.service_id IS NULL)
137
+ AND (appr.sub_service_id = sr.sub_service_id OR appr.sub_service_id IS NULL)
138
+ AND (%s)
139
+ AND (appr.approval_status = ANY($1) OR appr.approval_status IS NULL)
140
+ )',
141
+ v_match
142
+ );
143
+ END;
144
+ $$;
145
+
99
146
  DROP FUNCTION IF EXISTS sp_sla_approvals_workbook_excel_export(
100
147
  TEXT, TEXT, INT[], INT[], TEXT[], TEXT[], DATE, DATE, TEXT, TEXT, TEXT
101
148
  );
@@ -0,0 +1,214 @@
1
+ -- =============================================================================
2
+ -- User Role – Department / Section Data Validation
3
+ -- =============================================================================
4
+ -- Purpose:
5
+ -- Identify user_role rows whose department_id and/or section_id reference
6
+ -- master records that are missing or soft-deleted (is_deleted = true).
7
+ --
8
+ -- Database: PostgreSQL (CAA / FM User Service schema)
9
+ -- Tables: user_role, departments, sections
10
+ --
11
+ -- Usage:
12
+ -- psql -U <user> -d <database> -f shared_models/sql/user_role_department_section_validation.sql
13
+ --
14
+ -- Notes:
15
+ -- * department_id / section_id may be NULL on user_role; NULL is treated as
16
+ -- "not assigned" and is not flagged unless an ID is present but invalid.
17
+ -- * Soft-deleted user_role rows are included by default (see filter comment).
18
+ -- =============================================================================
19
+
20
+ -- -----------------------------------------------------------------------------
21
+ -- 1) Detailed invalid records with reason
22
+ -- -----------------------------------------------------------------------------
23
+ WITH validated AS (
24
+ SELECT
25
+ ur.id AS user_role_id,
26
+ ur.role_id,
27
+ ur.user_id,
28
+ ur.department_id,
29
+ ur.section_id,
30
+ ur.is_active,
31
+ COALESCE(ur.is_deleted, false) AS user_role_is_deleted,
32
+ ur.created_at,
33
+ ur.updated_at,
34
+
35
+ r.name AS role_name,
36
+ u.employee_name AS user_name,
37
+ u.email AS user_email,
38
+
39
+ d.id AS department_master_id,
40
+ d.department_name,
41
+ COALESCE(d.is_deleted, false) AS department_is_deleted,
42
+
43
+ s.id AS section_master_id,
44
+ s.section_name,
45
+ COALESCE(s.is_deleted, false) AS section_is_deleted,
46
+
47
+ CASE
48
+ WHEN ur.department_id IS NULL THEN 'NOT_APPLICABLE'
49
+ WHEN d.id IS NULL THEN 'DEPARTMENT_NOT_FOUND'
50
+ WHEN COALESCE(d.is_deleted, false) = true THEN 'DEPARTMENT_DELETED'
51
+ ELSE 'VALID'
52
+ END AS department_status,
53
+
54
+ CASE
55
+ WHEN ur.section_id IS NULL THEN 'NOT_APPLICABLE'
56
+ WHEN s.id IS NULL THEN 'SECTION_NOT_FOUND'
57
+ WHEN COALESCE(s.is_deleted, false) = true THEN 'SECTION_DELETED'
58
+ ELSE 'VALID'
59
+ END AS section_status
60
+
61
+ FROM user_role ur
62
+ LEFT JOIN role r
63
+ ON r.id = ur.role_id
64
+ LEFT JOIN users u
65
+ ON u.id = ur.user_id
66
+ LEFT JOIN departments d
67
+ ON d.id = ur.department_id
68
+ LEFT JOIN sections s
69
+ ON s.id = ur.section_id
70
+ -- Uncomment to restrict to active user_role rows only:
71
+ -- WHERE COALESCE(ur.is_deleted, false) = false
72
+ )
73
+ SELECT
74
+ user_role_id,
75
+ role_id,
76
+ role_name,
77
+ user_id,
78
+ user_name,
79
+ user_email,
80
+ department_id,
81
+ department_name,
82
+ section_id,
83
+ section_name,
84
+ is_active,
85
+ user_role_is_deleted,
86
+ department_status,
87
+ section_status,
88
+ CASE
89
+ WHEN department_status IN ('DEPARTMENT_NOT_FOUND', 'DEPARTMENT_DELETED')
90
+ AND section_status IN ('SECTION_NOT_FOUND', 'SECTION_DELETED')
91
+ THEN 'INVALID_DEPARTMENT_AND_SECTION'
92
+ WHEN department_status IN ('DEPARTMENT_NOT_FOUND', 'DEPARTMENT_DELETED')
93
+ THEN 'INVALID_DEPARTMENT'
94
+ WHEN section_status IN ('SECTION_NOT_FOUND', 'SECTION_DELETED')
95
+ THEN 'INVALID_SECTION'
96
+ END AS invalid_reason,
97
+ CASE
98
+ WHEN department_status IN ('DEPARTMENT_NOT_FOUND', 'DEPARTMENT_DELETED')
99
+ AND section_status IN ('SECTION_NOT_FOUND', 'SECTION_DELETED')
100
+ THEN department_status || ' | ' || section_status
101
+ WHEN department_status IN ('DEPARTMENT_NOT_FOUND', 'DEPARTMENT_DELETED')
102
+ THEN department_status
103
+ WHEN section_status IN ('SECTION_NOT_FOUND', 'SECTION_DELETED')
104
+ THEN section_status
105
+ END AS invalid_reason_detail,
106
+ created_at,
107
+ updated_at
108
+ FROM validated
109
+ WHERE
110
+ department_status IN ('DEPARTMENT_NOT_FOUND', 'DEPARTMENT_DELETED')
111
+ OR section_status IN ('SECTION_NOT_FOUND', 'SECTION_DELETED')
112
+ ORDER BY
113
+ invalid_reason,
114
+ user_role_id;
115
+
116
+
117
+ -- -----------------------------------------------------------------------------
118
+ -- 2) Summary counts by invalidity category
119
+ -- -----------------------------------------------------------------------------
120
+ WITH validated AS (
121
+ SELECT
122
+ ur.id AS user_role_id,
123
+ CASE
124
+ WHEN ur.department_id IS NULL THEN 'NOT_APPLICABLE'
125
+ WHEN d.id IS NULL THEN 'DEPARTMENT_NOT_FOUND'
126
+ WHEN COALESCE(d.is_deleted, false) = true THEN 'DEPARTMENT_DELETED'
127
+ ELSE 'VALID'
128
+ END AS department_status,
129
+ CASE
130
+ WHEN ur.section_id IS NULL THEN 'NOT_APPLICABLE'
131
+ WHEN s.id IS NULL THEN 'SECTION_NOT_FOUND'
132
+ WHEN COALESCE(s.is_deleted, false) = true THEN 'SECTION_DELETED'
133
+ ELSE 'VALID'
134
+ END AS section_status
135
+ FROM user_role ur
136
+ LEFT JOIN departments d ON d.id = ur.department_id
137
+ LEFT JOIN sections s ON s.id = ur.section_id
138
+ -- Uncomment to restrict to active user_role rows only:
139
+ -- WHERE COALESCE(ur.is_deleted, false) = false
140
+ ),
141
+ invalid AS (
142
+ SELECT
143
+ user_role_id,
144
+ CASE
145
+ WHEN department_status IN ('DEPARTMENT_NOT_FOUND', 'DEPARTMENT_DELETED')
146
+ AND section_status IN ('SECTION_NOT_FOUND', 'SECTION_DELETED')
147
+ THEN 'INVALID_DEPARTMENT_AND_SECTION'
148
+ WHEN department_status IN ('DEPARTMENT_NOT_FOUND', 'DEPARTMENT_DELETED')
149
+ THEN 'INVALID_DEPARTMENT'
150
+ WHEN section_status IN ('SECTION_NOT_FOUND', 'SECTION_DELETED')
151
+ THEN 'INVALID_SECTION'
152
+ END AS invalid_reason,
153
+ department_status,
154
+ section_status
155
+ FROM validated
156
+ WHERE
157
+ department_status IN ('DEPARTMENT_NOT_FOUND', 'DEPARTMENT_DELETED')
158
+ OR section_status IN ('SECTION_NOT_FOUND', 'SECTION_DELETED')
159
+ )
160
+ SELECT
161
+ invalid_reason,
162
+ COUNT(*) AS record_count
163
+ FROM invalid
164
+ GROUP BY invalid_reason
165
+
166
+ UNION ALL
167
+
168
+ SELECT
169
+ 'TOTAL_INVALID_RECORDS' AS invalid_reason,
170
+ COUNT(*) AS record_count
171
+ FROM invalid
172
+
173
+ ORDER BY
174
+ CASE invalid_reason
175
+ WHEN 'INVALID_DEPARTMENT' THEN 1
176
+ WHEN 'INVALID_SECTION' THEN 2
177
+ WHEN 'INVALID_DEPARTMENT_AND_SECTION' THEN 3
178
+ WHEN 'TOTAL_INVALID_RECORDS' THEN 4
179
+ ELSE 5
180
+ END;
181
+
182
+
183
+ -- -----------------------------------------------------------------------------
184
+ -- 3) Breakdown by underlying master-table status (optional drill-down)
185
+ -- -----------------------------------------------------------------------------
186
+ WITH validated AS (
187
+ SELECT
188
+ ur.id AS user_role_id,
189
+ CASE
190
+ WHEN ur.department_id IS NULL THEN 'NOT_APPLICABLE'
191
+ WHEN d.id IS NULL THEN 'DEPARTMENT_NOT_FOUND'
192
+ WHEN COALESCE(d.is_deleted, false) = true THEN 'DEPARTMENT_DELETED'
193
+ ELSE 'VALID'
194
+ END AS department_status,
195
+ CASE
196
+ WHEN ur.section_id IS NULL THEN 'NOT_APPLICABLE'
197
+ WHEN s.id IS NULL THEN 'SECTION_NOT_FOUND'
198
+ WHEN COALESCE(s.is_deleted, false) = true THEN 'SECTION_DELETED'
199
+ ELSE 'VALID'
200
+ END AS section_status
201
+ FROM user_role ur
202
+ LEFT JOIN departments d ON d.id = ur.department_id
203
+ LEFT JOIN sections s ON s.id = ur.section_id
204
+ )
205
+ SELECT
206
+ department_status,
207
+ section_status,
208
+ COUNT(*) AS record_count
209
+ FROM validated
210
+ WHERE
211
+ department_status IN ('DEPARTMENT_NOT_FOUND', 'DEPARTMENT_DELETED')
212
+ OR section_status IN ('SECTION_NOT_FOUND', 'SECTION_DELETED')
213
+ GROUP BY department_status, section_status
214
+ ORDER BY record_count DESC, department_status, section_status;
@@ -254,6 +254,7 @@ import { SlaMyRequestsView } from './models/SlaMyRequestsViewModel';
254
254
  import { SlaApprovalsView } from './models/SlaApprovalsViewModel';
255
255
  import { ServiceSlaApproval } from './models/ServiceSlaApprovalModel';
256
256
  import { SlaConfig } from './models/SlaConfigModel';
257
+ import { ChatbotQnsAnsMapping } from './models/ChatbotQnsAnsMappingModel';
257
258
  import { CSRMBusinessImpactRatingMaster } from './models/CSRMBusinessImpactRatingMasterModel';
258
259
  import { CSRMLikelihoodMaster } from './models/CSRMLikelihoodMasterModel';
259
260
  import { Location } from './models/LocationModel';
@@ -579,6 +580,7 @@ export const AppDataSource = new DataSource({
579
580
  SlaApprovalsView,
580
581
  ServiceSlaApproval,
581
582
  SlaConfig,
583
+ ChatbotQnsAnsMapping,
582
584
  CSRMBusinessImpactRatingMaster,
583
585
  CSRMLikelihoodMaster,
584
586
  Location,
@@ -309,6 +309,14 @@
309
309
  "logistics_actual_return_updated": {
310
310
  "en": "{{roleName}} updated the Vehicle Actual Return Date, Time, and Reason. Actual Return Date: {{date}}, Actual Return Time: {{time}}, Reason: {{reason}}",
311
311
  "ar": "قام {{roleName}} بتحديث تاريخ الإرجاع الفعلي للمركبة والوقت والسبب. تاريخ الإرجاع الفعلي: {{date}}، وقت الإرجاع الفعلي: {{time}}، السبب: {{reason}}"
312
+ },
313
+ "pending_with_user": {
314
+ "en": "Pending with {{userName}} ({{employeeId}})",
315
+ "ar": "قيد الانتظار لدى {{userName}} ({{employeeId}})"
316
+ },
317
+ "pending_with_role": {
318
+ "en": "Pending with {{roleName}}",
319
+ "ar": "قيد الانتظار لدى {{roleName}}"
312
320
  }
313
321
  },
314
322
  "notificationTemplates": {
@@ -1545,9 +1545,33 @@ export function resolveWorkflowStatusAr(status: string | null | undefined): stri
1545
1545
  return WORKFLOW_STATUS_LABELS[status] ?? status;
1546
1546
  }
1547
1547
 
1548
- export function resolveChatStatusAr(status: string | null | undefined): string | null {
1548
+ export function resolveChatStatusAr(
1549
+ status: string | null | undefined,
1550
+ actors?: BilingualActorNames,
1551
+ ): string | null {
1549
1552
  if (status == null || status === '') return null;
1550
- return CHAT_STATUS_LABELS[status] ?? status;
1553
+
1554
+ const labeled = CHAT_STATUS_LABELS[status];
1555
+ if (labeled) return labeled;
1556
+
1557
+ const normalized = status.trim();
1558
+ const pendingWithUserTpl = CHAT_TEMPLATES.pending_with_user;
1559
+ if (pendingWithUserTpl) {
1560
+ const params = matchTemplateMessage(pendingWithUserTpl.en, normalized);
1561
+ if (params) {
1562
+ return interpolate(pendingWithUserTpl.ar, buildArabicChatTemplateParams(params, actors));
1563
+ }
1564
+ }
1565
+
1566
+ const pendingWithRoleTpl = CHAT_TEMPLATES.pending_with_role;
1567
+ if (pendingWithRoleTpl) {
1568
+ const params = matchTemplateMessage(pendingWithRoleTpl.en, normalized);
1569
+ if (params) {
1570
+ return interpolate(pendingWithRoleTpl.ar, buildArabicChatTemplateParams(params, actors));
1571
+ }
1572
+ }
1573
+
1574
+ return status;
1551
1575
  }
1552
1576
 
1553
1577
  // ─── Legacy row enrichment (read API) ────────────────────────────────────────
@@ -2310,7 +2334,7 @@ export function enrichChatMessage(chat: Record<string, unknown>): Record<string,
2310
2334
  return {
2311
2335
  ...chat,
2312
2336
  message_ar: chat.message_ar ?? resolveChatMessageAr(message, actors),
2313
- status_ar: chat.status_ar ?? resolveChatStatusAr(status),
2337
+ status_ar: chat.status_ar ?? resolveChatStatusAr(status, actors),
2314
2338
  };
2315
2339
  }
2316
2340
 
package/src/index.ts CHANGED
@@ -504,6 +504,7 @@ export * from './models/RequestTenderAnalysisRequestModel';
504
504
  export * from './models/ContractServiceRequestModel';
505
505
  export * from './models/ContractServiceRequestDocumentModel';
506
506
  export * from './models/SlaConfigModel';
507
+ export * from './models/ChatbotQnsAnsMappingModel';
507
508
  export * from './models/SlaRequestModel';
508
509
  export * from './models/SlaMyRequestsViewModel';
509
510
  export * from './models/SlaApprovalsViewModel';
@@ -0,0 +1,21 @@
1
+ import { Column, Entity } from "typeorm";
2
+ import { BaseModel } from "./BaseModel";
3
+
4
+ @Entity({ name: "chatbot_qns_ans_mapping" })
5
+ export class ChatbotQnsAnsMapping extends BaseModel {
6
+ @Column({ type: "int", nullable: false })
7
+ service_id: number;
8
+
9
+ @Column({ type: "int", nullable: false })
10
+ sub_service_id: number;
11
+
12
+ @Column({ type: "jsonb", nullable: false })
13
+ question: Record<string, unknown> | unknown[];
14
+
15
+ @Column({ type: "jsonb", nullable: false })
16
+ answer: Record<string, unknown> | unknown[];
17
+
18
+ constructor() {
19
+ super();
20
+ }
21
+ }