Skip to content

Commit ed326f0

Browse files
authored
Merge pull request #6067 from bcgov/release/FOIMOD-release22.4-to-main
Release 22.4 (Ticket 4159 Linked Requests): DEV <> MAIN
2 parents 9645ab2 + ae3fda9 commit ed326f0

File tree

17 files changed

+710
-25
lines changed

17 files changed

+710
-25
lines changed

forms-flow-web/src/apiManager/endpoints/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const API = {
1111
FOI_GET_MINISTRY_REQUESTS_API: `${FOI_BASE_API_URL}/api/dashboard/ministry`,
1212
FOI_GET_REQUESTS_PAGE_API: `${FOI_BASE_API_URL}/api/dashboardpagination`,
1313
FOI_GET_MINISTRY_REQUESTS_PAGE_API: `${FOI_BASE_API_URL}/api/dashboardpagination/ministry`,
14-
// FOI_GET_OI_REQUESTS_PAGE_API: `${FOI_BASE_API_URL}/api/dashboardpagination/oi`,
1514
FOI_GET_CATEGORIES_API: `${FOI_BASE_API_URL}/api/foiflow/applicantcategories`,
1615
FOI_GET_PROGRAMAREAS_API: `${FOI_BASE_API_URL}/api/foiflow/programareas`,
1716
FOI_GET_PROGRAMAREAS_FORUSER_API: `${FOI_BASE_API_URL}/api/foiflow/programareasforuser`,
@@ -30,6 +29,7 @@ const API = {
3029
FOI_POST_REQUEST_POST: `${FOI_BASE_API_URL}/api/foirequests`,
3130
FOI_REQUEST_API: `${FOI_BASE_API_URL}/api/foirequests/<requestid>/ministryrequest/<ministryid>`,
3231
FOI_REQUEST_SECTION_API: `${FOI_BASE_API_URL}/api/foirequests/<requestid>/ministryrequest/<ministryid>/section`,
32+
FOI_MINISTRY_REQUEST_LINKEDREQUESTINFO: `${FOI_BASE_API_URL}/api/linkrequest/foiministryinfo/axisrequestid/<axisrequestid>`,
3333
FOI_MINISTRYVIEW_REQUEST_API: `${FOI_BASE_API_URL}/api/foirequests/<requestid>/ministryrequest/<ministryid>/ministry`,
3434
FOI_RAW_REQUEST_DESCRIPTION: `${FOI_BASE_API_URL}/api/foiaudit/rawrequest/<requestid>/description`,
3535
FOI_MINISTRY_REQUEST_DESCRIPTION: `${FOI_BASE_API_URL}/api/foiaudit/ministryrequest/<ministryid>/description`,
@@ -226,5 +226,6 @@ const API = {
226226
FOI_GET_RECORD_GROUP: `${FOI_BASE_API_URL}/api/foirecord/<requestid>/ministryrequest/<ministryrequestid>/groups`,
227227
FOI_DELETE_RECORD_GROUP: `${FOI_BASE_API_URL}/api/foirecord/<requestid>/ministryrequest/<ministryrequestid>/groups/<groupid>/records/<recordid>`,
228228

229+
FOI_GET_LINKED_REQUESTS_LIST: `${FOI_BASE_API_URL}/api/linkrequests/<ministrycode>/axisrequestid/<axisrequestid>`
229230
};
230231
export default API;

forms-flow-web/src/apiManager/services/FOI/foiRequestServices.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,3 +645,45 @@ export const updateSpecificRequestSection = (data, field, requestId, ministryId,
645645
});
646646
};
647647
}
648+
649+
export const linkedRequestsLists = (queryParams,axisrequestid, ministrycode, ...rest) => {
650+
const done = fnDone(rest);
651+
const serializedQueryParams = new URLSearchParams(queryParams).toString();
652+
const fixedQueryParams = serializedQueryParams.replace(/\+/g, '%20');
653+
let url= replaceUrl(replaceUrl(
654+
API.FOI_GET_LINKED_REQUESTS_LIST+`?${fixedQueryParams}`,
655+
"<ministrycode>",
656+
ministrycode),"<axisrequestid>",
657+
axisrequestid
658+
);
659+
return (dispatch) => {
660+
httpGETRequest(url, {}, UserService.getToken())
661+
.then((res) => {
662+
if (res.data) {
663+
done(null, res.data);
664+
} else {
665+
dispatch(serviceActionError(res));
666+
throw new Error(`Error in fetching axis request ids.`);
667+
}
668+
})
669+
.catch((error) => {
670+
catchError(error, dispatch);
671+
});
672+
}
673+
};
674+
675+
export const getFOIMinistryLinkedRequestInfo = (axisid) => async (dispatch) => {
676+
const apiUrl= replaceUrl(API.FOI_MINISTRY_REQUEST_LINKEDREQUESTINFO, "<axisrequestid>", axisid);
677+
try {
678+
const res = await httpGETRequest(apiUrl, {}, UserService.getToken());
679+
if (res.data) {
680+
return res.data;
681+
} else {
682+
console.error("API returned incomplete requeststatus data:", res);
683+
dispatch(serviceActionError(res));
684+
}
685+
} catch (error) {
686+
console.error("Error in fetching requeststatus data:", error);
687+
dispatch(serviceActionError(error));
688+
}
689+
};

forms-flow-web/src/components/FOI/FOIRequest/FOIRequest.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ import MANDATORY_FOI_REQUEST_FIELDS from "../../../constants/FOI/mandatoryFOIReq
132132
import RequestHistorySection from "../customComponents/RequestHistory";
133133
import { Fees } from "../customComponents/Fees";
134134
import OpenInfo from "./OpenInformation/OpenInfo";
135+
import LinkedRequests from "./LinkedRequests";
135136

136137
const useStyles = makeStyles((theme) => ({
137138
root: {
@@ -1598,6 +1599,20 @@ const FOIRequest = React.memo(({ userDetail, openApplicantProfileModal }) => {
15981599
createSaveRequestObject={createSaveRequestObject}
15991600
disableInput={disableInput || isHistoricalRequest}
16001601
/>
1602+
{requestDetails?.axisRequestId &&
1603+
<LinkedRequests
1604+
requestDetails={requestDetails}
1605+
requestStatus={_requestStatus}
1606+
handleRequestDetailsValue={handleRequestDetailsValue}
1607+
handleRequestDetailsInitialValue={
1608+
handleRequestDetailsInitialValue
1609+
}
1610+
createSaveRequestObject={createSaveRequestObject}
1611+
disableInput={disableInput || isHistoricalRequest}
1612+
isHistoricalRequest={isHistoricalRequest}
1613+
isMinistry={isMinistry}
1614+
/>
1615+
}
16011616
<RequestDetails
16021617
requestDetails={requestDetails}
16031618
requestStatus={_requestStatus}
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import React, { useEffect, useContext, useState } from "react";
2+
import { useSelector,useDispatch } from "react-redux";
3+
import FOI_COMPONENT_CONSTANTS from "../../../constants/FOI/foiComponentConstants";
4+
import { makeStyles } from "@material-ui/styles";
5+
import Accordion from "@material-ui/core/Accordion";
6+
import AccordionSummary from "@material-ui/core/AccordionSummary";
7+
import AccordionDetails from "@material-ui/core/AccordionDetails";
8+
import Typography from "@material-ui/core/Typography";
9+
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
10+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
11+
import { faCirclePlus } from '@fortawesome/free-solid-svg-icons';
12+
import SearchIcon from "@material-ui/icons/Search";
13+
import CloseIcon from '@mui/icons-material/Close';
14+
import {
15+
Grid,
16+
TextField,
17+
InputAdornment,
18+
IconButton,
19+
Autocomplete,
20+
CircularProgress
21+
} from "@mui/material";
22+
import {getFOIMinistryLinkedRequestInfo, linkedRequestsLists} from "../../../apiManager/services/FOI/foiRequestServices";
23+
import { LinkedRequestsTable } from "./LinkedRequestsTable";
24+
25+
const LinkedRequests = React.memo(
26+
({
27+
requestDetails,
28+
createSaveRequestObject,
29+
isMinistry
30+
}) => {
31+
const useStyles = makeStyles({
32+
heading: {
33+
color: "#FFF",
34+
fontSize: "16px !important",
35+
fontWeight: "bold !important",
36+
},
37+
accordionSummary: {
38+
flexDirection: "row-reverse",
39+
},
40+
linkedRequests:{
41+
float: "left",
42+
}
43+
});
44+
const classes = useStyles();
45+
const [linkedRequests, setLinkedRequests] = useState(requestDetails?.linkedRequests)
46+
const [linkedRequestsInfo, setLinkedRequestsInfo] = useState(requestDetails?.linkedRequestsInfo)
47+
const [loading, setLoading] = useState(false);
48+
49+
const dispatch = useDispatch();
50+
51+
const [showSearch, setShowSearch] = useState(false);
52+
const [searchQuery, setSearchQuery] = useState("");
53+
const [options, setOptions] = useState([]);
54+
55+
const getAxisRequestId = (item) => {
56+
const axisRequestId = Object.keys(item)[0];
57+
if (typeof axisRequestId === "string") return axisRequestId;
58+
return null;
59+
};
60+
61+
const handleSearch = (value) => {
62+
if (!value || value?.trim() === "") {
63+
setOptions([]);
64+
setSearchQuery("");
65+
return;
66+
}
67+
setSearchQuery(value);
68+
fetchSuggestions(value);
69+
};
70+
71+
const handleClearSearch = () => {
72+
setShowSearch(false);
73+
setSearchQuery("");
74+
setOptions([]);
75+
}
76+
77+
const removeLinkedRequest = (reqItem) => {
78+
const reqId = reqItem.axisrequestid;
79+
const updatedLinkedRequests = linkedRequests?.filter(item => getAxisRequestId(item) !== reqId);
80+
const updatedLinkedInfoRequests = linkedRequestsInfo?.filter(item => item.axisrequestid !== reqId);
81+
setLinkedRequests(updatedLinkedRequests);
82+
setLinkedRequestsInfo(updatedLinkedInfoRequests);
83+
createSaveRequestObject(FOI_COMPONENT_CONSTANTS.LINKED_REQUESTS, updatedLinkedRequests);
84+
}
85+
86+
const renderReviewRequest = (e, reqItem) => {
87+
e.preventDefault();
88+
const reqId = reqItem.axisrequestid;
89+
const item = linkedRequestsInfo.find(
90+
obj => obj.axisrequestid === reqId
91+
);
92+
const rawrequestId = item?.rawrequestid;
93+
const ministryId = item?.foiministryrequestid;
94+
let url = '';
95+
if (ministryId) {
96+
url = `/foi/foirequests/${ministryId}/ministryrequest/${ministryId}`
97+
} else {
98+
url = `/foi/reviewrequest/${rawrequestId}`;
99+
}
100+
window.open(url, "_blank", "noopener,noreferrer");
101+
};
102+
103+
const fetchSuggestions = (value) => {
104+
const ministryCode = requestDetails?.selectedMinistries?.length > 0
105+
? requestDetails.selectedMinistries[0].code
106+
: "";
107+
const queryParams = { q: value };
108+
try {
109+
dispatch(
110+
linkedRequestsLists(
111+
queryParams,
112+
requestDetails?.axisRequestId,
113+
ministryCode,
114+
(err, _res) => {
115+
if (err) {
116+
} else {
117+
setOptions(_res);
118+
console.log(_res);
119+
}
120+
}
121+
)
122+
);
123+
} catch (error) {
124+
console.log(error);
125+
}
126+
};
127+
128+
const linkRequest = async (selectedValue) => {
129+
// linkedrequestinfo = {"axisrequestid": str, requeststatus: str, bcgovcode: str}
130+
// linkedrequests = {"axisrequestid": bcgovcode}
131+
if (!selectedValue) {
132+
return;
133+
}
134+
// Create a new array to avoid mutation issues
135+
const updatedLinkedRequests = [...(linkedRequests || [])];
136+
137+
// Get the axis request ID from the selected value
138+
const newRequestId = selectedValue.axisrequestid;
139+
140+
// Check if this request ID already exists in the array
141+
const alreadyExists = updatedLinkedRequests.some(
142+
item => getAxisRequestId(item) === newRequestId
143+
);
144+
145+
// Get FOIMinistryRequest Status and MinistryId if FOIRawRequest Status is Archived
146+
if (selectedValue.requeststatus === "Archived" && !alreadyExists) {
147+
const res = await dispatch(getFOIMinistryLinkedRequestInfo(selectedValue.axisrequestid));
148+
selectedValue.requeststatus = res.requeststatus;
149+
selectedValue.foiministryrequestid = res.foiministryrequestid;
150+
}
151+
152+
// Add the new linkedrequest object to linkedrequest and linkedrequstinfo
153+
if (!alreadyExists && newRequestId) {
154+
const axisRequestId = selectedValue.axisrequestid;
155+
const govCode = selectedValue.govcode;
156+
const linkedReqObj = {
157+
[selectedValue.axisrequestid]: govCode,
158+
};
159+
const linkedReqInfoObj = {
160+
"axisrequestid": axisRequestId,
161+
"requeststatus": selectedValue.requeststatus,
162+
"foiministryrequestid": selectedValue.foiministryrequestid || null,
163+
"rawrequestid": selectedValue.rawrequestid,
164+
"govcode": govCode
165+
};
166+
updatedLinkedRequests.push(linkedReqObj);
167+
setLinkedRequests(updatedLinkedRequests);
168+
setLinkedRequestsInfo(prev => ([...prev, linkedReqInfoObj]));
169+
createSaveRequestObject(FOI_COMPONENT_CONSTANTS.LINKED_REQUESTS, updatedLinkedRequests);
170+
}
171+
172+
// Clear the search after selection
173+
handleClearSearch();
174+
}
175+
176+
return (
177+
<div className="request-accordian">
178+
<Accordion defaultExpanded={true}>
179+
<AccordionSummary
180+
className={classes.accordionSummary}
181+
expandIcon={<ExpandMoreIcon />}
182+
id="requestDetails-header"
183+
>
184+
<Typography className={classes.heading}>LINKED REQUESTS</Typography>
185+
</AccordionSummary>
186+
<AccordionDetails>
187+
<div className="linked-requests">
188+
<LinkedRequestsTable
189+
linkedRequestsInfo={linkedRequestsInfo}
190+
linkedRequests={linkedRequests}
191+
renderReviewRequest={renderReviewRequest}
192+
removeLinkedRequest={removeLinkedRequest}
193+
isMinistry={isMinistry}
194+
/>
195+
{!showSearch && (
196+
<div style={{display: "flex", flexDirection: "row", alignItems: "center", margin: "7px 0px 7px 0px"}}>
197+
<button onClick={() => setShowSearch(true)} style={{ border: "none", background: "none" }}>
198+
<FontAwesomeIcon icon={faCirclePlus} size="lg" color="#38598A" />
199+
</button>
200+
<p onClick={() => setShowSearch(true)} style={{fontWeight: "bold", color: "#38598A", cursor: "pointer"}}>Add Linked Request</p>
201+
</div>
202+
)}
203+
{showSearch && (
204+
<Grid
205+
item
206+
xs={12}
207+
sx={{
208+
p: 1,
209+
width: "35%"
210+
}}
211+
>
212+
<Autocomplete
213+
freeSolo
214+
disableClearable
215+
loading={loading}
216+
options={options || []}
217+
getOptionLabel={(option) => {
218+
const axisrequestid = option.axisrequestid
219+
if (!option) return "";
220+
return axisrequestid;
221+
}}
222+
inputValue={searchQuery}
223+
onInputChange={(e, newValue) => {
224+
if (e?.type === "change") {
225+
handleSearch(newValue);
226+
}
227+
}}
228+
onChange={(e, selectedValue) => {
229+
if (selectedValue) {
230+
linkRequest(selectedValue);
231+
}
232+
}}
233+
sx={{
234+
"& .MuiOutlinedInput-root": {
235+
borderRadius: "6px",
236+
height: 40,
237+
border: "1px solid #ccc",
238+
"& fieldset": { border: "none" },
239+
"&:hover fieldset": { border: "none" },
240+
"&.Mui-focused fieldset": { border: "1px solid #38598A" },
241+
},
242+
"& .MuiInputBase-input": {
243+
padding: "6px 8px",
244+
fontSize: "0.9rem",
245+
},
246+
}}
247+
renderInput={(params) => (
248+
<TextField
249+
{...params}
250+
placeholder="Search RequestID"
251+
InputProps={{
252+
...params.InputProps,
253+
startAdornment: (
254+
<InputAdornment position="start">
255+
<IconButton sx={{ color: "#003388" }}>
256+
<SearchIcon sx={{ color: "#038", "& path": { fill: "#038" } }} />
257+
</IconButton>
258+
</InputAdornment>
259+
),
260+
endAdornment: (
261+
<InputAdornment position="end">
262+
{loading ? (
263+
<CircularProgress size={18} />
264+
) :
265+
<IconButton
266+
size="small"
267+
onClick={handleClearSearch}
268+
aria-label="Clear search"
269+
sx={{ p: 0.5, color: "#038" }}
270+
>
271+
<CloseIcon fontSize="small" />
272+
</IconButton>
273+
}
274+
</InputAdornment>
275+
),
276+
}}
277+
/>
278+
)}
279+
/>
280+
</Grid>
281+
)}
282+
</div>
283+
<div className="row foi-details-row foi-details-row-break"></div>
284+
</AccordionDetails>
285+
</Accordion>
286+
</div>
287+
);
288+
}
289+
);
290+
291+
export default LinkedRequests;

0 commit comments

Comments
 (0)