@redacto.io/consent-sdk-react 3.6.0 → 5.0.0

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 (103) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/index.d.mts +4 -4
  3. package/dist/index.d.ts +4 -4
  4. package/dist/index.js +36 -7
  5. package/dist/index.mjs +36 -7
  6. package/dist/privacy-center.d.mts +134 -51
  7. package/dist/privacy-center.d.ts +134 -51
  8. package/dist/privacy-center.js +6391 -2741
  9. package/dist/privacy-center.mjs +6135 -2482
  10. package/package.json +1 -1
  11. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +5 -5
  12. package/src/RedactoNoticeConsent/language-codes.ts +37 -1
  13. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -3
  14. package/src/RedactoNoticeConsentInline/api/index.ts +0 -1
  15. package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +154 -0
  16. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +3 -0
  17. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +81 -50
  18. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +4 -0
  19. package/src/RedactoPrivacyCenter/api/actions.ts +68 -36
  20. package/src/RedactoPrivacyCenter/api/client.ts +125 -14
  21. package/src/RedactoPrivacyCenter/components/CaseDetails/AttachmentCard.tsx +38 -0
  22. package/src/RedactoPrivacyCenter/components/CaseDetails/ChatBubble.tsx +31 -0
  23. package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +73 -0
  24. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +87 -0
  25. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +95 -0
  26. package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +147 -0
  27. package/src/RedactoPrivacyCenter/components/CaseDetails/RequestDetailsTab.tsx +171 -0
  28. package/src/RedactoPrivacyCenter/components/CaseDetails/StatusEvent.tsx +23 -0
  29. package/src/RedactoPrivacyCenter/components/CaseDetails/useFileUpload.ts +116 -0
  30. package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +36 -0
  31. package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +56 -0
  32. package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +1 -0
  33. package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +6 -13
  34. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +6 -0
  35. package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +0 -1
  36. package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +2 -2
  37. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +2 -3
  38. package/src/RedactoPrivacyCenter/components/Form.tsx +152 -11
  39. package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +4 -6
  40. package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +9 -2
  41. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +1 -0
  42. package/src/RedactoPrivacyCenter/components/Pagination.tsx +12 -12
  43. package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +3 -1
  44. package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +15 -1
  45. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +8 -8
  46. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +7 -11
  47. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +1 -1
  48. package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +1 -1
  49. package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +4 -24
  50. package/src/RedactoPrivacyCenter/components/constant.ts +3 -7
  51. package/src/RedactoPrivacyCenter/components/types.ts +12 -4
  52. package/src/RedactoPrivacyCenter/context/AuthContext.tsx +30 -16
  53. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +3 -6
  54. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +35 -3
  55. package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +1 -1
  56. package/src/RedactoPrivacyCenter/context/types.ts +14 -0
  57. package/src/RedactoPrivacyCenter/lib/api.ts +101 -3
  58. package/src/RedactoPrivacyCenter/lib/constants.ts +46 -1
  59. package/src/RedactoPrivacyCenter/lib/i18n.ts +1 -1
  60. package/src/RedactoPrivacyCenter/lib/queries.ts +106 -2
  61. package/src/RedactoPrivacyCenter/lib/types.ts +186 -1
  62. package/src/RedactoPrivacyCenter/lib/utils.ts +1 -0
  63. package/src/RedactoPrivacyCenter/locales/as/translation.json +93 -1
  64. package/src/RedactoPrivacyCenter/locales/bho/translation.json +93 -1
  65. package/src/RedactoPrivacyCenter/locales/bn/translation.json +93 -1
  66. package/src/RedactoPrivacyCenter/locales/brx/translation.json +93 -1
  67. package/src/RedactoPrivacyCenter/locales/doi/translation.json +93 -1
  68. package/src/RedactoPrivacyCenter/locales/en/translation.json +69 -2
  69. package/src/RedactoPrivacyCenter/locales/gom/translation.json +93 -1
  70. package/src/RedactoPrivacyCenter/locales/gu/translation.json +93 -1
  71. package/src/RedactoPrivacyCenter/locales/hi/translation.json +93 -1
  72. package/src/RedactoPrivacyCenter/locales/kn/translation.json +93 -1
  73. package/src/RedactoPrivacyCenter/locales/ks/translation.json +93 -1
  74. package/src/RedactoPrivacyCenter/locales/mai/translation.json +93 -1
  75. package/src/RedactoPrivacyCenter/locales/ml/translation.json +93 -1
  76. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +93 -1
  77. package/src/RedactoPrivacyCenter/locales/mr/translation.json +93 -1
  78. package/src/RedactoPrivacyCenter/locales/ne/translation.json +93 -1
  79. package/src/RedactoPrivacyCenter/locales/or/translation.json +93 -1
  80. package/src/RedactoPrivacyCenter/locales/pa/translation.json +93 -1
  81. package/src/RedactoPrivacyCenter/locales/sa/translation.json +93 -1
  82. package/src/RedactoPrivacyCenter/locales/sat/translation.json +93 -1
  83. package/src/RedactoPrivacyCenter/locales/sd/translation.json +93 -1
  84. package/src/RedactoPrivacyCenter/locales/ta/translation.json +93 -1
  85. package/src/RedactoPrivacyCenter/locales/te/translation.json +93 -1
  86. package/src/RedactoPrivacyCenter/locales/ur/translation.json +93 -1
  87. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +725 -0
  88. package/src/RedactoPrivacyCenter/styles/pcStyles.ts +0 -6
  89. package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +5 -3
  90. package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +9 -12
  91. package/src/RedactoPrivacyCenter/ui/Footer.tsx +4 -2
  92. package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +24 -18
  93. package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +30 -20
  94. package/src/RedactoPrivacyCenter/ui/PCInput.tsx +17 -13
  95. package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +3 -2
  96. package/src/RedactoPrivacyCenter/ui/PCModal.tsx +3 -1
  97. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +0 -2
  98. package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +10 -4
  99. package/src/RedactoPrivacyCenter/ui/PCTable.tsx +44 -32
  100. package/src/RedactoPrivacyCenter/ui/PCText.tsx +5 -3
  101. package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +0 -2
  102. package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +4 -2
  103. package/src/privacy-center.ts +1 -0
@@ -1,5 +1,10 @@
1
1
  import { apiFetch } from "./fetcher";
2
2
  import type { ApiFetchOptions, ApiResponse } from "./fetcher";
3
+ import type {
4
+ CaseMessage,
5
+ MessageDocument,
6
+ UploadDocumentResponse,
7
+ } from "../lib/types";
3
8
 
4
9
  /**
5
10
  * Base request parameters for consent service calls
@@ -81,6 +86,7 @@ export function createConsentClient(baseUrl: string) {
81
86
  const errorData = (await response.json()) as {
82
87
  detail?: unknown;
83
88
  message?: string;
89
+ code?: string;
84
90
  } | null;
85
91
  if (errorData) {
86
92
  if ("detail" in errorData && errorData.detail) {
@@ -90,6 +96,9 @@ export function createConsentClient(baseUrl: string) {
90
96
  : JSON.stringify(errorData.detail, null, 2);
91
97
  } else if ("message" in errorData && errorData.message) {
92
98
  errorMessage = errorData.message;
99
+ } else if ("code" in errorData && errorData.code) {
100
+ // e.g. { code: "empty_file" } or { code: "document_already_exists" }
101
+ errorMessage = errorData.code;
93
102
  }
94
103
  }
95
104
  } catch {
@@ -104,7 +113,8 @@ export function createConsentClient(baseUrl: string) {
104
113
  }
105
114
  }
106
115
 
107
- throw new Error(errorMessage);
116
+ // Prepend HTTP status so callers can branch on it without losing context
117
+ throw new Error(`[${response.status}] ${errorMessage}`);
108
118
  }
109
119
 
110
120
  // Parse JSON response
@@ -139,6 +149,18 @@ export function createConsentClient(baseUrl: string) {
139
149
  }
140
150
  }
141
151
 
152
+ function privacyCenterUrl(baseRequest: BaseRequest, path: string): string {
153
+ return `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center${path}`;
154
+ }
155
+
156
+ function caseUrl(baseRequest: BaseRequest, caseUuid: string, path = ''): string {
157
+ return privacyCenterUrl(baseRequest, `/cases/${caseUuid}${path}`);
158
+ }
159
+
160
+ function resolveAuthToken(authToken: string | undefined, baseRequest: BaseRequest): string {
161
+ return authToken || baseRequest.authToken;
162
+ }
163
+
142
164
  return {
143
165
  /**
144
166
  * Get form data (privacy center)
@@ -153,7 +175,7 @@ export function createConsentClient(baseUrl: string) {
153
175
  const queryParams = new URLSearchParams();
154
176
  if (language) queryParams.append("language", language);
155
177
  const queryString = queryParams.toString();
156
- const url = `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center/form/data${queryString ? `?${queryString}` : ""}`;
178
+ const url = privacyCenterUrl(baseRequest, `/form/data${queryString ? `?${queryString}` : ""}`);
157
179
  const { useSessionAuth = false, getAuthToken, ...fetchOptions } = options || {};
158
180
 
159
181
  // Case management uses session-based auth (X-Auth-Token)
@@ -172,7 +194,7 @@ export function createConsentClient(baseUrl: string) {
172
194
  ...fetchOptions,
173
195
  method: "POST",
174
196
  body: JSON.stringify({ contact }),
175
- authToken: authToken || baseRequest.authToken,
197
+ authToken: resolveAuthToken(authToken, baseRequest),
176
198
  });
177
199
  },
178
200
 
@@ -185,7 +207,7 @@ export function createConsentClient(baseUrl: string) {
185
207
  authToken?: string,
186
208
  options?: ApiFetchOptions & { useSessionAuth?: boolean }
187
209
  ): Promise<unknown> {
188
- const url = `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center/case`;
210
+ const url = privacyCenterUrl(baseRequest, '/case');
189
211
  const { useSessionAuth = false, getAuthToken, ...fetchOptions } = options || {};
190
212
 
191
213
  // Case management uses session-based auth (X-Auth-Token)
@@ -204,7 +226,7 @@ export function createConsentClient(baseUrl: string) {
204
226
  ...fetchOptions,
205
227
  method: "POST",
206
228
  body: JSON.stringify(payload),
207
- authToken: authToken || baseRequest.authToken,
229
+ authToken: resolveAuthToken(authToken, baseRequest),
208
230
  });
209
231
  },
210
232
 
@@ -325,12 +347,12 @@ export function createConsentClient(baseUrl: string) {
325
347
  if (product_uuid) queryParams.append("product_uuid", product_uuid);
326
348
  if (language) queryParams.append("language", language);
327
349
  const queryString = queryParams.toString();
328
- const url = `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center/user-consents${queryString ? `?${queryString}` : ""}`;
350
+ const url = privacyCenterUrl(baseRequest, `/user-consents${queryString ? `?${queryString}` : ""}`);
329
351
  return privacyCenterApiFetch(url, {
330
352
  ...options,
331
353
  method: "POST",
332
354
  body: JSON.stringify({ contact }),
333
- authToken: authToken || baseRequest.authToken,
355
+ authToken: resolveAuthToken(authToken, baseRequest),
334
356
  });
335
357
  },
336
358
 
@@ -347,7 +369,7 @@ export function createConsentClient(baseUrl: string) {
347
369
  nominatorContact?: string,
348
370
  dataElementUuids?: string[]
349
371
  ): Promise<unknown> {
350
- const url = `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center/manage-consent?status=${status}`;
372
+ const url = privacyCenterUrl(baseRequest, `/manage-consent?status=${status}`);
351
373
  const body: Record<string, unknown> = { purpose_uuid: purposeId, contact };
352
374
  if (nominatorContact) {
353
375
  body.nominator_contact = nominatorContact;
@@ -359,7 +381,7 @@ export function createConsentClient(baseUrl: string) {
359
381
  ...options,
360
382
  method: "POST",
361
383
  body: JSON.stringify(body),
362
- authToken: authToken || baseRequest.authToken,
384
+ authToken: resolveAuthToken(authToken, baseRequest),
363
385
  });
364
386
  },
365
387
 
@@ -379,10 +401,10 @@ export function createConsentClient(baseUrl: string) {
379
401
  if (limit !== undefined) queryParams.append("limit", limit.toString());
380
402
  if (language) queryParams.append("language", language);
381
403
  const queryString = queryParams.toString();
382
- const url = `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center/activities${queryString ? `?${queryString}` : ""}`;
404
+ const url = privacyCenterUrl(baseRequest, `/activities${queryString ? `?${queryString}` : ""}`);
383
405
  return privacyCenterApiFetch<unknown>(url, {
384
406
  ...options,
385
- authToken: authToken || baseRequest.authToken,
407
+ authToken: resolveAuthToken(authToken, baseRequest),
386
408
  });
387
409
  },
388
410
 
@@ -391,7 +413,7 @@ export function createConsentClient(baseUrl: string) {
391
413
  */
392
414
  async getCaseHistory(
393
415
  baseRequest: BaseRequest,
394
- contact: string,
416
+ _contact: string,
395
417
  offset?: number,
396
418
  limit?: number,
397
419
  language?: string,
@@ -403,10 +425,99 @@ export function createConsentClient(baseUrl: string) {
403
425
  if (limit !== undefined) queryParams.append("limit", limit.toString());
404
426
  if (language) queryParams.append("language", language);
405
427
  const queryString = queryParams.toString();
406
- const url = `${baseUrl}/organisations/${baseRequest.organisationUuid}/workspaces/${baseRequest.workspaceUuid}/dsar/privacy-center/cases?${queryString}`;
428
+ const url = privacyCenterUrl(baseRequest, `/cases?${queryString}`);
407
429
  return privacyCenterApiFetch<unknown>(url, {
408
430
  ...options,
409
- authToken: authToken || baseRequest.authToken,
431
+ authToken: resolveAuthToken(authToken, baseRequest),
432
+ });
433
+ },
434
+
435
+ // ─── Case Communication ─────────────────────────────────────────────────
436
+
437
+ /**
438
+ * Get messages for a case (privacy center)
439
+ */
440
+ async getCaseMessages(
441
+ baseRequest: BaseRequest,
442
+ caseUuid: string,
443
+ authToken?: string
444
+ ): Promise<CaseMessage[]> {
445
+ const url = caseUrl(baseRequest, caseUuid, '/messages');
446
+ return privacyCenterApiFetch<CaseMessage[]>(url, {
447
+ authToken: resolveAuthToken(authToken, baseRequest),
448
+ });
449
+ },
450
+
451
+ /**
452
+ * Send a message on a case (privacy center)
453
+ */
454
+ async sendCaseMessage(
455
+ baseRequest: BaseRequest,
456
+ caseUuid: string,
457
+ body: string,
458
+ documentUuids: string[],
459
+ authToken?: string
460
+ ): Promise<CaseMessage> {
461
+ const url = caseUrl(baseRequest, caseUuid, '/messages');
462
+ return privacyCenterApiFetch<CaseMessage>(url, {
463
+ method: 'POST',
464
+ body: JSON.stringify({ body, document_uuids: documentUuids }),
465
+ authToken: resolveAuthToken(authToken, baseRequest),
466
+ });
467
+ },
468
+
469
+ /**
470
+ * Upload a document on a case (multipart/form-data, privacy center)
471
+ */
472
+ async uploadCaseDocument(
473
+ baseRequest: BaseRequest,
474
+ caseUuid: string,
475
+ file: File,
476
+ authToken?: string
477
+ ): Promise<UploadDocumentResponse> {
478
+ const url = caseUrl(baseRequest, caseUuid, '/upload-document');
479
+ const formData = new FormData();
480
+ formData.append('file', file, file.name); // field name must be exactly "file"
481
+ // "payload" is required by the privacy-center endpoint (omitting it causes 422)
482
+ formData.append('payload', JSON.stringify({ metadata: null }));
483
+ return privacyCenterApiFetch<UploadDocumentResponse>(url, {
484
+ method: 'POST',
485
+ body: formData,
486
+ authToken: resolveAuthToken(authToken, baseRequest),
487
+ });
488
+ },
489
+
490
+ /**
491
+ * Submit a document against a handler document request (privacy center)
492
+ */
493
+ async submitDocumentForRequest(
494
+ baseRequest: BaseRequest,
495
+ caseUuid: string,
496
+ requestEventUuid: string,
497
+ documentUuid: string,
498
+ body: string,
499
+ authToken?: string
500
+ ): Promise<CaseMessage> {
501
+ const url = caseUrl(baseRequest, caseUuid, `/document-requests/${requestEventUuid}/submit`);
502
+ return privacyCenterApiFetch<CaseMessage>(url, {
503
+ method: 'POST',
504
+ body: JSON.stringify({ document_uuid: documentUuid, body }),
505
+ authToken: resolveAuthToken(authToken, baseRequest),
506
+ });
507
+ },
508
+
509
+ /**
510
+ * Get document info/download URL for a case document (privacy center)
511
+ */
512
+ async getCaseDocument(
513
+ baseRequest: BaseRequest,
514
+ caseUuid: string,
515
+ documentUuid: string,
516
+ authToken?: string
517
+ ): Promise<MessageDocument> {
518
+ const url = caseUrl(baseRequest, caseUuid, `/documents/${documentUuid}`);
519
+ return privacyCenterApiFetch<MessageDocument>(url, {
520
+ authToken: resolveAuthToken(authToken, baseRequest),
410
521
  });
411
522
  },
412
523
  };
@@ -0,0 +1,38 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import { IoDocumentText } from 'react-icons/io5';
3
+ import type { AttachmentCardProps } from '../../lib/types';
4
+ import { formatFileSize, getDownloadUrl } from './utils';
5
+
6
+ export const AttachmentCard = ({ doc }: AttachmentCardProps) => {
7
+ const { t } = useTranslation();
8
+ const downloadUrl = getDownloadUrl(doc.file_url);
9
+ const fileName = doc.file_name || t('document');
10
+
11
+ const content = (
12
+ <>
13
+ <IoDocumentText size={20} className="pc-attachment-icon" />
14
+ <div className="pc-attachment-info">
15
+ <div className="pc-attachment-name">{fileName}</div>
16
+ <div className="pc-attachment-size">
17
+ {doc.file_size ? formatFileSize(doc.file_size) : ''}
18
+ </div>
19
+ </div>
20
+ </>
21
+ );
22
+
23
+ if (downloadUrl) {
24
+ return (
25
+ <a
26
+ href={downloadUrl}
27
+ target="_blank"
28
+ rel="noopener noreferrer"
29
+ aria-label={t('downloadAttachment', { fileName })}
30
+ className="pc-attachment-card"
31
+ >
32
+ {content}
33
+ </a>
34
+ );
35
+ }
36
+
37
+ return <div className="pc-attachment-card">{content}</div>;
38
+ };
@@ -0,0 +1,31 @@
1
+ import type { ChatBubbleProps } from '../../lib/types';
2
+ import { AttachmentCard } from './AttachmentCard';
3
+
4
+ export const ChatBubble = ({
5
+ message,
6
+ isPrincipal,
7
+ senderLabel,
8
+ timestamp,
9
+ }: ChatBubbleProps) => {
10
+ const direction = isPrincipal ? 'sent' : 'received';
11
+
12
+ return (
13
+ <div className={`pc-chat-bubble-wrap pc-chat-bubble-wrap--${direction}`}>
14
+ <span className="pc-chat-sender">
15
+ {senderLabel} · {timestamp}
16
+ </span>
17
+
18
+ {/* Text bubble */}
19
+ {message.body && (
20
+ <div className={`pc-chat-bubble pc-chat-bubble--${direction}`}>
21
+ {message.body}
22
+ </div>
23
+ )}
24
+
25
+ {/* Attachment cards */}
26
+ {message.documents.map((doc) => (
27
+ <AttachmentCard key={doc.uuid} doc={doc} />
28
+ ))}
29
+ </div>
30
+ );
31
+ };
@@ -0,0 +1,73 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import { IoAttach } from 'react-icons/io5';
3
+ import { PCButton, PCLoader } from '../../ui';
4
+ import type { DocumentRequestCardProps } from '../../lib/types';
5
+ import { UPLOAD_ACTIVE_STATUSES, DOCUMENT_REQUEST_STATUS_LABEL, STATUS_COLORS } from '../../lib/constants';
6
+
7
+ export const DocumentRequestCard = ({
8
+ message,
9
+ onUpload,
10
+ isSubmitting,
11
+ }: DocumentRequestCardProps) => {
12
+ const { t } = useTranslation();
13
+ const meta = message.document_request_metadata;
14
+ if (!meta) return null;
15
+
16
+ const canUpload = UPLOAD_ACTIVE_STATUSES.includes(meta.document_request_status);
17
+ const statusInfo = DOCUMENT_REQUEST_STATUS_LABEL[meta.document_request_status];
18
+
19
+ return (
20
+ <div
21
+ className="pc-doc-request-card"
22
+ style={{
23
+ border: `1.5px dashed ${STATUS_COLORS.info.border}`,
24
+ backgroundColor: STATUS_COLORS.info.bg,
25
+ }}
26
+ >
27
+ <div className="pc-doc-request-header">
28
+ <IoAttach size={16} className="pc-doc-request-icon" style={{ color: STATUS_COLORS.info.color }} />
29
+ <div>
30
+ <div className="pc-doc-request-title" style={{ color: STATUS_COLORS.infoDark }}>
31
+ {t('documentRequestedLabel', { title: meta.title })}
32
+ </div>
33
+ {meta.accepted_document && (
34
+ <div className="pc-doc-request-accepted" style={{ color: STATUS_COLORS.info.color }}>
35
+ {t('acceptedLabel', { accepted: meta.accepted_document })}
36
+ </div>
37
+ )}
38
+ </div>
39
+
40
+ <span
41
+ className="pc-doc-request-status"
42
+ style={{ color: statusInfo.color, border: `1px solid ${statusInfo.color}` }}
43
+ >
44
+ {t(statusInfo.labelKey)}
45
+ </span>
46
+ </div>
47
+
48
+ {canUpload && (
49
+ <PCButton
50
+ variant="outline"
51
+ size="sm"
52
+ disabled={isSubmitting}
53
+ onClick={() => onUpload(message.uuid)}
54
+ className="pc-doc-request-upload-btn"
55
+ >
56
+ {isSubmitting
57
+ ? <PCLoader variant="spinner" size="sm" />
58
+ : <IoAttach size={14} />}
59
+ {t('uploadAndSubmit')}
60
+ </PCButton>
61
+ )}
62
+
63
+ {meta.document_request_status === 'rejected' && meta.rejection_reason && (
64
+ <div
65
+ className="pc-doc-request-rejection"
66
+ style={{ color: STATUS_COLORS.error.color, backgroundColor: STATUS_COLORS.error.bg }}
67
+ >
68
+ {t('rejectionReasonLabel', { reason: meta.rejection_reason })}
69
+ </div>
70
+ )}
71
+ </div>
72
+ );
73
+ };
@@ -0,0 +1,87 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import { IoAttach, IoSend, IoDocumentText } from 'react-icons/io5';
3
+ import { PCButton, PCLoader } from '../../ui';
4
+ import type { MessageComposerProps } from '../../lib/types';
5
+ import { formatFileSize } from './utils';
6
+
7
+ export const MessageComposer = ({
8
+ messageText,
9
+ onMessageTextChange,
10
+ pendingUpload,
11
+ onRemovePendingUpload,
12
+ onAttachClick,
13
+ onSend,
14
+ isSending,
15
+ isUploading,
16
+ }: MessageComposerProps) => {
17
+ const { t } = useTranslation();
18
+
19
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
20
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
21
+ e.preventDefault();
22
+ onSend();
23
+ }
24
+ };
25
+
26
+ return (
27
+ <div className="pc-composer">
28
+ {pendingUpload && (
29
+ <div className="pc-composer-pending-row">
30
+ <div className="pc-composer-pending-pill">
31
+ <IoDocumentText size={14} />
32
+ <span>{pendingUpload.file_name}</span>
33
+ <span className="pc-composer-pending-size">
34
+ {formatFileSize(pendingUpload.file_size)}
35
+ </span>
36
+ <button
37
+ type="button"
38
+ aria-label={t('removeAttachment')}
39
+ onClick={onRemovePendingUpload}
40
+ className="pc-composer-remove-btn"
41
+ >
42
+ ×
43
+ </button>
44
+ </div>
45
+ </div>
46
+ )}
47
+
48
+ <div className="pc-composer-textarea-wrap">
49
+ <textarea
50
+ value={messageText}
51
+ onChange={(e) => onMessageTextChange(e.target.value)}
52
+ onKeyDown={handleKeyDown}
53
+ placeholder={t('writeMessagePlaceholder')}
54
+ rows={3}
55
+ className="pc-composer-textarea"
56
+ />
57
+ </div>
58
+ <div className="pc-composer-actions">
59
+ <PCButton
60
+ variant="outline"
61
+ size="sm"
62
+ disabled={isUploading || isSending}
63
+ onClick={onAttachClick}
64
+ className="pc-composer-btn"
65
+ >
66
+ {isUploading
67
+ ? <PCLoader variant="spinner" size="sm" />
68
+ : <IoAttach size={16} />}
69
+ {t('attachFile')}
70
+ </PCButton>
71
+ <PCButton
72
+ size="sm"
73
+ disabled={(!messageText.trim() && !pendingUpload) || isSending || isUploading}
74
+ onClick={onSend}
75
+ className="pc-composer-btn"
76
+ >
77
+ {isSending ? <PCLoader variant="spinner" size="sm" /> : null}
78
+ {t('send')}
79
+ <IoSend size={14} />
80
+ </PCButton>
81
+ </div>
82
+ <div className="pc-composer-footer">
83
+ {t('messagesSecureFooter')}
84
+ </div>
85
+ </div>
86
+ );
87
+ };
@@ -0,0 +1,95 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import {
3
+ IoCheckmarkCircle, IoCloseCircle, IoAlertCircle,
4
+ } from 'react-icons/io5';
5
+ import type { MessageItemProps } from '../../lib/types';
6
+ import { STATUS_COLORS } from '../../lib/constants';
7
+ import { formatMessageTime } from './utils';
8
+ import { DocumentRequestCard } from './DocumentRequestCard';
9
+ import { StatusEvent } from './StatusEvent';
10
+ import { ChatBubble } from './ChatBubble';
11
+
12
+ export const MessageItem = ({
13
+ message,
14
+ onUploadForRequest,
15
+ isSubmittingForRequest,
16
+ }: MessageItemProps) => {
17
+ const { t } = useTranslation();
18
+ const isPrincipal = message.sender_role === 'data_principal';
19
+ const timestamp = formatMessageTime(message.created_at);
20
+ const senderLabel = isPrincipal ? t('senderYou') : message.triggered_by_email || t('senderPrivacyTeam');
21
+
22
+ switch (message.message_type) {
23
+ case 'document_requested':
24
+ return (
25
+ <>
26
+ {message.body && (
27
+ <div className="pc-chat-bubble-wrap pc-chat-bubble-wrap--received">
28
+ <span className="pc-chat-sender">
29
+ {senderLabel} · {timestamp}
30
+ </span>
31
+ <div className="pc-chat-bubble pc-chat-bubble--received">
32
+ {message.body}
33
+ </div>
34
+ </div>
35
+ )}
36
+ <DocumentRequestCard
37
+ message={message}
38
+ onUpload={onUploadForRequest}
39
+ isSubmitting={isSubmittingForRequest}
40
+ />
41
+ </>
42
+ );
43
+
44
+ case 'document_approved':
45
+ return (
46
+ <StatusEvent
47
+ icon={<IoCheckmarkCircle size={18} />}
48
+ color={STATUS_COLORS.success.color}
49
+ bgColor={STATUS_COLORS.success.bg}
50
+ borderColor={STATUS_COLORS.success.border}
51
+ text={t('documentAccepted')}
52
+ timestamp={timestamp}
53
+ />
54
+ );
55
+
56
+ case 'document_rejected':
57
+ return (
58
+ <StatusEvent
59
+ icon={<IoCloseCircle size={18} />}
60
+ color={STATUS_COLORS.error.color}
61
+ bgColor={STATUS_COLORS.error.bg}
62
+ borderColor={STATUS_COLORS.error.border}
63
+ text={t('documentRejected')}
64
+ subText={message.document_request_metadata?.rejection_reason}
65
+ timestamp={timestamp}
66
+ />
67
+ );
68
+
69
+ case 'document_replacement_requested':
70
+ return (
71
+ <StatusEvent
72
+ icon={<IoAlertCircle size={18} />}
73
+ color={STATUS_COLORS.warning.color}
74
+ bgColor={STATUS_COLORS.warning.bg}
75
+ borderColor={STATUS_COLORS.warning.border}
76
+ text={t('replacementRequested')}
77
+ subText={message.body}
78
+ timestamp={timestamp}
79
+ />
80
+ );
81
+
82
+ case 'document_submitted':
83
+ case 'document_replaced':
84
+ case 'message_sent':
85
+ default:
86
+ return (
87
+ <ChatBubble
88
+ message={message}
89
+ isPrincipal={isPrincipal}
90
+ senderLabel={senderLabel}
91
+ timestamp={timestamp}
92
+ />
93
+ );
94
+ }
95
+ };