@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.
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +36 -7
- package/dist/index.mjs +36 -7
- package/dist/privacy-center.d.mts +134 -51
- package/dist/privacy-center.d.ts +134 -51
- package/dist/privacy-center.js +6391 -2741
- package/dist/privacy-center.mjs +6135 -2482
- package/package.json +1 -1
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +5 -5
- package/src/RedactoNoticeConsent/language-codes.ts +37 -1
- package/src/RedactoNoticeConsentInline/RedactoNoticeConsentInline.tsx +0 -3
- package/src/RedactoNoticeConsentInline/api/index.ts +0 -1
- package/src/RedactoPrivacyCenter/PrivacyCenterCaseDetailsPage.tsx +154 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +3 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +81 -50
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +4 -0
- package/src/RedactoPrivacyCenter/api/actions.ts +68 -36
- package/src/RedactoPrivacyCenter/api/client.ts +125 -14
- package/src/RedactoPrivacyCenter/components/CaseDetails/AttachmentCard.tsx +38 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/ChatBubble.tsx +31 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/DocumentRequestCard.tsx +73 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessageComposer.tsx +87 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessageItem.tsx +95 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/MessagesTab.tsx +147 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/RequestDetailsTab.tsx +171 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/StatusEvent.tsx +23 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/useFileUpload.ts +116 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/useMessagePolling.ts +36 -0
- package/src/RedactoPrivacyCenter/components/CaseDetails/utils.ts +56 -0
- package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +1 -0
- package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +6 -13
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +6 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +0 -1
- package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +2 -2
- package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +2 -3
- package/src/RedactoPrivacyCenter/components/Form.tsx +152 -11
- package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +4 -6
- package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +9 -2
- package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +1 -0
- package/src/RedactoPrivacyCenter/components/Pagination.tsx +12 -12
- package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +3 -1
- package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +15 -1
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +8 -8
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +7 -11
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +1 -1
- package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +1 -1
- package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +4 -24
- package/src/RedactoPrivacyCenter/components/constant.ts +3 -7
- package/src/RedactoPrivacyCenter/components/types.ts +12 -4
- package/src/RedactoPrivacyCenter/context/AuthContext.tsx +30 -16
- package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +3 -6
- package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +35 -3
- package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +1 -1
- package/src/RedactoPrivacyCenter/context/types.ts +14 -0
- package/src/RedactoPrivacyCenter/lib/api.ts +101 -3
- package/src/RedactoPrivacyCenter/lib/constants.ts +46 -1
- package/src/RedactoPrivacyCenter/lib/i18n.ts +1 -1
- package/src/RedactoPrivacyCenter/lib/queries.ts +106 -2
- package/src/RedactoPrivacyCenter/lib/types.ts +186 -1
- package/src/RedactoPrivacyCenter/lib/utils.ts +1 -0
- package/src/RedactoPrivacyCenter/locales/as/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/en/translation.json +69 -2
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/or/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/te/translation.json +93 -1
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +93 -1
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +725 -0
- package/src/RedactoPrivacyCenter/styles/pcStyles.ts +0 -6
- package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +5 -3
- package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +9 -12
- package/src/RedactoPrivacyCenter/ui/Footer.tsx +4 -2
- package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +24 -18
- package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +30 -20
- package/src/RedactoPrivacyCenter/ui/PCInput.tsx +17 -13
- package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +3 -2
- package/src/RedactoPrivacyCenter/ui/PCModal.tsx +3 -1
- package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +0 -2
- package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +10 -4
- package/src/RedactoPrivacyCenter/ui/PCTable.tsx +44 -32
- package/src/RedactoPrivacyCenter/ui/PCText.tsx +5 -3
- package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +0 -2
- package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +4 -2
- package/src/privacy-center.ts +1 -0
|
@@ -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
|
+
}
|
|
@@ -18,7 +18,7 @@ import { useAuth } from '../context/AuthContext';
|
|
|
18
18
|
import { useRequestTypeLabels, useGrievanceTabs } from '../lib/constants';
|
|
19
19
|
import type { CaseHistoryProps } from './types';
|
|
20
20
|
import { GRIEVANCE_TAB_IDS } from './constant';
|
|
21
|
-
import
|
|
21
|
+
import { useInternalNavigation } from '../context/NavigationContext';
|
|
22
22
|
import { getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
|
23
23
|
import { useCaseHistoryColumns } from './caseHistoryColumns';
|
|
24
24
|
import { PCPopover, PCCheckbox } from '../ui';
|
|
@@ -27,6 +27,7 @@ import GrievanceTablePagination from './GrievanceTablePagination';
|
|
|
27
27
|
|
|
28
28
|
export default function CaseHistory({ onCreateRequest }: CaseHistoryProps) {
|
|
29
29
|
const { t } = useTranslation();
|
|
30
|
+
const { navigateTo, setSelectedCase } = useInternalNavigation();
|
|
30
31
|
const grievanceTabs = useGrievanceTabs();
|
|
31
32
|
const requestTypeLabels = useRequestTypeLabels();
|
|
32
33
|
const [activeTab, setActiveTab] = useState<string>(GRIEVANCE_TAB_IDS.ALL);
|
|
@@ -34,8 +35,6 @@ export default function CaseHistory({ onCreateRequest }: CaseHistoryProps) {
|
|
|
34
35
|
const [selectedRequestTypes, setSelectedRequestTypes] = useState<string[]>(
|
|
35
36
|
Object.values(RequestTypeEnum)
|
|
36
37
|
);
|
|
37
|
-
const [selectedCase, setSelectedCase] = useState<CaseRequest | null>(null);
|
|
38
|
-
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
39
38
|
const [pageIndex, setPageIndex] = useState(0);
|
|
40
39
|
const [pageSize, setPageSize] = useState(10);
|
|
41
40
|
const { organization } = useOrganization();
|
|
@@ -116,12 +115,10 @@ export default function CaseHistory({ onCreateRequest }: CaseHistoryProps) {
|
|
|
116
115
|
|
|
117
116
|
const handleViewDetails = useCallback((caseRequest: CaseRequest) => {
|
|
118
117
|
setSelectedCase(caseRequest);
|
|
119
|
-
|
|
120
|
-
}, []);
|
|
118
|
+
navigateTo('case-details');
|
|
119
|
+
}, [navigateTo, setSelectedCase]);
|
|
121
120
|
|
|
122
|
-
const columns = useCaseHistoryColumns(
|
|
123
|
-
onViewDetails: handleViewDetails,
|
|
124
|
-
});
|
|
121
|
+
const columns = useCaseHistoryColumns();
|
|
125
122
|
|
|
126
123
|
// Reset to first page when filters change (but not when pagination changes)
|
|
127
124
|
useEffect(() => {
|
|
@@ -265,6 +262,7 @@ export default function CaseHistory({ onCreateRequest }: CaseHistoryProps) {
|
|
|
265
262
|
<PCInput
|
|
266
263
|
type="search"
|
|
267
264
|
clearable
|
|
265
|
+
clearAriaLabel={t('clearInput')}
|
|
268
266
|
placeholder={t('searchCasesByID')}
|
|
269
267
|
value={searchQuery}
|
|
270
268
|
onClear={() => setSearchQuery('')}
|
|
@@ -423,11 +421,6 @@ export default function CaseHistory({ onCreateRequest }: CaseHistoryProps) {
|
|
|
423
421
|
setPageIndex(0);
|
|
424
422
|
}}
|
|
425
423
|
/>
|
|
426
|
-
<CaseDetailsModal
|
|
427
|
-
isOpen={isModalOpen}
|
|
428
|
-
onClose={() => setIsModalOpen(false)}
|
|
429
|
-
selectedCase={selectedCase}
|
|
430
|
-
/>
|
|
431
424
|
</div>
|
|
432
425
|
);
|
|
433
426
|
}
|
|
@@ -592,6 +592,7 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
592
592
|
<PCInput
|
|
593
593
|
type="search"
|
|
594
594
|
clearable
|
|
595
|
+
clearAriaLabel={t('clearInput')}
|
|
595
596
|
placeholder={t('search')}
|
|
596
597
|
value={searchQuery}
|
|
597
598
|
onClear={() => {
|
|
@@ -623,6 +624,7 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
623
624
|
onChange={handleStatusFilterChange}
|
|
624
625
|
onClear={() => setStatusFilter(null)}
|
|
625
626
|
clearable={statusFilter !== null}
|
|
627
|
+
clearAriaLabel={t('clearSelection')}
|
|
626
628
|
dropdownMatchTriggerWidth
|
|
627
629
|
style={{
|
|
628
630
|
width: '100%',
|
|
@@ -632,11 +634,14 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
632
634
|
className="pc-filter-select"
|
|
633
635
|
placeholder={t('product')}
|
|
634
636
|
searchable
|
|
637
|
+
searchPlaceholder={t('searchPlaceholder')}
|
|
638
|
+
noOptionsMessage={t('noOptionsFound')}
|
|
635
639
|
options={productOptions}
|
|
636
640
|
value={productFilter}
|
|
637
641
|
onChange={handleProductFilterChange}
|
|
638
642
|
onClear={() => setProductFilter(null)}
|
|
639
643
|
clearable={productFilter !== null}
|
|
644
|
+
clearAriaLabel={t('clearSelection')}
|
|
640
645
|
dropdownMatchTriggerWidth
|
|
641
646
|
style={{
|
|
642
647
|
width: '100%',
|
|
@@ -665,6 +670,7 @@ export default function ConsentManager({ variant }: ConsentManagerProps) {
|
|
|
665
670
|
isOpen={exportModal}
|
|
666
671
|
onClose={() => setExportModal(false)}
|
|
667
672
|
size="sm"
|
|
673
|
+
closeAriaLabel={t('closeModal')}
|
|
668
674
|
title={
|
|
669
675
|
<div style={{ minWidth: 0, flex: 1, paddingRight: getVar('--pc-spacing-sm') }}>
|
|
670
676
|
<PCText size="base" weight="semibold" style={{ margin: 0, color: getVar('--pc-text-primary') }}>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import { PCText, PCTooltip, PCBadge } from '../ui';
|
|
4
|
-
import { cn } from '../utils/cn';
|
|
5
4
|
import { pcStyles } from '../styles/pcStyles';
|
|
6
5
|
import { Smartphone, LaptopMinimal } from 'lucide-react';
|
|
7
6
|
import { createColumnHelper } from '@tanstack/react-table';
|