Skip to content

Commit 676bdd8

Browse files
author
Brijesh
committed
Exemption implementation
1 parent 8f7b28c commit 676bdd8

17 files changed

Lines changed: 954 additions & 123 deletions

ExemptionsDropdown.js

Whitespace-only changes.

src/api/upload.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ export const getSignedUploadUrl = async (fileName) => {
66
return res.data.url;
77
};
88

9-
export const getSignedDownloadUrl = async (fileId) => {
10-
const response = await axios.get(API.GET_SIGNED_DOWNLOAD_URL(fileId), getAuthHeaderConfig());
9+
export const getSignedDownloadUrl = async (fileId, fileType = 'plan') => {
10+
const response = await axios.get(API.GET_SIGNED_DOWNLOAD_URL(fileId, fileType), getAuthHeaderConfig());
1111

1212
return response.data;
1313
};

src/components/rangeUsePlanPage/attachments/AttachmentRow.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ export const attachmentAccess = [
2626
},
2727
];
2828

29-
export const downloadAttachment = async (attachmentId, attachmentName) => {
30-
const res = await axios.get(GET_SIGNED_DOWNLOAD_URL(attachmentId), getAuthHeaderConfig());
29+
export const downloadAttachment = async (attachmentId, attachmentName, fileType) => {
30+
const res = await axios.get(GET_SIGNED_DOWNLOAD_URL(attachmentId, fileType), getAuthHeaderConfig());
3131
const fileRes = await axios.get(res.data.url, {
3232
responseType: 'blob',
3333
skipAuthorizationHeader: true,
@@ -41,15 +41,15 @@ export const downloadAttachment = async (attachmentId, attachmentName) => {
4141
link.parentNode.removeChild(link);
4242
};
4343

44-
const AttachmentRow = ({ attachment, index, onDelete, error }) => {
44+
const AttachmentRow = ({ attachment, index, onDelete, error, fileType }) => {
4545
const [isDownloading, setDownloading] = useState(false);
4646
const [errorDownloading, setErrorDownloading] = useState();
4747

4848
const handleDownload = async () => {
4949
setErrorDownloading(null);
5050
setDownloading(true);
5151
try {
52-
downloadAttachment(attachment.id, attachment.name);
52+
downloadAttachment(attachment.id, attachment.name, fileType);
5353
} catch (e) {
5454
setErrorDownloading(e);
5555
}

src/components/rangeUsePlanPage/attachments/index.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,22 @@ import { PrimaryButton } from '../../common';
99
import { IfEditable } from '../../common/PermissionsField';
1010
import AttachmentRow from './AttachmentRow';
1111
import * as API from '../../../constants/api';
12+
import { ATTACHMENT_TYPE } from '../../../constants/variables';
1213

1314
const sortByDate = (a, b) => {
1415
if (b.uploadDate > a.uploadDate) return -1;
1516
if (b.uploadDate < a.uploadDate) return 1;
1617
return 0;
1718
};
1819

19-
const Attachments = ({ planId, attachments = [], label = '', propertyName, fetchPlan }) => {
20+
const Attachments = ({
21+
planId,
22+
attachments = [],
23+
label = '',
24+
propertyName,
25+
fetchPlan,
26+
fileType = ATTACHMENT_TYPE.PLAN_ATTACHMENT,
27+
}) => {
2028
const [toRemove, setToRemove] = useState(null);
2129
const formik = useFormikContext();
2230

@@ -98,6 +106,7 @@ const Attachments = ({ planId, attachments = [], label = '', propertyName, fetch
98106
key={index}
99107
attachment={attachment}
100108
onDelete={() => setToRemove(attachment)}
109+
fileType={fileType}
101110
index={index}
102111
error={formik.errors?.files?.[index]?.url}
103112
/>
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React, { useState } from 'react';
2+
import useSWR from 'swr';
3+
import * as API from '../../../constants/api';
4+
import { axios, getAuthHeaderConfig } from '../../../utils';
5+
import ExemptionDropdownList from './ExemptionDropdownList';
6+
import { TableCell, TableRow } from 'semantic-ui-react';
7+
8+
const ExemptionDropdown = ({ agreementId, open, onEditExemption }) => {
9+
const [selectedExemption, setSelectedExemption] = useState(null);
10+
const endpoint = API.GET_AGREEMENT_EXEMPTIONS(agreementId);
11+
12+
const { data, error, isValidating, mutate } = useSWR(
13+
() => (open ? endpoint : null),
14+
(key) =>
15+
axios
16+
.get(key, {
17+
...getAuthHeaderConfig(),
18+
})
19+
.then((res) => res.data),
20+
);
21+
22+
const exemptions = data;
23+
24+
const handleExemptionUpdate = () => {
25+
mutate();
26+
};
27+
28+
if (error) return <div>Error: {JSON.stringify(error.message)}</div>;
29+
if (isValidating) {
30+
return (
31+
<TableRow>
32+
<TableCell colSpan={13} style={{ paddingBottom: 0, paddingTop: 0, borderBottom: 'none' }}>
33+
<p>Loading...</p>
34+
</TableCell>
35+
</TableRow>
36+
);
37+
}
38+
39+
if (!exemptions || exemptions.length === 0) {
40+
return null;
41+
}
42+
return (
43+
<ExemptionDropdownList
44+
exemptions={exemptions}
45+
selectedExemption={selectedExemption}
46+
open={open}
47+
onSelectExemption={(e, { value }) => setSelectedExemption(value)}
48+
onExemptionUpdate={handleExemptionUpdate}
49+
onEditExemption={onEditExemption}
50+
/>
51+
);
52+
};
53+
54+
export default ExemptionDropdown;
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import React, { useState } from 'react';
2+
import Box from '@material-ui/core/Box';
3+
import Collapse from '@material-ui/core/Collapse';
4+
import CircularProgress from '@material-ui/core/CircularProgress';
5+
import Table from '@material-ui/core/Table';
6+
import TableBody from '@material-ui/core/TableBody';
7+
import TableCell from '@material-ui/core/TableCell';
8+
import TableHead from '@material-ui/core/TableHead';
9+
import TableRow from '@material-ui/core/TableRow';
10+
import Typography from '@material-ui/core/Typography';
11+
import moment from 'moment';
12+
import { PrimaryButton } from '../../common';
13+
import { axios, getAuthHeaderConfig } from '../../../utils';
14+
import * as API from '../../../constants/api';
15+
import { useUser } from '../../../providers/UserProvider';
16+
import { isUserDecisionMaker, isUserAdmin } from '../../../utils/helper/user';
17+
import useConfirm from '../../../providers/ConfrimationModalProvider';
18+
import { downloadAttachment } from '../attachments/AttachmentRow';
19+
import { ATTACHMENT_TYPE, EXEMPTION_STATUS } from '../../../constants/variables';
20+
21+
const ExemptionDropdownList = ({ exemptions = [], open, onExemptionUpdate, onEditExemption }) => {
22+
const user = useUser();
23+
const confirm = useConfirm();
24+
const isAdminOrDecisionMaker = isUserDecisionMaker(user) || isUserAdmin(user);
25+
const [submittingId, setSubmittingId] = useState(null);
26+
const [error, setError] = useState(null);
27+
28+
const onDownloadClicked = async (exemption) => {
29+
await axios
30+
.get(API.DOWNLOAD_AGREEMENT_EXEMPTION(exemption.agreementId, exemption.id), {
31+
...getAuthHeaderConfig(),
32+
})
33+
.then((response) => {
34+
const blob = new Blob([Buffer.from(response.data, 'base64')], {
35+
type: 'application/pdf',
36+
});
37+
const url = window.URL.createObjectURL(blob);
38+
const link = document.createElement('a');
39+
link.href = url;
40+
link.download = `${exemption.agreementId}_Exemption${exemption.id}.pdf`;
41+
link.style.display = 'none';
42+
link.click();
43+
window.URL.revokeObjectURL(url);
44+
link.remove();
45+
46+
exemption.attachments.forEach((attachment) => {
47+
downloadAttachment(attachment.id, attachment.name, ATTACHMENT_TYPE.EXEMPTION_ATTACHMENT);
48+
});
49+
});
50+
};
51+
52+
const onTransitionClicked = async (exemption, action) => {
53+
const actionTexts = {
54+
approve: 'Approve',
55+
reject: 'Reject',
56+
cancel: 'Cancel',
57+
};
58+
59+
const choice = await confirm({
60+
titleText: `${actionTexts[action]} Exemption`,
61+
contentText: `Are you sure you want to ${action.toLowerCase()} this exemption?`,
62+
confirmText: actionTexts[action],
63+
cancelText: 'Back',
64+
});
65+
66+
if (!choice) return;
67+
68+
setSubmittingId(exemption.id);
69+
setError(null);
70+
try {
71+
await axios.post(
72+
API.TRANSITION_EXEMPTION(exemption.agreementId, exemption.id),
73+
{ action, comment: '' },
74+
{ ...getAuthHeaderConfig() },
75+
);
76+
if (onExemptionUpdate) {
77+
onExemptionUpdate();
78+
}
79+
} catch (err) {
80+
setError(err.response?.data?.message || 'Failed to perform action');
81+
console.error('Error transitioning exemption:', err);
82+
} finally {
83+
setSubmittingId(null);
84+
}
85+
};
86+
87+
return (
88+
<TableRow>
89+
<TableCell
90+
colSpan={isAdminOrDecisionMaker ? 14 : 12}
91+
style={{ paddingBottom: 0, paddingTop: 0, borderBottom: 'none' }}
92+
>
93+
<Table>
94+
<TableRow>
95+
<TableCell style={{ paddingBottom: 0, paddingTop: 0, borderBottom: 'none' }}>
96+
<Collapse in={open} timeout="auto" unmountOnExit>
97+
<Box margin={0} style={{ marginBottom: '10px' }}>
98+
<Typography variant="h6" gutterBottom component="div">
99+
Exemption
100+
</Typography>
101+
{error && (
102+
<Typography style={{ color: 'red', marginBottom: '10px' }} variant="body2">
103+
{error}
104+
</Typography>
105+
)}
106+
<Table size="small">
107+
<TableHead>
108+
<TableRow>
109+
<TableCell style={{ color: 'grey', width: 175 }}>Reason</TableCell>
110+
<TableCell style={{ color: 'grey', width: 175 }}>Submited By</TableCell>
111+
<TableCell style={{ color: 'grey', width: 175 }}>Submission Date</TableCell>
112+
<TableCell style={{ color: 'grey', width: 175 }}>Start Date</TableCell>
113+
<TableCell style={{ color: 'grey', width: 175 }}>End Date</TableCell>
114+
<TableCell style={{ color: 'grey', width: 175 }}>Approved By</TableCell>
115+
<TableCell style={{ color: 'grey', width: 175 }}>Approval Date</TableCell>
116+
<TableCell style={{ color: 'grey', width: 175, align: 'left' }}>Status</TableCell>
117+
<TableCell style={{ color: 'grey', width: 175 }}>Download</TableCell>
118+
{isAdminOrDecisionMaker && (
119+
<>
120+
<TableCell style={{ color: 'grey', width: 175 }}>Approve/Reject</TableCell>
121+
<TableCell style={{ color: 'grey', width: 175 }}>Cancel</TableCell>
122+
<TableCell style={{ color: 'grey', width: 175 }}>View/Edit</TableCell>
123+
</>
124+
)}
125+
</TableRow>
126+
</TableHead>
127+
<TableBody>
128+
{exemptions.map((exemption, index) => {
129+
return (
130+
<React.Fragment key={index}>
131+
<TableRow key={index} hover={true}>
132+
<TableCell>{exemption.reason}</TableCell>
133+
<TableCell>
134+
{exemption?.user.givenName} {exemption?.user.familyName}
135+
</TableCell>
136+
<TableCell>
137+
{exemption.createdAt ? moment(exemption.createdAt).format('MMM DD YYYY') : ''}
138+
</TableCell>
139+
<TableCell>
140+
{exemption.startDate ? moment(exemption.startDate).format('MMM DD YYYY') : ''}
141+
</TableCell>
142+
<TableCell>
143+
{exemption.endDate ? moment(exemption.endDate).format('MMM DD YYYY') : ''}
144+
</TableCell>
145+
<TableCell>
146+
{exemption.approvedByUser
147+
? `${exemption.approvedByUser.givenName} ${exemption.approvedByUser.familyName}`
148+
: exemption.approvedBy}
149+
</TableCell>
150+
<TableCell>
151+
{exemption.approvalDate
152+
? moment(exemption.approvalDate).format('MMM DD YYYY')
153+
: exemption.approvedAt
154+
? moment(exemption.approvedAt).format('MMM DD YYYY')
155+
: ''}
156+
</TableCell>
157+
<TableCell>{exemption.status}</TableCell>
158+
<TableCell>
159+
<PrimaryButton
160+
icon
161+
inverted
162+
onClick={() => {
163+
onDownloadClicked(exemption);
164+
}}
165+
>
166+
<i className="download icon" />
167+
</PrimaryButton>
168+
</TableCell>
169+
{isAdminOrDecisionMaker && (
170+
<>
171+
<TableCell>
172+
{exemption.status === EXEMPTION_STATUS.PENDING_APPROVAL &&
173+
(submittingId === exemption.id ? (
174+
<CircularProgress size={24} />
175+
) : (
176+
<>
177+
<PrimaryButton
178+
icon
179+
inverted
180+
onClick={() => onTransitionClicked(exemption, 'approve')}
181+
>
182+
<i className="thumbs up icon" />
183+
</PrimaryButton>
184+
<PrimaryButton
185+
icon
186+
inverted
187+
onClick={() => onTransitionClicked(exemption, 'reject')}
188+
>
189+
<i className="thumbs down icon" />
190+
</PrimaryButton>
191+
</>
192+
))}
193+
</TableCell>
194+
<TableCell>
195+
{exemption.status !== EXEMPTION_STATUS.CANCELLED &&
196+
(submittingId === exemption.id ? (
197+
<CircularProgress size={24} />
198+
) : (
199+
<PrimaryButton
200+
icon
201+
inverted
202+
onClick={() => onTransitionClicked(exemption, 'cancel')}
203+
>
204+
<i className="x icon" />
205+
</PrimaryButton>
206+
))}
207+
</TableCell>
208+
<TableCell>
209+
{exemption.status === EXEMPTION_STATUS.REJECTED ||
210+
(isAdminOrDecisionMaker &&
211+
exemption.status === EXEMPTION_STATUS.PENDING_APPROVAL) ? (
212+
submittingId === exemption.id ? (
213+
<CircularProgress size={24} />
214+
) : (
215+
<PrimaryButton
216+
icon
217+
inverted
218+
onClick={() => onEditExemption && onEditExemption(exemption)}
219+
>
220+
<i className="edit icon" />
221+
</PrimaryButton>
222+
)
223+
) : (
224+
<PrimaryButton
225+
icon
226+
inverted
227+
onClick={() =>
228+
onEditExemption && onEditExemption({ ...exemption, viewOnly: true })
229+
}
230+
>
231+
<i className="eye icon" />
232+
</PrimaryButton>
233+
)}
234+
</TableCell>
235+
</>
236+
)}
237+
</TableRow>
238+
</React.Fragment>
239+
);
240+
})}
241+
</TableBody>
242+
</Table>
243+
</Box>
244+
</Collapse>
245+
</TableCell>
246+
</TableRow>
247+
</Table>
248+
</TableCell>
249+
</TableRow>
250+
);
251+
};
252+
253+
export default ExemptionDropdownList;

src/components/rangeUsePlanPage/pdf/PDFView.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
22
import { generatePDF } from '../../../api';
33
import { PrimaryButton } from '../../common';
44
import { downloadAttachment } from '../attachments/AttachmentRow';
5+
import { ATTACHMENT_TYPE } from '../../../constants/variables';
56

67
const PDFView = ({ match, agreementId, mapAttachments }) => {
78
const { planId } = match.params;
@@ -26,7 +27,7 @@ const PDFView = ({ match, agreementId, mapAttachments }) => {
2627
link.parentNode.removeChild(link);
2728
setLoading(false);
2829
mapAttachments.forEach((attachment) => {
29-
downloadAttachment(attachment.id, attachment.name);
30+
downloadAttachment(attachment.id, attachment.name, ATTACHMENT_TYPE.PLAN_ATTACHMENT);
3031
});
3132
} catch (e) {
3233
setLoading(false);

0 commit comments

Comments
 (0)