@redacto.io/consent-sdk-react 4.0.0 → 5.1.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 (161) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/LICENSE +176 -0
  3. package/README.md +98 -1
  4. package/dist/index.js +231 -96
  5. package/dist/index.mjs +231 -96
  6. package/dist/privacy-center.d.mts +649 -0
  7. package/dist/privacy-center.d.ts +649 -0
  8. package/dist/privacy-center.js +20824 -0
  9. package/dist/privacy-center.mjs +20789 -0
  10. package/package.json +47 -3
  11. package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +6 -4
  12. package/src/RedactoNoticeConsent/api/index.ts +2 -0
  13. package/src/RedactoNoticeConsent/api/types.ts +2 -0
  14. package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +128 -62
  15. package/src/RedactoNoticeConsent/language-codes.ts +97 -0
  16. package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -3
  17. package/src/RedactoNoticeConsentInline/api/index.ts +0 -1
  18. package/src/RedactoPrivacyCenter/PrivacyCenterActivityPage.tsx +19 -0
  19. package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +154 -0
  20. package/src/RedactoPrivacyCenter/PrivacyCenterConsentManagerPage.tsx +17 -0
  21. package/src/RedactoPrivacyCenter/PrivacyCenterFormPage.tsx +79 -0
  22. package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +30 -0
  23. package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +173 -0
  24. package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +55 -0
  25. package/src/RedactoPrivacyCenter/api/actions.ts +476 -0
  26. package/src/RedactoPrivacyCenter/api/client.ts +524 -0
  27. package/src/RedactoPrivacyCenter/api/config.ts +33 -0
  28. package/src/RedactoPrivacyCenter/api/fetcher.ts +181 -0
  29. package/src/RedactoPrivacyCenter/api/upload.ts +93 -0
  30. package/src/RedactoPrivacyCenter/assets/character.svg +31 -0
  31. package/src/RedactoPrivacyCenter/assets/fileAttach.svg +9 -0
  32. package/src/RedactoPrivacyCenter/assets/greenTick.svg +4 -0
  33. package/src/RedactoPrivacyCenter/assets/mail.svg +3 -0
  34. package/src/RedactoPrivacyCenter/assets/poweredByRedacto.svg +9 -0
  35. package/src/RedactoPrivacyCenter/assets/sentImg.svg +9 -0
  36. package/src/RedactoPrivacyCenter/assets/uploadFileIcon.svg +9 -0
  37. package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +272 -0
  38. package/src/RedactoPrivacyCenter/components/CaseDetails/AttachmentCard.tsx +38 -0
  39. package/src/RedactoPrivacyCenter/components/CaseDetails/ChatBubble.tsx +31 -0
  40. package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +73 -0
  41. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +87 -0
  42. package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +95 -0
  43. package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +147 -0
  44. package/src/RedactoPrivacyCenter/components/CaseDetails/RequestDetailsTab.tsx +171 -0
  45. package/src/RedactoPrivacyCenter/components/CaseDetails/StatusEvent.tsx +23 -0
  46. package/src/RedactoPrivacyCenter/components/CaseDetails/useFileUpload.ts +116 -0
  47. package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +36 -0
  48. package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +56 -0
  49. package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +469 -0
  50. package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +426 -0
  51. package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +732 -0
  52. package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +238 -0
  53. package/src/RedactoPrivacyCenter/components/ConsentManagerConstants.ts +7 -0
  54. package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +155 -0
  55. package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +189 -0
  56. package/src/RedactoPrivacyCenter/components/DataRequest/GrievanceDataSection.tsx +118 -0
  57. package/src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection.tsx +66 -0
  58. package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +304 -0
  59. package/src/RedactoPrivacyCenter/components/Form.tsx +1596 -0
  60. package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +174 -0
  61. package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +337 -0
  62. package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +330 -0
  63. package/src/RedactoPrivacyCenter/components/Pagination.tsx +140 -0
  64. package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +304 -0
  65. package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +232 -0
  66. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +475 -0
  67. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +408 -0
  68. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +42 -0
  69. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/menuItemsPC.tsx +1 -0
  70. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +40 -0
  71. package/src/RedactoPrivacyCenter/components/SideMenuBarPC/utils.ts +38 -0
  72. package/src/RedactoPrivacyCenter/components/ThemeTransitionOverlay.tsx +85 -0
  73. package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +141 -0
  74. package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +159 -0
  75. package/src/RedactoPrivacyCenter/components/constant.ts +16 -0
  76. package/src/RedactoPrivacyCenter/components/types.ts +76 -0
  77. package/src/RedactoPrivacyCenter/components/utils.tsx +10 -0
  78. package/src/RedactoPrivacyCenter/config/routes.ts +9 -0
  79. package/src/RedactoPrivacyCenter/config/table.ts +25 -0
  80. package/src/RedactoPrivacyCenter/context/AuthContext.tsx +137 -0
  81. package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +41 -0
  82. package/src/RedactoPrivacyCenter/context/LocaleContext.tsx +11 -0
  83. package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +96 -0
  84. package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +30 -0
  85. package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +60 -0
  86. package/src/RedactoPrivacyCenter/context/theme.tsx +127 -0
  87. package/src/RedactoPrivacyCenter/context/types.ts +14 -0
  88. package/src/RedactoPrivacyCenter/lib/api.ts +566 -0
  89. package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +63 -0
  90. package/src/RedactoPrivacyCenter/lib/constants.ts +161 -0
  91. package/src/RedactoPrivacyCenter/lib/i18n.ts +81 -0
  92. package/src/RedactoPrivacyCenter/lib/locales.ts +105 -0
  93. package/src/RedactoPrivacyCenter/lib/queries.ts +134 -0
  94. package/src/RedactoPrivacyCenter/lib/types.ts +531 -0
  95. package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +37 -0
  96. package/src/RedactoPrivacyCenter/lib/utils.ts +626 -0
  97. package/src/RedactoPrivacyCenter/locales/as/translation.json +280 -0
  98. package/src/RedactoPrivacyCenter/locales/bho/translation.json +280 -0
  99. package/src/RedactoPrivacyCenter/locales/bn/translation.json +280 -0
  100. package/src/RedactoPrivacyCenter/locales/brx/translation.json +280 -0
  101. package/src/RedactoPrivacyCenter/locales/doi/translation.json +280 -0
  102. package/src/RedactoPrivacyCenter/locales/en/translation.json +255 -0
  103. package/src/RedactoPrivacyCenter/locales/gom/translation.json +280 -0
  104. package/src/RedactoPrivacyCenter/locales/gu/translation.json +280 -0
  105. package/src/RedactoPrivacyCenter/locales/hi/translation.json +280 -0
  106. package/src/RedactoPrivacyCenter/locales/kn/translation.json +280 -0
  107. package/src/RedactoPrivacyCenter/locales/ks/translation.json +280 -0
  108. package/src/RedactoPrivacyCenter/locales/mai/translation.json +280 -0
  109. package/src/RedactoPrivacyCenter/locales/ml/translation.json +280 -0
  110. package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +280 -0
  111. package/src/RedactoPrivacyCenter/locales/mr/translation.json +280 -0
  112. package/src/RedactoPrivacyCenter/locales/ne/translation.json +280 -0
  113. package/src/RedactoPrivacyCenter/locales/or/translation.json +280 -0
  114. package/src/RedactoPrivacyCenter/locales/pa/translation.json +280 -0
  115. package/src/RedactoPrivacyCenter/locales/sa/translation.json +280 -0
  116. package/src/RedactoPrivacyCenter/locales/sat/translation.json +280 -0
  117. package/src/RedactoPrivacyCenter/locales/sd/translation.json +280 -0
  118. package/src/RedactoPrivacyCenter/locales/ta/translation.json +280 -0
  119. package/src/RedactoPrivacyCenter/locales/te/translation.json +280 -0
  120. package/src/RedactoPrivacyCenter/locales/ur/translation.json +280 -0
  121. package/src/RedactoPrivacyCenter/styles/base.css +117 -0
  122. package/src/RedactoPrivacyCenter/styles/injectStyles.ts +1725 -0
  123. package/src/RedactoPrivacyCenter/styles/pcStyles.ts +813 -0
  124. package/src/RedactoPrivacyCenter/styles/responsive.ts +138 -0
  125. package/src/RedactoPrivacyCenter/styles/variables.css +133 -0
  126. package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +367 -0
  127. package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +476 -0
  128. package/src/RedactoPrivacyCenter/table/index.ts +2 -0
  129. package/src/RedactoPrivacyCenter/types/images.d.ts +29 -0
  130. package/src/RedactoPrivacyCenter/ui/Footer.tsx +75 -0
  131. package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +54 -0
  132. package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +93 -0
  133. package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +47 -0
  134. package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +69 -0
  135. package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +48 -0
  136. package/src/RedactoPrivacyCenter/ui/PCButton.tsx +77 -0
  137. package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +55 -0
  138. package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +107 -0
  139. package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +35 -0
  140. package/src/RedactoPrivacyCenter/ui/PCInput.tsx +132 -0
  141. package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +31 -0
  142. package/src/RedactoPrivacyCenter/ui/PCModal.tsx +106 -0
  143. package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +173 -0
  144. package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +387 -0
  145. package/src/RedactoPrivacyCenter/ui/PCTable.tsx +149 -0
  146. package/src/RedactoPrivacyCenter/ui/PCText.tsx +41 -0
  147. package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +43 -0
  148. package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +37 -0
  149. package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +119 -0
  150. package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +25 -0
  151. package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +53 -0
  152. package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +72 -0
  153. package/src/RedactoPrivacyCenter/ui/index.ts +18 -0
  154. package/src/RedactoPrivacyCenter/utils/cn.ts +28 -0
  155. package/src/RedactoPrivacyCenter/utils/date.ts +48 -0
  156. package/src/RedactoPrivacyCenter/utils/status.ts +111 -0
  157. package/src/RedactoPrivacyCenter/utils/toast.ts +130 -0
  158. package/src/privacy-center.ts +108 -0
  159. package/tsconfig.json +2 -0
  160. package/tsup.config.ts +16 -1
  161. package/.turbo/turbo-build.log +0 -21
@@ -0,0 +1,147 @@
1
+ import { useState, useMemo, useCallback } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { IoDocumentText, IoRefresh } from 'react-icons/io5';
4
+ import { PCText, PCLoader } from '../../ui';
5
+ import { useOrganization } from '../../context/OrganizationContext';
6
+ import { useAuth } from '../../context/AuthContext';
7
+ import {
8
+ useCaseMessages,
9
+ useSendCaseMessage,
10
+ useUploadCaseDocument,
11
+ useSubmitDocumentForRequest,
12
+ } from '../../lib/queries';
13
+ import { STATUS_COLORS } from '../../lib/constants';
14
+ import type { MessagesTabProps } from '../../lib/types';
15
+ import { MessageItem } from './MessageItem';
16
+ import { MessageComposer } from './MessageComposer';
17
+ import { useMessagePolling } from './useMessagePolling';
18
+ import { useFileUpload } from './useFileUpload';
19
+
20
+ export const MessagesTab = ({ caseUuid }: MessagesTabProps) => {
21
+ const { t } = useTranslation();
22
+ const { organization } = useOrganization();
23
+ const { accessToken, refreshToken, onError, updateTokens } = useAuth();
24
+ const authContext = useMemo(
25
+ () => ({ accessToken, refreshToken, onError, updateTokens }),
26
+ [accessToken, refreshToken, onError, updateTokens]
27
+ );
28
+
29
+ const organizationId = organization?.uuid;
30
+ const workspaceId = organization?.workspaces[0]?.uuid;
31
+
32
+ const { data: messages = [], isLoading, isError, isFetching, refetch } = useCaseMessages(
33
+ organizationId, workspaceId, caseUuid, authContext
34
+ );
35
+
36
+ const sendMutation = useSendCaseMessage(organizationId, workspaceId, caseUuid, authContext);
37
+ const uploadMutation = useUploadCaseDocument(organizationId, workspaceId, caseUuid, authContext);
38
+ const submitMutation = useSubmitDocumentForRequest(organizationId, workspaceId, caseUuid, authContext);
39
+
40
+ const { scrollContainerRef, handleScroll, scrollToBottom } = useMessagePolling(refetch, messages);
41
+
42
+ const {
43
+ pendingUpload, setPendingUpload, submittingForRequest,
44
+ attachInputRef, submitInputRef,
45
+ handleAttachFile, handleSubmitInputChange, triggerUploadForRequest,
46
+ } = useFileUpload(uploadMutation, submitMutation, refetch, scrollToBottom);
47
+
48
+ const [messageText, setMessageText] = useState('');
49
+
50
+ const handleSend = useCallback(() => {
51
+ if (sendMutation.isPending || uploadMutation.isPending) return;
52
+ const trimmedBody = messageText.trim();
53
+ if (!trimmedBody && !pendingUpload) return;
54
+ sendMutation.mutate(
55
+ { body: trimmedBody, documentUuids: pendingUpload ? [pendingUpload.uuid] : [] },
56
+ {
57
+ onSuccess: () => {
58
+ setMessageText('');
59
+ setPendingUpload(null);
60
+ scrollToBottom();
61
+ refetch();
62
+ },
63
+ }
64
+ );
65
+ }, [messageText, pendingUpload, sendMutation, uploadMutation, refetch, setPendingUpload, scrollToBottom]);
66
+
67
+ const isSending = sendMutation.isPending;
68
+ const isUploading = uploadMutation.isPending;
69
+
70
+ return (
71
+ <div className="pc-messages-container">
72
+ {/* Hidden file inputs */}
73
+ <input ref={attachInputRef} type="file" className="pc-hidden" onChange={handleAttachFile} />
74
+ <input ref={submitInputRef} type="file" className="pc-hidden" onChange={handleSubmitInputChange} />
75
+
76
+ {/* Secure notice */}
77
+ <div
78
+ className="pc-messages-notice"
79
+ style={{
80
+ backgroundColor: STATUS_COLORS.infoBanner.bg,
81
+ borderBottom: `1px solid ${STATUS_COLORS.infoBanner.border}`,
82
+ color: STATUS_COLORS.infoBanner.color,
83
+ }}
84
+ >
85
+ <span>🔒</span>
86
+ <span className="pc-messages-notice-text">{t('secureMessagesNotice')}</span>
87
+ <button
88
+ type="button"
89
+ onClick={() => refetch()}
90
+ disabled={isFetching}
91
+ aria-label={t('refreshMessages', 'Refresh messages')}
92
+ title={t('refreshMessages', 'Refresh messages')}
93
+ className="pc-messages-refresh-btn"
94
+ style={{ color: STATUS_COLORS.infoBanner.color }}
95
+ >
96
+ <IoRefresh
97
+ size={16}
98
+ style={{ animation: isFetching ? 'pc-spin 1s linear infinite' : 'none' }}
99
+ />
100
+ </button>
101
+ </div>
102
+
103
+ {/* Messages list */}
104
+ <div
105
+ ref={scrollContainerRef}
106
+ onScroll={handleScroll}
107
+ className="pc-messages-list"
108
+ >
109
+ {isLoading && (
110
+ <div className="pc-messages-center">
111
+ <PCLoader variant="spinner" size="md" />
112
+ </div>
113
+ )}
114
+ {isError && (
115
+ <div className="pc-messages-center-text">
116
+ <PCText size="sm" color="secondary">{t('errorLoadingMessages')}</PCText>
117
+ </div>
118
+ )}
119
+ {!isLoading && !isError && messages.length === 0 && (
120
+ <div className="pc-messages-empty">
121
+ <IoDocumentText size={40} className="pc-messages-empty-icon" />
122
+ <PCText size="sm" color="secondary">{t('noMessagesYet')}</PCText>
123
+ </div>
124
+ )}
125
+ {messages.map((msg) => (
126
+ <MessageItem
127
+ key={msg.uuid}
128
+ message={msg}
129
+ onUploadForRequest={triggerUploadForRequest}
130
+ isSubmittingForRequest={submittingForRequest === msg.uuid}
131
+ />
132
+ ))}
133
+ </div>
134
+
135
+ <MessageComposer
136
+ messageText={messageText}
137
+ onMessageTextChange={setMessageText}
138
+ pendingUpload={pendingUpload}
139
+ onRemovePendingUpload={() => setPendingUpload(null)}
140
+ onAttachClick={() => attachInputRef.current?.click()}
141
+ onSend={handleSend}
142
+ isSending={isSending}
143
+ isUploading={isUploading}
144
+ />
145
+ </div>
146
+ );
147
+ };
@@ -0,0 +1,171 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import { PCText, PCBadge, PCTable } from '../../ui';
3
+ import { formatDate } from '../utils';
4
+ import type { RequestDetailsTabProps, RawPurposeWrapper, DataElement, UpdateCorrectionDataItem, GrievanceTypeElement } from '../../lib/types';
5
+ import { getCaseStatusVariant, formatCaseStatus } from './utils';
6
+
7
+ export const RequestDetailsTab = ({
8
+ selectedCase,
9
+ request_details,
10
+ requestTypeLabel,
11
+ }: RequestDetailsTabProps) => {
12
+ const { t } = useTranslation();
13
+ return (
14
+ <div className="pc-request-details">
15
+ {/* Summary card */}
16
+ <div className="pc-request-summary-card">
17
+ <div className="pc-case-details-grid pc-request-summary-grid">
18
+ <div>
19
+ <PCText size="xs" weight="medium" color="secondary" className="pc-request-field-label">
20
+ {t('requestType')}
21
+ </PCText>
22
+ <PCText size="sm" weight="semibold" className="pc-request-field-value--capitalize">
23
+ {requestTypeLabel}
24
+ </PCText>
25
+ </div>
26
+ <div>
27
+ <PCText size="xs" weight="medium" color="secondary" className="pc-request-field-label">
28
+ {t('status')}
29
+ </PCText>
30
+ <PCBadge variant={getCaseStatusVariant(selectedCase.status)} size="sm" className="pc-status-badge">
31
+ {selectedCase.status_display || formatCaseStatus(selectedCase.status)}
32
+ </PCBadge>
33
+ </div>
34
+ <div>
35
+ <PCText size="xs" weight="medium" color="secondary" className="pc-request-field-label">
36
+ {t('createdAt')}
37
+ </PCText>
38
+ <PCText size="sm" weight="medium" className="pc-request-field-value">
39
+ {formatDate(selectedCase.created_at)}
40
+ </PCText>
41
+ </div>
42
+ {selectedCase.completed_at && (
43
+ <div>
44
+ <PCText size="xs" weight="medium" color="secondary" className="pc-request-field-label">
45
+ {t('completedAt')}
46
+ </PCText>
47
+ <PCText size="sm" weight="medium" className="pc-request-field-value">
48
+ {formatDate(selectedCase.completed_at)}
49
+ </PCText>
50
+ </div>
51
+ )}
52
+ </div>
53
+ </div>
54
+
55
+ {/* Description */}
56
+ {(selectedCase.description_display || selectedCase.description) && (
57
+ <div className="pc-request-description">
58
+ <PCText size="sm" weight="semibold" className="pc-request-description-title">
59
+ {t('descriptionLabel')}
60
+ </PCText>
61
+ <PCText size="sm" color="secondary" className="pc-request-description-text">
62
+ {selectedCase.description_display || selectedCase.description}
63
+ </PCText>
64
+ </div>
65
+ )}
66
+
67
+ {/* Request specifics */}
68
+ {request_details && (
69
+ <div className="pc-request-specifics">
70
+ <div className="pc-request-section-divider">
71
+ <span className="pc-request-section-title">{t('requestSpecifics')}</span>
72
+ </div>
73
+
74
+ {request_details.purposes && request_details.purposes.length > 0 && (
75
+ <div>
76
+ <PCText size="sm" weight="semibold" className="pc-request-section-heading">
77
+ {t('purposesInvolved')}
78
+ </PCText>
79
+ <ul className="pc-request-purpose-list">
80
+ {request_details.purposes.map((p: RawPurposeWrapper) => (
81
+ <li key={p.purpose?.uuid} className="pc-request-purpose-item">
82
+ <div className="pc-request-purpose-name">
83
+ {p.purpose?.name}
84
+ </div>
85
+ {p.purpose?.description && (
86
+ <div className="pc-request-purpose-desc">
87
+ {p.purpose.description}
88
+ </div>
89
+ )}
90
+ {p.purpose?.data_elements && p.purpose.data_elements.some((de: DataElement) => de.selected) && (
91
+ <div className="pc-request-data-elements">
92
+ {p.purpose.data_elements.filter((de: DataElement) => de.selected).map((de: DataElement) => (
93
+ <PCBadge key={de.uuid} variant="secondary" size="sm" className="pc-status-badge">{de.name}</PCBadge>
94
+ ))}
95
+ </div>
96
+ )}
97
+ </li>
98
+ ))}
99
+ </ul>
100
+ </div>
101
+ )}
102
+
103
+ {request_details.correction_data && request_details.correction_data.length > 0 && (
104
+ <div>
105
+ <PCText size="sm" weight="semibold" className="pc-request-section-heading">
106
+ {t('dataToCorrect')}
107
+ </PCText>
108
+ <div className="pcOverflowXAuto">
109
+ <div className="pc-request-table-wrap">
110
+ <PCTable className="pc-request-table">
111
+ <PCTable.Header className="pc-request-table-header">
112
+ <PCTable.Row>
113
+ {[t('field'), t('currentValue'), t('newValue')].map((h) => (
114
+ <PCTable.Head key={h} className="pc-request-table-head">
115
+ {h}
116
+ </PCTable.Head>
117
+ ))}
118
+ </PCTable.Row>
119
+ </PCTable.Header>
120
+ <PCTable.Body>
121
+ {request_details.correction_data.map((item: UpdateCorrectionDataItem) => (
122
+ <PCTable.Row key={item.name} className="pc-request-table-row">
123
+ <PCTable.Cell className="pc-request-table-cell pc-request-table-cell--name">{item.name}</PCTable.Cell>
124
+ <PCTable.Cell className="pc-request-table-cell pc-request-table-cell--value">{item.curr_value || '-'}</PCTable.Cell>
125
+ <PCTable.Cell className="pc-request-table-cell pc-request-table-cell--value">{item.new_value || '-'}</PCTable.Cell>
126
+ </PCTable.Row>
127
+ ))}
128
+ </PCTable.Body>
129
+ </PCTable>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ )}
134
+
135
+ {request_details.nomination_data && (
136
+ <div>
137
+ <PCText size="sm" weight="semibold" className="pc-request-section-heading">
138
+ {t('nomination')}
139
+ </PCText>
140
+ <div className="pc-request-nomination-card">
141
+ <div className="pc-request-nomination-field">
142
+ <PCText size="xs" weight="medium" color="secondary">{t('nomineeEmail')}</PCText>
143
+ <PCText size="sm" weight="medium" className="pc-request-field-value">{request_details.nomination_data.nominee_email || '-'}</PCText>
144
+ </div>
145
+ <div className="pc-request-nomination-field">
146
+ <PCText size="xs" weight="medium" color="secondary">{t('nomineeMobile')}</PCText>
147
+ <PCText size="sm" weight="medium" className="pc-request-field-value">{request_details.nomination_data.nominee_mobile || '-'}</PCText>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ )}
152
+
153
+ {request_details.grievance_types && request_details.grievance_types.length > 0 && (
154
+ <div>
155
+ <PCText size="sm" weight="semibold" className="pc-request-section-heading">
156
+ {t('grievanceTypes')}
157
+ </PCText>
158
+ <div className="pc-request-grievance-wrap">
159
+ {request_details.grievance_types.map((g: GrievanceTypeElement) => (
160
+ <PCBadge key={g.grievance} variant="secondary" size="sm" className="pc-status-badge">
161
+ {g.grievance?.replace('_', ' ')}
162
+ </PCBadge>
163
+ ))}
164
+ </div>
165
+ </div>
166
+ )}
167
+ </div>
168
+ )}
169
+ </div>
170
+ );
171
+ };
@@ -0,0 +1,23 @@
1
+ import type { StatusEventProps } from '../../lib/types';
2
+
3
+ export const StatusEvent = ({
4
+ icon, color, bgColor, borderColor, text, subText, timestamp,
5
+ }: StatusEventProps) => {
6
+ return (
7
+ <div className="pc-status-event">
8
+ <div
9
+ className="pc-status-event-pill"
10
+ style={{ backgroundColor: bgColor, border: `1px solid ${borderColor}`, color }}
11
+ >
12
+ {icon}
13
+ <span>{text}</span>
14
+ </div>
15
+ {subText && (
16
+ <div className="pc-status-event-sub" style={{ color }}>{subText}</div>
17
+ )}
18
+ <span className="pc-status-event-time">
19
+ {timestamp}
20
+ </span>
21
+ </div>
22
+ );
23
+ };
@@ -0,0 +1,116 @@
1
+ import { useState, useRef, useCallback } from 'react';
2
+ import type { PendingUpload } from '../../lib/types';
3
+ import { toast } from '../../utils/toast';
4
+ import { validateUploadFile, getUploadErrorMessage } from './utils';
5
+
6
+ type UploadMutation = {
7
+ mutate: (file: File, opts: {
8
+ onSuccess: (result: { uuid: string; file_name: string; file_size: number } | null) => void;
9
+ onError: (error: unknown) => void;
10
+ }) => void;
11
+ isPending: boolean;
12
+ };
13
+
14
+ type SubmitMutation = {
15
+ mutate: (params: { requestEventUuid: string; documentUuid: string; body: string }, opts: {
16
+ onSuccess: () => void;
17
+ onError: () => void;
18
+ }) => void;
19
+ };
20
+
21
+ export function useFileUpload(
22
+ uploadMutation: UploadMutation,
23
+ submitMutation: SubmitMutation,
24
+ refetch: () => void,
25
+ scrollToBottom: () => void,
26
+ ) {
27
+ const [pendingUpload, setPendingUpload] = useState<PendingUpload | null>(null);
28
+ const [submittingForRequest, setSubmittingForRequest] = useState<string | null>(null);
29
+
30
+ const attachInputRef = useRef<HTMLInputElement>(null);
31
+ const submitInputRef = useRef<HTMLInputElement>(null);
32
+ const pendingRequestRef = useRef<string | null>(null);
33
+
34
+ const handleAttachFile = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
35
+ const file = e.target.files?.[0];
36
+ if (!file) return;
37
+ e.target.value = '';
38
+ const validationError = validateUploadFile(file);
39
+ if (validationError) {
40
+ toast.error(validationError);
41
+ return;
42
+ }
43
+ uploadMutation.mutate(file, {
44
+ onSuccess: (result) => {
45
+ if (result) {
46
+ setPendingUpload({ uuid: result.uuid, file_name: result.file_name, file_size: result.file_size });
47
+ toast.success(`"${result.file_name}" uploaded successfully`);
48
+ } else {
49
+ toast.error('Document upload failed. Please try again.');
50
+ }
51
+ },
52
+ onError: (error) => {
53
+ toast.error(getUploadErrorMessage(error));
54
+ },
55
+ });
56
+ }, [uploadMutation]);
57
+
58
+ const handleSubmitForRequest = useCallback((requestEventUuid: string, file: File) => {
59
+ const validationError = validateUploadFile(file);
60
+ if (validationError) {
61
+ toast.error(validationError);
62
+ return;
63
+ }
64
+ setSubmittingForRequest(requestEventUuid);
65
+ uploadMutation.mutate(file, {
66
+ onSuccess: (uploaded) => {
67
+ if (!uploaded) {
68
+ setSubmittingForRequest(null);
69
+ toast.error('Document upload failed. Please try again.');
70
+ return;
71
+ }
72
+ toast.success(`"${uploaded.file_name}" uploaded successfully`);
73
+ submitMutation.mutate(
74
+ { requestEventUuid, documentUuid: uploaded.uuid, body: '' },
75
+ {
76
+ onSuccess: () => { setSubmittingForRequest(null); scrollToBottom(); refetch(); },
77
+ onError: () => {
78
+ setSubmittingForRequest(null);
79
+ toast.error('Failed to submit document. Please try again.');
80
+ },
81
+ }
82
+ );
83
+ },
84
+ onError: (error) => {
85
+ setSubmittingForRequest(null);
86
+ toast.error(getUploadErrorMessage(error));
87
+ },
88
+ });
89
+ }, [uploadMutation, submitMutation, refetch, scrollToBottom]);
90
+
91
+ const handleSubmitInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
92
+ const file = e.target.files?.[0];
93
+ e.target.value = '';
94
+ const requestUuid = pendingRequestRef.current;
95
+ pendingRequestRef.current = null;
96
+ if (file && requestUuid) {
97
+ handleSubmitForRequest(requestUuid, file);
98
+ }
99
+ }, [handleSubmitForRequest]);
100
+
101
+ const triggerUploadForRequest = useCallback((requestEventUuid: string) => {
102
+ pendingRequestRef.current = requestEventUuid;
103
+ submitInputRef.current?.click();
104
+ }, []);
105
+
106
+ return {
107
+ pendingUpload,
108
+ setPendingUpload,
109
+ submittingForRequest,
110
+ attachInputRef,
111
+ submitInputRef,
112
+ handleAttachFile,
113
+ handleSubmitInputChange,
114
+ triggerUploadForRequest,
115
+ };
116
+ }
@@ -0,0 +1,36 @@
1
+ import { useEffect, useLayoutEffect, useRef, useCallback } from 'react';
2
+ import type { CaseMessage } from '../../lib/types';
3
+
4
+ export function useMessagePolling(
5
+ refetch: () => void,
6
+ messages: CaseMessage[],
7
+ ) {
8
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
9
+ const isNearBottomRef = useRef(true);
10
+ const shouldAutoScrollRef = useRef(true);
11
+
12
+ useEffect(() => {
13
+ const interval = setInterval(() => { refetch(); }, 30_000);
14
+ return () => clearInterval(interval);
15
+ }, [refetch]);
16
+
17
+ const handleScroll = useCallback(() => {
18
+ const el = scrollContainerRef.current;
19
+ if (!el) return;
20
+ isNearBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 100;
21
+ }, []);
22
+
23
+ useLayoutEffect(() => {
24
+ const el = scrollContainerRef.current;
25
+ if (el && (isNearBottomRef.current || shouldAutoScrollRef.current)) {
26
+ el.scrollTop = el.scrollHeight;
27
+ shouldAutoScrollRef.current = false;
28
+ }
29
+ }, [messages]);
30
+
31
+ const scrollToBottom = useCallback(() => {
32
+ shouldAutoScrollRef.current = true;
33
+ }, []);
34
+
35
+ return { scrollContainerRef, handleScroll, scrollToBottom };
36
+ }
@@ -0,0 +1,56 @@
1
+ import { MAX_FILE_SIZE, ALLOWED_MIME_TYPES } from '../../lib/constants';
2
+ import type { CaseStatusVariant } from '../../lib/types';
3
+
4
+ export const getVar = (v: string) => `var(${v})`;
5
+
6
+ export const getCaseStatusVariant = (status: string): CaseStatusVariant => {
7
+ const n = status.toLowerCase().replace(/_/g, '');
8
+ if (n === 'completed') return 'success';
9
+ if (n === 'rejected' || n === 'declined') return 'error';
10
+ if (n === 'processing' || n === 'inprogress' || n === 'expired') return 'warning';
11
+ if (n === 'initiated') return 'info';
12
+ return 'secondary';
13
+ };
14
+
15
+ export const formatCaseStatus = (s: string) =>
16
+ s.split('_').map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
17
+
18
+ export const getDownloadUrl = (fileUrl: { url?: string; download_url?: string } | null | undefined): string | null =>
19
+ fileUrl ? (fileUrl.url || fileUrl.download_url || null) : null;
20
+
21
+ /** Formats a UTC ISO string to local timezone display, e.g. "Mar 24, 2:14 PM" */
22
+ export function formatMessageTime(isoString: string): string {
23
+ const d = new Date(isoString);
24
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
25
+ + ', '
26
+ + d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
27
+ }
28
+
29
+ export function formatFileSize(bytes: number): string {
30
+ if (bytes < 1024) return `${bytes} B`;
31
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
32
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
33
+ }
34
+
35
+ export function validateUploadFile(file: File): string | null {
36
+ if (file.size === 0) return 'File is empty. Please select a valid file.';
37
+ if (file.size > MAX_FILE_SIZE) return 'File size must not exceed 10 MB.';
38
+ if (!file.type || file.type.trim() === '') return 'Could not determine file type. Please try a different file.';
39
+ if (!ALLOWED_MIME_TYPES.includes(file.type))
40
+ return 'File type not supported. Allowed: PDF, JPG, PNG, WEBP, DOC, DOCX, XLS, XLSX, TXT, CSV.';
41
+ return null;
42
+ }
43
+
44
+ export function getUploadErrorMessage(error: unknown): string {
45
+ if (!(error instanceof Error)) return 'Something went wrong. Please try again.';
46
+ const msg = error.message;
47
+ if (msg.includes('[409]') || msg.includes('document_already_exists'))
48
+ return 'This document has already been uploaded.';
49
+ if (msg.includes('[422]'))
50
+ return 'File format not accepted. Please upload a PDF, image, or Office document.';
51
+ if (msg.includes('[400]') || msg.includes('empty_file'))
52
+ return 'File is empty or the request is invalid.';
53
+ if (msg.includes('[500]'))
54
+ return 'Something went wrong on the server. Please try again.';
55
+ return 'Document upload failed. Please try again.';
56
+ }