diff --git a/.env.local b/.env.local index b4bd9db4..7d13acc3 100644 --- a/.env.local +++ b/.env.local @@ -49,15 +49,15 @@ CRMURL = "tscrm2016.llcdt.gov.wales/UATPINS" # CRMURL = "crm2016.llc.gov.wales/PEDW" # //New Prod -# CLIENT_ID = 3e4141a3-1fbd-454e-98f0-7f3a4f14a3cc -# CLIENT_SECRET = tFk8Q~E.R-HFjROTu1sRKqwb_ONzy05uPYoF6awR -# RELAY_ROOT = https://ar-ns-lz-pedw-prod-uks-01.servicebus.windows.net/ar-hc-lz-pedw-prod-uks-01/ -# RELAYURI = "ar-ns-lz-pedw-prod-uks-01.servicebus.windows.net" -# RELAYPATH = "ar-hc-lz-pedw-prod-uks-01" -# # RELAY_ROOT = https://ar-ns-lz-pedw-ppe-ukw-01.servicebus.windows.net/ar-hc-lz-pedw-prod-ukw-01/ -# # RELAYURI = "ar-ns-lz-pedw-prod-ukw-01.servicebus.windows.net" -# # RELAYPATH = "ar-hc-lz-pedw-prod-ukw-01" -# CRMURL = "crm2016.llcdt.gov.wales/PEDW" +CLIENT_ID = 3e4141a3-1fbd-454e-98f0-7f3a4f14a3cc +CLIENT_SECRET = tFk8Q~E.R-HFjROTu1sRKqwb_ONzy05uPYoF6awR +RELAY_ROOT = https://ar-ns-lz-pedw-prod-uks-01.servicebus.windows.net/ar-hc-lz-pedw-prod-uks-01/ +RELAYURI = "ar-ns-lz-pedw-prod-uks-01.servicebus.windows.net" +RELAYPATH = "ar-hc-lz-pedw-prod-uks-01" +# RELAY_ROOT = https://ar-ns-lz-pedw-ppe-ukw-01.servicebus.windows.net/ar-hc-lz-pedw-prod-ukw-01/ +# RELAYURI = "ar-ns-lz-pedw-prod-ukw-01.servicebus.windows.net" +# RELAYPATH = "ar-hc-lz-pedw-prod-ukw-01" +CRMURL = "crm2016.llcdt.gov.wales/PEDW" //GOOGLE_TAG_MANAGER = GTM-T78CBC3 @@ -84,7 +84,11 @@ DATABASE_URL = sqlserver://sql-srv-plt-pedw-cnr-uks-01.database.windows.net:1433 // dev test sql server # DATABASE_URL = sqlserver://pedw-dev-test-sql-svr.database.windows.net:1433;database=PEDWDev;user=RobBondSQL;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword - DATABASE_URL = sqlserver://sql-srv-plt-pedw-dev-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword +// DATABASE_URL = sqlserver://sql-srv-plt-pedw-dev-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword + +// test sql server + DATABASE_URL = sqlserver://sql-srv-plt-pedw-test-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword + // Pre prod sql server # DATABASE_URL = sqlserver://sql-srv-plt-pedw-ppe-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustservercertificate=true;hostnameincertificate=*.database.windows.net;logintimeout=30;authentication=SqlPassword @@ -93,7 +97,7 @@ DATABASE_URL = sqlserver://sql-srv-plt-pedw-cnr-uks-01.database.windows.net:1433 # DATABASE_URL = sqlserver://pp-pedw-sql.database.windows.net:1433;database=PEDWPreProd;user=RobBondSQL;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword // Prod sql server -# DATABASE_URL = sqlserver://sql-srv-plt-pedw-prod-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword + DATABASE_URL = sqlserver://sql-srv-plt-pedw-prod-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword SECRET = SuperSecret @@ -131,9 +135,9 @@ AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwtestuks.blob.core.windows.net # AZURE_PEDW_STORAGE_ENDPOINT = https://pedwprod.blob.core.windows.net # // New PROD key -# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwproduks" -# AZURE_STORAGE_ACCOUNT_KEY = OfV9MFqXcLLPam9GhEdMWadIOwIzhc5hd5TUqzIuQoswFdtV4ybaemjatSv+Kht0Ny1qQKjLplwY+AStUTZJrg== -# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwproduks.blob.core.windows.net +AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwproduks" +AZURE_STORAGE_ACCOUNT_KEY = OfV9MFqXcLLPam9GhEdMWadIOwIzhc5hd5TUqzIuQoswFdtV4ybaemjatSv+Kht0Ny1qQKjLplwY+AStUTZJrg== +AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwproduks.blob.core.windows.net AZURE_PEDW_CONTAINER = "pedwapplications" @@ -145,7 +149,7 @@ NOTIFY_API_KEY = pedwnextauthlivekey-90f93710-9dae-423e-aea1-11eccd0dc132-5c0ed1 SHOW_DEBUG = true - + // DEV Connection string //NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=4b7568b3-70bd-4591-9dc2-267c0a6501d1;IngestionEndpoint=https://ukwest-0.in.applicationinsights.azure.com/;LiveEndpoint=https://ukwest.livediagnostics.monitor.azure.com/;ApplicationId=447d38b5-5d40-4238-8fb3-0ef4741b3db1 diff --git a/actions/index.js b/actions/index.js index 9f780704..2fbe3561 100644 --- a/actions/index.js +++ b/actions/index.js @@ -828,6 +828,19 @@ export const getMyCases = (loggedInUserId) => { }); }; +export const getMyInvolvements = async (loggedInUserId) => { + try { + const res = await axios.get( + BASE_URL + + "/api/endpoint/getmyinvolvements_api?loggedInUserId=" + + loggedInUserId + ); + return res.data; + } catch (error) { + consoleLogger(error); + } +}; + export const getMyLPACases = (lpaid) => { return axios .get(BASE_URL + "/api/endpoint/getmylpacases_api?lpaid=" + lpaid) @@ -2207,3 +2220,74 @@ export const getAppealPDFDocument = async (incidentid) => { return error.response; }); }; + +export const getNewAppeals = async (searchString) => { + try { + const res = await axios.get(BASE_URL + "/api/admin/getnewappeals_api"); + return res.data; + } catch (error) { + consoleLogger(error); + return error.response; + } +}; + +export const getNewAppealsPage = async ( + searchString, + pageNumber, + orderBy, + fieldSort, + showNumberOfRecords +) => { + return axios + .get( + "/api/admin/getnewappeals_api?searchString=" + + searchString + + "&pageNumber=" + + pageNumber + + "&orderby=" + + orderBy + + "&fieldSort=" + + fieldSort + + "&showNumberOfRecords=" + + showNumberOfRecords + ) + .then((res) => { + return res.data; + }) + .catch((error) => { + consoleLogger(error); + return error.response; + }); +}; + +export const getNewDocumentsPaged = async ( + pageNumber, + orderBy, + fieldSort, + showNumberOfRecords, + documentType, + selectedWeeks +) => { + return axios + .get( + "/api/admin/getlatestdocuments_api?pageNumber=" + + pageNumber + + "&orderby=" + + orderBy + + "&fieldSort=" + + fieldSort + + "&showNumberOfRecords=" + + showNumberOfRecords + + "&documentType=" + + documentType + + "&numberWeeks=" + + selectedWeeks + ) + .then((res) => { + return res.data; + }) + .catch((error) => { + consoleLogger(error); + return error.response; + }); +}; diff --git a/components/admin/tabs/accounts.js b/components/admin/tabs/accounts.js new file mode 100644 index 00000000..97498248 --- /dev/null +++ b/components/admin/tabs/accounts.js @@ -0,0 +1,38 @@ +// components/admin/tabs/AccountsTab.js +import Link from "next/link"; + +export default function AccountsTab({ userData }) { + return ( +
+

User Accounts

+

Number of accounts created: {userData.length}

+ +
+
+
+ Name +
+
+ Last accessed +
+
+ + {userData.map((user) => ( +
+
+ + {user.email} + +
+
+ Last accessed: {user.created} +
+
+ ))} +
+
+ ); +} diff --git a/components/admin/tabs/appeals.js b/components/admin/tabs/appeals.js new file mode 100644 index 00000000..ce39a05f --- /dev/null +++ b/components/admin/tabs/appeals.js @@ -0,0 +1,568 @@ +// components/admin/tabs/AppealsTab.js +import useTranslation from "next-translate/useTranslation"; +import { useCallback, useState, useEffect } from "react"; +import { JSONPath as jsonpath } from "jsonpath-plus"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import PaginationControl from "../../../components/search/pagination"; +import { connect } from "react-redux"; + +import { + setSearchResults, + setSearchDetails, +} from "../../../store/searchOutput/action"; + +import { getDetailsProxy, getSearchDetailsPaged } from "../../utils"; +import { getAdvancedSearchPaged, getNewAppealsPage } from "../../../actions"; +import { + setCurrentPage, + setCurrentReference, +} from "../../../store/currentView/action"; + +function AppealsTab(props) { + const [searchLoaded, setSearchLoaded] = useState(false); + const [showSpinnerState, setShowSpinnerState] = useState(false); + const [selectedOption, setSelectedOption] = useState(10); + const [orderByState, setOrderbyState] = useState("createdon"); + const [fieldSortState, setfieldSortState] = useState("desc"); + const [loginToWatch, setLoginToWatch] = useState("true"); + + const searchDetailsObj = props.searchResultsObj.searchDetailsObj || {}; + const resultsArr = props.searchResultsObj.searchResultsObj.value || []; + const searchResultsObj = props.searchResultsObj.searchResultsObj; + + const { t } = useTranslation(); + const router = useRouter(); + const myportal = false; + const advancedSearch = false; + const searchString = ""; + + const spinnerState = () => { + setShowSpinnerState((prev) => !prev); + }; + + const pagesCount = Math.ceil(searchResultsObj["@odata.count"] / 10); + + let currentPage = 0; + try { + currentPage = decodeURI(searchResultsObj["@odata.nextLink"]).split( + "skiptoken=" + )[1]; + currentPage = + typeof currentPage !== "undefined" + ? parseInt( + decodeURI(currentPage) + .split('pagenumber="')[1] + .slice( + 0, + decodeURI(currentPage) + .split('pagenumber="')[1] + .indexOf('"') + ) - 1 + ) + : pagesCount; + } catch (e) { + currentPage = pagesCount; + } + + const getSearchPageResults = useCallback( + (pageNumber, orderBy, fieldSort) => { + getNewAppealsPage( + searchString, + pageNumber, + orderBy, + fieldSort, + selectedOption + ).then((data) => { + props.setSearchResults(data); // ✅ use props, not import + return getSearchDetailsPaged(data).then((details) => { + props.setSearchDetails(details); // ✅ use props, not import + setShowSpinnerState(false); + }); + }); + }, + [ + advancedSearch, + searchString, + selectedOption, + props.setSearchDetails, + props.setSearchResults, + ] + ); + + useEffect(() => { + if (searchLoaded) { + if (selectedOption) { + setShowSpinnerState(true); + getSearchPageResults(1, orderByState, fieldSortState); + } + } else { + setSearchLoaded(true); + } + }, [ + selectedOption, + fieldSortState, + orderByState, + searchLoaded, + getSearchPageResults, + router.query, + ]); + + const ResultsView = (resultsArr) => { + return ( + <> +
+
+
+ +
+
+ {t("search:searchresults-site-address-label")} +
+
+ +
+
+ +
+
+ +
+
+ {/* {t("search:searchresults-status-label")} */} + +
+ +
+ Origin +
+
+ {showSpinnerState == true ? ( +
+
+ {t("common:spinner-fetching-data")} + { +
+
+ ) : ( + resultsArr.map((item, key) => { + let detailsObj = jsonpath({ + path: + '$..value[?(@ && @.ticketnumber=="' + + item.ticketnumber + + '")]', + json: searchDetailsObj, + eval: true, + }); + + detailsObj = detailsObj[0] || {}; + return ( +
+
+
+ {item.pinswg_publishtoweb == true ? ( + { + setCurrentReference({ + "ticketnumber": + item.ticketnumber, + "currentReference": + item.title, + "currentType": + "searchResultsObj", + "incidentid": + item.incidentid, + "appealType": + item.pinswg_appealcasetype, + "showLoginCheck": + props.showLoginCheck, + "specialistProcess": + detailsObj.hasOwnProperty( + "pinswg_speacialistcaseprocess" + ) + ? detailsObj.pinswg_speacialistcaseprocess + : detailsObj.hasOwnProperty( + "pinswg_specialistcaseprocess" + ) + ? detailsObj.pinswg_specialistcaseprocess + : "", + }); + }} + > + + {t( + "search:searchresults-case-reference-label" + )} + : + + {item.title} + + ) : ( + item.title + )} + +
+ {new Date( + item.createdon + ).toLocaleString()} +
+
+ + {t( + "search:searchresults-site-address-label" + )} + : + + {_.has( + detailsObj, + "pinswg_projectlocation" + ) + ? detailsObj.pinswg_projectlocation != + null + ? detailsObj.pinswg_projectlocation.indexOf( + ";" + ) < 0 + ? detailsObj.pinswg_projectlocation + : router.locale == "cy" + ? detailsObj.pinswg_projectlocation.split( + ";" + )[1] + : detailsObj.pinswg_projectlocation.split( + ";" + )[0] + : "" + : ""} + {_.has( + detailsObj, + "pinswg_siteaddressline1" + ) + ? detailsObj.pinswg_siteaddressline1 + : ""} + + {/* {detailsObj.pinswg_siteaddressline1 != + null &&
} */} + + {_.has( + detailsObj, + "pinswg_siteaddressline1" + ) && + detailsObj.pinswg_siteaddressline1 != + null &&
} + + {_.has( + detailsObj, + "pinswg_siteaddressline1" + ) + ? detailsObj.pinswg_siteaddressline2 + : ""} + + {_.has( + detailsObj, + "pinswg_siteaddressline2" + ) && + detailsObj.pinswg_siteaddressline2 != + null &&
} + {_.has( + detailsObj, + "pinswg_siteaddresstown" + ) + ? detailsObj.pinswg_siteaddresstown + : ""} + {_.has( + detailsObj, + "pinswg_siteaddresstown" + ) && + detailsObj.pinswg_siteaddresstown != + null &&
} + + {_.has( + detailsObj, + "pinswg_siteaddresscounty" + ) + ? detailsObj.pinswg_siteaddresscounty + : ""} + {_.has( + detailsObj, + "pinswg_siteaddresscounty" + ) && + detailsObj.pinswg_siteaddresscounty != + null &&
} + {_.has( + detailsObj, + "pinswg_siteaddresspostcode" + ) + ? detailsObj.pinswg_siteaddresspostcode + : ""} +
+
+ + {t( + "search:searchresults-applicant-label" + )} + : + + {item.pinswg_appellantagent == 846040001 + ? item.pinswg_appellantfirstname + + " " + + item.pinswg_appellantlastname + : _.has(detailsObj, [ + "_pinswg_appellant_value@OData.Community.Display.V1.FormattedValue", + ]) + ? detailsObj[ + "_pinswg_appellant_value@OData.Community.Display.V1.FormattedValue" + ] + : ""} +
+
+ + {t( + "search:searchresults-authority-label" + )} + : + + {_.has(item, [ + "_pinswg_associatedlpa_value@OData.Community.Display.V1.FormattedValue", + ]) + ? router.locale == "cy" + ? jsonpath({ + path: + '$..[?(@ && @.value=="' + + item[ + "_pinswg_associatedlpa_value@OData.Community.Display.V1.FormattedValue" + ] + + '")].value_cy', + json: transLookup, + eval: true, + }) + : item[ + "_pinswg_associatedlpa_value@OData.Community.Display.V1.FormattedValue" + ] || "N/A" + : ""} +
+
+ + {t( + "search:searchresults-case-type-label" + )} + : + + {router.locale == "cy" + ? jsonpath({ + path: + '$..[?(@ && @.value=="' + + item[ + "pinswg_appealcasetype@OData.Community.Display.V1.FormattedValue" + ] + + '")].value_cy', + json: transLookup, + eval: true, + }) + : item[ + "pinswg_appealcasetype@OData.Community.Display.V1.FormattedValue" + ] || "N/A"} +
+
+ + {t( + "search:searchresults-status-label" + )} + : + + + {router.locale == "cy" + ? jsonpath({ + path: + '$..[?(@ && @.value=="' + + item[ + "statuscode@OData.Community.Display.V1.FormattedValue" + ] + + '")].value_cy', + json: transLookup, + eval: true, + }) + : item[ + "statuscode@OData.Community.Display.V1.FormattedValue" + ] || "N/A"} +
+
+ {item[ + "caseorigincode@OData.Community.Display.V1.FormattedValue" + ] || "N/A"} +
+
+ ); + }) + )} +
+ + + ); + }; + + return ( +
+

Appeals

+ {searchResultsObj["@odata.count"] > 5 && ( +
+
+ +
+
+ )} + {resultsArr.length > 0 ? ( + ResultsView(resultsArr) + ) : ( +
+
+

{t("search:searchresults-no-records-label")}

+
+
+ )} +
+ ); +} + +const mapDispatchToProps = (dispatch) => { + return { + setCurrentReference: (currentReference) => { + dispatch(setCurrentReference(currentReference)); + }, + setSearchResults: (searchResultsObj) => { + dispatch(setSearchResults(searchResultsObj)); + }, + setSearchDetails: (searchDetailsObj) => { + dispatch(setSearchDetails(searchDetailsObj)); + }, + setCurrentPage: (currentPage) => { + dispatch(setCurrentPage(currentPage)); + }, + }; +}; + +const mapStateToProps = (state) => { + return { + searchResultsObj: state.searchResultsObj, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(AppealsTab); diff --git a/components/admin/tabs/documents.js b/components/admin/tabs/documents.js new file mode 100644 index 00000000..23818da2 --- /dev/null +++ b/components/admin/tabs/documents.js @@ -0,0 +1,1047 @@ +import { JSONPath as jsonpath } from "jsonpath-plus"; +import useTranslation from "next-translate/useTranslation"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { connect } from "react-redux"; +import transLookup from "../../../data/lookuptranslations.json"; +import { formatDates } from "../../utils"; +import PaginationControl from "../../../components/case/pagination"; +import { sendGAEvent } from "@next/third-parties/google"; + +import { + getSearchDocumentDetails, + getSearchDocumentDetailsPaged, + getSearchDocumentTypes, + getNewDocumentsPaged, +} from "../../../actions"; +import { setCurrentPage } from "../../../store/currentView/action"; + +import { setDocumentDetails } from "../../../store/searchOutput/action"; +const getDocumentTypes = [ + { + "Value": 846040009, + "Label": "Main Party - Pre-Submission Docs", + }, + { + "Value": 846040000, + "Label": "Main Party - Submission Document", + }, + { + "Value": 846040001, + "Label": "Main Party - Questionnaire", + }, + { + "Value": 846040002, + "Label": "Main Party - Statements", + }, + { + "Value": 846040003, + "Label": "Main Party - Comments", + }, + { + "Value": 846040004, + "Label": "Main Party - Written Statements of Evidence", + }, + { + "Value": 846040010, + "Label": "Main Party - Environmental Statements", + }, + { + "Value": 846040005, + "Label": "Interested Party Representations", + }, + { + "Value": 846040006, + "Label": "Correspondence", + }, + { + "Value": 846040008, + "Label": "Inspector Notes", + }, + { + "Value": 846040007, + "Label": "Final Decision/Report", + }, + { + "Value": 846040011, + "Label": "Post Decision Correspondence", + }, + { + "Value": 846040012, + "Label": "High Court Challenge Correspondence", + }, + { + "Value": 846040013, + "Label": "Event - Correspondence", + }, + { + "Value": 846040014, + "Label": "Event - Documents", + }, + { + "Value": 846040015, + "Label": "Pre-Application - General", + }, + { + "Value": 846040016, + "Label": "Pre-Application - EIA Screening", + }, + { + "Value": 846040017, + "Label": "Pre-Application - EIA Scoping", + }, + { + "Value": 846040018, + "Label": "EIA Screening", + }, + { + "Value": 846040019, + "Label": "EIA Scoping", + }, + { + "Value": 846040020, + "Label": "Pre-Application Services Documents", + }, + { + "Value": 846040021, + "Label": "Notification", + }, + { + "Value": 846040022, + "Label": + "Submission - Environmental Statement - written statement and NTS", + }, + { + "Value": 846040023, + "Label": "Submission - Environmental Statement - Appendices", + }, + { + "Value": 846040024, + "Label": "Submission - Environmental Statement - Figures", + }, + { + "Value": 846040025, + "Label": "Submission - Draft Infrastructure Consent Orders", + }, + { + "Value": 846040026, + "Label": "Submission - Compulsory Acquisition Information", + }, + { + "Value": 846040027, + "Label": "Submission - Application Documents", + }, + { + "Value": 846040028, + "Label": "Examination Notices", + }, + { + "Value": 846040029, + "Label": "Local/Marine Impact Reports", + }, + { + "Value": 846040030, + "Label": "Submission - Further Information", + }, + { + "Value": 846040031, + "Label": "Formal Variation requests", + }, + { + "Value": 846040032, + "Label": "Further Information - Interested Party Representations", + }, + { + "Value": 846040033, + "Label": "Hearing Statements", + }, + { + "Value": 846040034, + "Label": "Events - Recordings", + }, + { + "Value": 846040035, + "Label": "Internal Correspondence", + }, + { + "Value": 846040036, + "Label": "Consultation Response", + }, +]; + +const DocumentsTab = (props) => { + let { t, lang } = useTranslation(); + const firstRender = useRef(false); + const { + incidentid, + caseReference, + searchResultsObj, + searchDetailsObj, + documentDetailsObj, + setDocumentDetails, + setCurrentPage, + docsOffline, + showFilteredDocs, + } = props; + + const router = useRouter(); + const { locale } = router; + + var documentDetailsArr = documentDetailsObj || []; + + const [selectedOption, setSelectedOption] = useState(10); + const [selectedWeeks, setSelectedWeeks] = useState(1); + const [documentTypes, setDocumentTypes] = useState(getDocumentTypes); + const [selectedDocumentType, setSelectedDocumentType] = useState("all"); + const [selectAll, setSelectAll] = useState(false); + + const [checkedItems, setCheckedItems] = useState( + documentTypes.reduce((acc, item) => { + acc[item.pinswg_isharedocumentlocations] = false; + return acc; + }, {}) + ); + + const handleCheckboxChange = (e) => { + const { name, checked } = e.target; + + setCheckedItems((prev) => ({ + ...prev, + [name]: checked, + })); + setSelectAll(false); + }; + + const handleSelectAllChange = (e) => { + const { checked } = e.target; + setSelectAll(checked); // Set the 'Select All' state + if (checked) { + // If 'Select All' is checked, check all the other checkboxes + + setCheckedItems( + documentTypes.reduce((acc, item) => { + acc[item.pinswg_isharedocumentlocations] = true; + return acc; + }, {}) + ); + } else { + clearCheckboxes(); + } + }; + + const clearCheckboxes = () => { + setSelectAll(false); + setCheckedItems( + documentTypes.reduce((acc, item) => { + acc[item.pinswg_isharedocumentlocations] = false; + return acc; + }, {}), + setSelectedDocumentType("all"), + setCurrentPage(1) + ); + }; + let ShowDocLinks = false; + let checkShowDocLinks = docsOffline != false ? true : false; + + const getDocLink = (docsOffline) => { + var startTime = docsOffline.split(",")[0]; + var endTime = docsOffline.split(",")[1]; + + // console.log(startTime); + // console.log(endTime); + + var currentDate = new Date(); + + var startDate = new Date(currentDate.getTime()); + startDate.setHours(startTime.split(":")[0]); + startDate.setMinutes(startTime.split(":")[1]); + startDate.setSeconds(startTime.split(":")[2]); + + var endDate = new Date(currentDate.getTime()); + endDate.setHours(endTime.split(":")[0]); + endDate.setMinutes(endTime.split(":")[1]); + endDate.setSeconds(endTime.split(":")[2]); + + // console.log(startDate); + // console.log(endDate); + // console.log(currentDate); + + var valid = startDate < currentDate && endDate > currentDate; + ShowDocLinks = valid; + }; + + checkShowDocLinks && getDocLink(docsOffline); + + const FilteredDocumentView = (documentDetailsArr) => { + documentDetailsArr = documentDetailsArr.value; + + return ( + <> + {ShowDocLinks && ( +
+ + + + Notice + + + {t("case:documents-not-available-label")} + +
+ )} + +
+
+ {" "} +
+

+ {documentDetailsObj["@odata.count"] > 1 + ? t("case:documents-count-label-1") + : t("case:documents-count-label-1a")}{" "} + {documentDetailsObj["@odata.count"]}{" "} + {documentDetailsObj["@odata.count"] > 1 + ? t("case:documents-count-label-2") + : t("case:documents-count-label-2a")} +

+
+ +
+
+ {" "} + <> +
+ + +
+ + {documentTypes.map((item, key) => { + return ( +
+ + +
+ ); + })} +
{" "} +
+ +
+ {documentDetailsObj["@odata.count"] > 5 && ( +
+
+ +
+
+ )} +
+
+ +
+
+
+ {_.isEmpty(documentDetailsObj.value) ? ( +
+
+

+ {/* {showDocumentSearchState == false + ? t("case:documents-checking-label") + : t( + "case:documents-non-available-label" + )} */} + No documents found +

+
+
+ ) : ( + <> +
+
+
+ +
+
+ {/* {t("case:documents-doc-type-label")} */} + +
+
+ {/* {t("case:documents-date-published-label")} */} + +
+
+ {/* {t("case:documents-date-published-label")} */} + Case +
+
+ {/* {t("case:documents-date-published-label")} */} + Origin +
+
+ {/* {t("case:documents-date-published-label")} */} + Published +
+ + {/*
+ {t("case:documents-size-label")} +
*/} +
+ {showSpinnerState == true ? ( +
+
+ {t( + "common:spinner-fetching-data" + )} + { +
+
+ ) : ( + documentDetailsArr.map((item, key) => { + let detailsObj = jsonpath({ + path: + "$..[?(@ && @._pinswg_documentids_value=='" + + incidentid + + "')]", + json: item, + eval: true, + }); + detailsObj = item; + return ( +
+
+ {showDownloadingState === + key && ( +
+ {/* { */} +
+ )} + + {!ShowDocLinks ? ( + detailsObj.pinswg_publishtoweb ? ( + { + toggleDownLoadLink( + key + ), + sendGAEvent( + "event", + "DownloadedFile", + { + caseReference: + props.caseReference, + filename: + detailsObj.pinswg_name, + } + ); + }} + > + + {t( + "case:documents-name-label" + )} + : + + {_.has( + detailsObj, + "pinswg_name" + ) + ? detailsObj.pinswg_name + : ""} + + ) : ( + <> + + {t( + "case:documents-name-label" + )} + : + + {_.has( + detailsObj, + "pinswg_name" + ) + ? detailsObj.pinswg_name + : ""} + + ) + ) : ( + <> + + {t( + "case:documents-name-label" + )} + : + + {_.has( + detailsObj, + "pinswg_name" + ) + ? detailsObj.pinswg_name + : ""} + + )} +
+
+ + {t( + "case:documents-doc-type-label" + )} + : + + + {router.locale == "cy" + ? jsonpath({ + path: + '$..[?(@ && @.value=="' + + detailsObj[ + "pinswg_isharedocumentlocations@OData.Community.Display.V1.FormattedValue" + ] + + '")].value_cy', + json: transLookup, + eval: true, + }) + : detailsObj[ + "pinswg_isharedocumentlocations@OData.Community.Display.V1.FormattedValue" + ] || + t( + "case:summary-no-date-entered-label" + )} +
+
+ + {t( + "case:documents-date-published-label" + )} + : + + + {_.has( + detailsObj, + "createdon" + ) + ? new Date( + detailsObj.createdon + ).toLocaleString() + : ""} +
+
+ {detailsObj.publishtoweb == + true ? ( + + { + detailsObj[ + "_pinswg_documentids_value@OData.Community.Display.V1.FormattedValue" + ] + } + + ) : ( + detailsObj[ + "_pinswg_documentids_value@OData.Community.Display.V1.FormattedValue" + ] + )} +
+
+ { + detailsObj[ + "pinswg_origin@OData.Community.Display.V1.FormattedValue" + ] + } +
+
+ { + detailsObj[ + "pinswg_publishtoweb@OData.Community.Display.V1.FormattedValue" + ] + } +
+ + {/*
+ + {t("case:documents-size-label")}: + + 92 +
*/} +
+ ); + }) + )} +
+ + + )} +
+
+ + ); + }; + + const getDocumentResults = useCallback( + (pageNumber, orderBy, fieldSort) => { + getNewDocumentsPaged( + pageNumber, + orderBy, + fieldSort, + selectedOption, + selectedDocumentType, + selectedWeeks + ) + .then((data) => data) + .then((data) => { + props.setDocumentDetails(data); + setShowSpinnerState(false); + }); + }, + [ + props.incidentid, + selectedOption, + selectedDocumentType, + selectedWeeks, + props.setDocumentDetails, + ] + ); + + const getDocumentDetails = async () => { + let docObj = await getSearchDocumentDetailsPaged( + incidentid, + pageNumber, + orderBy, + fieldSort + ).then((data) => { + props.setDocumentDetails(data); + return data; + }); + + return docObj; + }; + + const pagesCount = Math.ceil(documentDetailsObj["@odata.count"] / 10); + + let currentPage = 0; + + currentPage = decodeURI(documentDetailsObj["@odata.nextLink"]).split( + "skiptoken=" + )[1]; + currentPage = + typeof currentPage != "undefined" + ? parseInt( + decodeURI(currentPage) + .split('pagenumber="')[1] + .slice( + 0, + decodeURI(currentPage) + .split('pagenumber="')[1] + .indexOf('"') + ) - 1 + ) + : pagesCount; + + const [showSpinnerState, setShowSpinnerState] = useState(false); + + const [showDownloadingState, setShowDownloadingState] = useState(null); + + const toggleDownLoadLink = (i) => { + setShowDownloadingState(i); + + const downloadTime = setTimeout(hideDownloadCover, 5000); + downloadTime; + }; + + function hideDownloadCover() { + setShowDownloadingState(null); + } + + let spinnerState = () => { + showSpinnerState == true + ? setShowSpinnerState(false) + : setShowSpinnerState(true); + }; + + const [showDocumentSearchState, setDocumentSearchState] = useState(false); + const [orderByState, setOrderbyState] = useState("createdon"); + const [fieldSortState, setfieldSortState] = useState("desc"); + + let documentSearchState = () => { + showDocumentSearchState == true + ? setDocumentSearchState(false) + : setDocumentSearchState(true); + }; + + const sortDataBy = (whichField) => { + setOrderbyState(whichField); + setfieldSortState("asc"); + + getDocumentResults(1, whichField, fieldSortState); + whichField == orderByState + ? fieldSortState == "desc" + ? setfieldSortState("asc") + : setfieldSortState("desc") + : setfieldSortState("asc"); + setCurrentPage(1); + setShowSpinnerState(true); + }; + + useEffect(() => { + if (selectedOption) { + setShowSpinnerState(true); + getDocumentResults(1, orderByState, fieldSortState); + } + + if (selectedWeeks) { + setShowSpinnerState(true); + getDocumentResults(1, orderByState, fieldSortState); + } + + if (selectedDocumentType) { + setShowSpinnerState(true); + getDocumentResults( + 1, + orderByState, + fieldSortState, + selectedDocumentType + ); + } + + const docDetailsObj = async () => { + let docObj = await getNewDocumentsPaged( + pageNumber, + orderBy, + fieldSort, + selectedOption, + selectedDocumentType, + selectedWeeks + ).then((data) => { + props.setDocumentDetails(data); + setDocumentSearchState(true); + + return data; + }); + + return docObj; + }; + + //const searchDocumentResultsObj = docDetailsObj(); + }, [ + incidentid, + props.setDocumentDetails, + selectedOption, + selectedDocumentType, + fieldSortState, + orderByState, + getDocumentTypes, + getDocumentResults, + lang, + selectedWeeks, + ]); + + function countFieldValue(obj, field, targetValue) { + let count = 0; + + function traverse(node) { + if (Array.isArray(node)) { + node.forEach(traverse); + } else if (node !== null && typeof node === "object") { + // If this object has the field, check it + if (node.hasOwnProperty(field) && node[field] === targetValue) { + count++; + } + // Keep traversing children + Object.values(node).forEach(traverse); + } + } + + traverse(obj); + return count; + } + + return ( +
+
+

+ {t("case:documents-heading-label")} +

+ {/*
+ MDU :{" "} + {countFieldValue( + documentDetailsObj, + "pinswg_origin", + 846040000 + )}{" "} + / Portal :{" "} + {countFieldValue( + documentDetailsObj, + "pinswg_origin", + 846040001 + )} +
*/} +
+
+ +
+
+ {FilteredDocumentView(documentDetailsArr)} +
+
+ ); +}; + +const mapStateToProps = (state) => { + return { + ...state, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + setDocumentDetails: (detailsObj) => { + dispatch(setDocumentDetails(detailsObj)); + }, + setCurrentPage: (currentPage) => { + dispatch(setCurrentPage(currentPage)); + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(DocumentsTab); diff --git a/components/admin/tabs/storage.js b/components/admin/tabs/storage.js new file mode 100644 index 00000000..fcbfb15f --- /dev/null +++ b/components/admin/tabs/storage.js @@ -0,0 +1,266 @@ +// components/admin/tabs/StorageTab.js +import { useState } from "react"; +import Link from "next/link"; +import { bytesToSize } from "../../../components/utils"; +import { + deleteMyRepresentationsFromBlob, + deleteAwaitingSubmissionsFromBlob, + sendRepCompleteMessage, +} from "../../../actions"; + +import { formatBlobDates } from "../utils/adminHelper"; + +export default function StorageTab({ data, repCompleteCount }) { + const [showRepJson, setShowRepJson] = useState(true); + const [showTmpFile, setShowTmpFile] = useState(true); + const [listData, setListData] = useState(data); + + const toggleAll = () => { + const newState = !(showRepJson || showTmpFile); + setShowRepJson(newState); + setShowTmpFile(newState); + }; + + return ( +
+

Storage Account Contents

+ {repCompleteCount > 0 && ( +

Number of Orphaned Reps: {repCompleteCount}

+ )} + + {/* Toggle buttons */} +
+ + + +
+ + {/* Render users & containers */} + {listData.map((user) => ( +
+ {user.containers.map((c) => ( +
+
+

+ {c.container} -{" "} + + {user.email} + +

+ {/* folders */} + {c.folders.map((folder) => ( +
+

{folder.folderPath || "Root"}

+ + {/* Buttons for TMP + Rep actions go here (same logic as before) */} + + {folder.hasTmpFile && ( + <> + + + )} + {folder.hasRepJson && ( +
+ {" "} + + {folder.repComplete && ( + + )} + {folder.repComplete && ( + + )} +
+ )} +
    + {folder.blobs.map((blob) => ( +
  • + + {blob.name} ( + {bytesToSize( + blob.contentLength || + blob.size + )} + ) + {" "} + {blob.lastModified && ( + + {/* { + blob.displayDate + } */} + {formatBlobDates( + blob + )} + + )} +
  • + ))} +
+
+ ))} +
+
+ ))} +
+ ))} +
+ ); +} diff --git a/components/admin/utils/adminHelper.js b/components/admin/utils/adminHelper.js new file mode 100644 index 00000000..87223cf1 --- /dev/null +++ b/components/admin/utils/adminHelper.js @@ -0,0 +1,81 @@ +// components/admin/utils/adminStorage.js + +/** + * Get the folder path up to the last slash. + * Used for grouping blobs by "case folder". + */ +export function getUpToLastSlash(url) { + let lastSlash = url.lastIndexOf("/"); + + url = url.substring(0, lastSlash); + + url = + url.lastIndexOf("/files") > 0 + ? url.substring(0, url.lastIndexOf("/files")) + : url; + return url; +} + +/** + * Convert a stream (Azure blob download) to a string. + */ +export async function streamToString(readableStream) { + return new Promise((resolve, reject) => { + const chunks = []; + readableStream.on("data", (data) => { + chunks.push(data.toString()); + }); + readableStream.on("end", () => { + resolve(chunks.join("")); + }); + readableStream.on("error", reject); + }); +} + +/** + * Format how long ago a date occurred. + */ +export function formatTimeSince(date) { + if (!date) return "Unknown date"; + + const now = new Date(); + const diffMs = now - date; + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays < 7) { + return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`; + } else { + const diffWeeks = Math.floor(diffDays / 7); + return `${diffWeeks} week${diffWeeks !== 1 ? "s" : ""} ago`; + } +} + +/** + * Format Azure blob created/modified dates into a readable string. + */ +export function formatBlobDates(blob) { + const created = blob.createdOn ? new Date(blob.createdOn) : null; + const modified = blob.lastModified ? new Date(blob.lastModified) : null; + + if (!created && !modified) return "Unknown"; + + // If both exist and are the same + if (created && modified && created.getTime() === modified.getTime()) { + return `${created.toLocaleString("en-GB")} (${formatTimeSince( + created + )})`; + } + + // If modified exists and is newer than created + if (created && modified && modified.getTime() > created.getTime()) { + return `Created: ${created.toLocaleString("en-GB")} (${formatTimeSince( + created + )}), Modified: ${modified.toLocaleString("en-GB")} (${formatTimeSince( + modified + )})`; + } + + // Only one date exists + const date = modified || created; + return `${date.toLocaleString("en-GB")} (${formatTimeSince(date)})`; +} diff --git a/components/admin/utils/prisma.js b/components/admin/utils/prisma.js new file mode 100644 index 00000000..ed7b9045 --- /dev/null +++ b/components/admin/utils/prisma.js @@ -0,0 +1,14 @@ +// lib/prisma.js +import { PrismaClient } from "@prisma/client"; + +const globalForPrisma = globalThis; + +export const prisma = + globalForPrisma.prisma || + new PrismaClient({ + log: ["query", "error", "warn"], + }); + +if (process.env.NODE_ENV !== "production") { + globalForPrisma.prisma = prisma; +} diff --git a/components/admin/utils/serverside.js b/components/admin/utils/serverside.js new file mode 100644 index 00000000..9bb35048 --- /dev/null +++ b/components/admin/utils/serverside.js @@ -0,0 +1,166 @@ +// components/admin/utils/serverStorage.js +import { DefaultAzureCredential } from "@azure/identity"; +import { BlobServiceClient } from "@azure/storage-blob"; +import { PrismaClient } from "@prisma/client"; +import CryptoJS from "crypto-js"; +import { getUpToLastSlash, streamToString } from "../utils/adminHelper"; +import { listContainersForUser } from "../../../actions/azurestorage"; + +const prisma = global.prisma || new PrismaClient(); + +/** + * Generate HMAC hash for blob API link. + */ +function hashAPIPath(queryPath, WORDKEY) { + let hashlink = CryptoJS.HmacSHA256( + queryPath, + CryptoJS.enc.Hex.parse(WORDKEY) + ); + return hashlink.toString(CryptoJS.enc.Hex); +} + +/** + * Fetch blob structure + users for admin storage page. + */ +export async function fetchAdminStorageData() { + const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME; + const creds = new DefaultAzureCredential(); + const blobServiceClient = new BlobServiceClient( + `https://${accountName}.blob.core.windows.net`, + creds + ); + + const prisma = global.prisma || new PrismaClient(); + + const users = await prisma.user.findMany(); + + let data = await Promise.all( + users.map(async (user) => { + const containers = await listContainersForUser( + blobServiceClient, + `${user.id}`, + user.email + ); + + const containersWithHashes = await Promise.all( + containers.map(async (container) => { + const groupedByFolder = await container.blobs.reduce( + async (accP, blob) => { + const acc = await accP; + const folderPath = + getUpToLastSlash(blob.name.trim()) || ""; + + const hashedfilepath = hashAPIPath( + `/api/file/downloadblob?container=${ + container.container + }&casefolderID=${folderPath}&blobname=${blob.name + .substring(blob.name.lastIndexOf("/") + 1) + .trim()}`, + process.env.HASHKEY + ); + + const fileNameOnly = blob.name.substring( + blob.name.lastIndexOf("/") + 1 + ); + const isTmpFile = fileNameOnly.startsWith("TMP-"); + + const blobWithHash = { + ...blob, + hashedfilepath, + isTmpFile, + }; + + let folderGroup = acc.find( + (f) => f.folderPath === folderPath + ); + if (!folderGroup) { + folderGroup = { + folderPath, + blobs: [], + hasRepJson: false, + repJsonBlob: null, + repComplete: false, + hasTmpFile: false, + }; + acc.push(folderGroup); + } + + folderGroup.blobs.push(blobWithHash); + + if (isTmpFile) { + folderGroup.hasTmpFile = true; + } + + // rep.json handling + if (blob.name.toLowerCase().endsWith("rep.json")) { + folderGroup.hasRepJson = true; + folderGroup.repJsonBlob = blobWithHash; + + const containerClient = + blobServiceClient.getContainerClient( + container.container + ); + const blockBlobClient = + containerClient.getBlockBlobClient( + blob.name + ); + + try { + const downloadResponse = + await blockBlobClient.download(0); + const downloaded = await streamToString( + downloadResponse.readableStreamBody + ); + const json = JSON.parse(downloaded); + folderGroup.repComplete = Boolean( + json.repComplete + ); + } catch (err) { + console.error( + "Error reading rep.json", + err + ); + folderGroup.repComplete = false; + } + } + + return acc; + }, + Promise.resolve([]) + ); + + return { ...container, folders: groupedByFolder }; + }) + ); + + return containersWithHashes.length > 0 + ? { + id: user.id, + email: user.email, + containers: containersWithHashes, + } + : null; + }) + ); + + data = data.filter(Boolean); + + let userData = users + .filter((user) => user.email && user.email.trim() !== "") + .map((user) => ({ + id: user.id, + email: user.email, + created: user.emailVerified?.toLocaleString("en-GB") || "", + })); + + let repCompleteCount = 0; + data.forEach((user) => { + user.containers.forEach((container) => { + container.folders.forEach((folder) => { + if (folder.repComplete === true) repCompleteCount++; + }); + }); + }); + + return { data, userData, repCompleteCount }; +} diff --git a/data/collections.json b/data/collections.json index 9073ccfa..347bae55 100644 --- a/data/collections.json +++ b/data/collections.json @@ -47,27 +47,7 @@ "UrlName": "transportandworkact", "MetadataId": "bc3edc4b-7d82-eb11-aac0-00224800be9c", "appealTypeID": "846040013", - "NavigationProperty": "", - "value": "Transport and Works Act" - }, - { - "LogicalCollectionName": "pinswg_transportandworkacts", - "LogicalName": "pinswg_transportandworkact", - "PrimaryIdAttribute": "pinswg_transportandworkactid", - "UrlName": "transportandworkact", - "MetadataId": "bc3edc4b-7d82-eb11-aac0-00224800be9c", - "appealTypeID": "846040013", - "NavigationProperty": "pinswg_TransportandWorkActIdspinswg_TransportandWorkActIds", - "value": "Transport and Works Act" - }, - { - "LogicalCollectionName": "pinswg_transportandworkacts", - "LogicalName": "pinswg_transportandworksact", - "PrimaryIdAttribute": "pinswg_transportandworkactid", - "UrlName": "transportandworksact", - "MetadataId": "bc3edc4b-7d82-eb11-aac0-00224800be9c", - "appealTypeID": "846040013", - "NavigationProperty": "pinswg_TransportandWorkActIdspinswg_TransportandWorkActIds", + "NavigationProperty": "pinswg_TransportandWorkActIds", "value": "Transport and Works Act" }, { diff --git a/i18n.js b/i18n.js index 047546f3..3b680589 100644 --- a/i18n.js +++ b/i18n.js @@ -78,5 +78,6 @@ module.exports = { "/auth/signin": ["common", "common", "auth"], "/auth/verify-request": ["common", "common", "auth"], "/auth/error": ["common", "common", "auth"], + "/admin": ["case"], }, }; diff --git a/pages/admin/index.js b/pages/admin/index.js new file mode 100644 index 00000000..ab21835b --- /dev/null +++ b/pages/admin/index.js @@ -0,0 +1,215 @@ +// pages/admin/storage.js +import { useState } from "react"; +import Head from "next/head"; +import CookieBanner from "../../components/cookieBanner"; +import Header from "../../components/header"; +import Footer from "../../components/footer"; +import { connect } from "react-redux"; +import { wrapper } from "../../store/store"; + +import StorageTab from "../../components/admin/tabs/storage"; +import AccountsTab from "../../components/admin/tabs/accounts"; +import AppealsTab from "../../components/admin/tabs/appeals"; +import DocumentsTab from "../../components/admin/tabs/documents"; + +import { fetchAdminStorageData } from "../../components/admin/utils/serverside"; +import { getNewAppeals } from "../../actions"; + +import { getSearchDetails } from "../../components/utils"; + +import { + setSearchDetails, + setSearchResults, +} from "../../store/searchOutput/action"; +import { setCurrentPage } from "../../store/currentView/action"; + +const StoragePage = (props) => { + const { + data, + userData, + repCompleteCount, + searchResultsObj, + docsOffline, + showFilteredDocs, + } = props; + const [whichTab, setWhichTab] = useState("documents"); + + return ( +
+ + Admin Browser + +
+ +
+ +
+
+ +
+
+ +
+
+
+ ); +}; + +export const getServerSideProps = wrapper.getServerSideProps( + (store) => async (ctx) => { + const { query, req, res } = ctx; + // IP restriction check + const ALLOWED_IPS = process.env.ALLOWED_IPS + ? process.env.ALLOWED_IPS.split(",").map((ip) => ip.trim()) + : []; + const LOCALHOST_IPS = ["127.0.0.1", "::1"]; + const forwarded = req.headers["x-forwarded-for"]; + const ip = + typeof forwarded === "string" + ? forwarded.split(",")[0] + : req.socket.remoteAddress; + + if (![...ALLOWED_IPS, ...LOCALHOST_IPS].includes(ip)) { + return { + redirect: { destination: "/403", permanent: false }, + }; + } + + // 🔹 fetch actual data + const { data, userData, repCompleteCount } = + await fetchAdminStorageData(); + + const newAppeals = await getNewAppeals(); + + const searchDetailsObj = await getSearchDetails(newAppeals); + + store.dispatch(setSearchResults(newAppeals)); + store.dispatch(setSearchDetails(searchDetailsObj)); + const state = store.getState(); + + store.dispatch(setCurrentPage(1)); + + return { + props: { + data, + userData, + repCompleteCount, + docsOffline: process.env.DOCAPI_OFFLINE || false, + showFilteredDocs: process.env.SHOWFILTERDOCS || false, + }, + }; + } +); + +const mapDispatchToProps = (dispatch) => { + return { + setCurrentPage: (currentPage) => { + dispatch(setCurrentPage(currentPage)); + }, + }; +}; + +const mapStateToProps = (state) => { + console.log("===============================\n state:", state); + return { + searchResultsObj: state.searchResultsObj, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(StoragePage); diff --git a/pages/api/admin/getlatestdocuments_api.js b/pages/api/admin/getlatestdocuments_api.js new file mode 100644 index 00000000..c5742dab --- /dev/null +++ b/pages/api/admin/getlatestdocuments_api.js @@ -0,0 +1,156 @@ +/** + * @swagger + * /api/endpoint/getbasicsearch_api: + * get: + * tags: [Search] + * description: Basic search + * parameters: + * - name: searchString + * in: query + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ + +import axios from "axios"; +import CryptoJS from "crypto-js"; +import _ from "lodash"; +import { + azureHeadersPagedCustom, + consoleLogger, + getToken, +} from "../../../actions"; +import { number } from "prop-types"; + +const WORDKEY = process.env.HASHKEY; + +const WEBAPI_URL = + process.env.RELAY_ROOT || + "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; + +const hashAPIPath = (queryPath) => { + var hashlink = CryptoJS.HmacSHA256( + queryPath, + CryptoJS.enc.Hex.parse(WORDKEY) + ); + hashlink = hashlink.toString(CryptoJS.enc.Hex); + + //return "&hash=" + hashlink; + + return (queryPath.indexOf("?") > -1 ? "&hash=" : "?hash=") + hashlink; +}; + +const encryptDocReference = (documentRef) => { + var hashlink = CryptoJS.HmacSHA256( + "documents/download/" + documentRef, + CryptoJS.enc.Hex.parse(WORDKEY) + ); + hashlink = hashlink.toString(CryptoJS.enc.Hex); + + var hashStr = + "/api/documents/download/" + documentRef + "?hash=" + hashlink; + + return hashStr; +}; + +export default async function ApiProxy(req, res) { + var pageNumber = req.query.pageNumber || 1; + var token = await getToken(); + + var orderby = req.query.orderby || "createdon"; + var fieldSort = req.query.fieldSort || "desc"; + var showNumberOfRecords = req.query.showNumberOfRecords || 10; + var documentType = req.query.documentType || "all"; + var numberOfWeeks = req.query.numberWeeks || 1; + + var docTypeQueryString = ""; + + if (documentType != "all") { + if (documentType.indexOf(",") >= 0) { + const documentTypeArr = documentType.split(","); + + docTypeQueryString = " and ("; + documentTypeArr.forEach(function (item, index) { + console.log(item, index); + + if (item != "all") { + docTypeQueryString += + " pinswg_isharedocumentlocations eq " + item; + + docTypeQueryString += + index < documentTypeArr.length - 1 ? " or " : ""; + } + }); + docTypeQueryString += " )"; + } else { + docTypeQueryString += + " and pinswg_isharedocumentlocations eq " + documentType; + } + } + + function daysAgoISO(days) { + const today = new Date(); + const resultDate = new Date(today); + resultDate.setUTCDate(today.getUTCDate() - days); // subtract weeks + resultDate.setUTCHours(0, 0, 0, 0); // reset time to 00:00:00 + return resultDate.toISOString().replace(/\.000Z$/, "Z"); // format as 'YYYY-MM-DDT00:00:00Z' + } + + var queryUrl = + "pinswg_documents?$select=pinswg_name,createdon,pinswg_publishtoweb,_pinswg_documentids_value,pinswg_isharedocumentlocations,pinswg_isharedocumentreference,pinswg_uploadurl,pinswg_uploadstatus,pinswg_origin&$filter=createdon ge " + + daysAgoISO(numberOfWeeks) + + docTypeQueryString + + "&$orderby=" + + orderby + + " " + + fieldSort + + "&$count=true" + + "&$expand=pinswg_DocumentIds($select = ticketnumber,pinswg_publishtoweb)" + + (typeof pageNumber != "undefined" + ? "&$skiptoken=" + '' + : ""); + + console.log( + "\n==========================================\n", + "\nnew docs search ", + "\n\nQuery url: " + queryUrl, + "\n\nRelay link: " + WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), + "\n==========================================\n" + ); + + var apiResponse = axios + .get( + WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), + azureHeadersPagedCustom(token.access_token, showNumberOfRecords) + ) + .then(({ data }) => { + let flattened = data.value.map((r) => ({ + ...r, + ticketnumber: r.pinswg_DocumentIds?.ticketnumber || null, + publishtoweb: r.pinswg_DocumentIds?.pinswg_publishtoweb || null, + })); + + data.value = flattened; + data.value.forEach(function (element) { + element.pinswg_hashlink = encryptDocReference( + element.pinswg_isharedocumentreference + ); + }); + + var dataStr; + + _.has(data, "@odata.nextLink") == true && + ((dataStr = JSON.stringify(data["@odata.nextLink"])), + (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); + res.status(200).json(data); + }) + .catch((error) => { + consoleLogger(error); + res.status(400).json(error); + }); + + return apiResponse; +} diff --git a/pages/api/admin/getnewappeals_api.js b/pages/api/admin/getnewappeals_api.js new file mode 100644 index 00000000..daa69cb0 --- /dev/null +++ b/pages/api/admin/getnewappeals_api.js @@ -0,0 +1,90 @@ +/** + * @swagger + * /api/endpoint/getbasicsearch_api: + * get: + * tags: [Search] + * description: Basic search + * parameters: + * - name: searchString + * in: query + * required: true + * schema: + * type: string + * responses: + * 200: + * description: Success + */ + +import axios from "axios"; +import CryptoJS from "crypto-js"; +import _ from "lodash"; +import { + azureHeadersPagedCustom, + consoleLogger, + getToken, +} from "../../../actions"; + +const WORDKEY = process.env.HASHKEY; + +const WEBAPI_URL = + process.env.RELAY_ROOT || + "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; + +const hashAPIPath = (queryPath) => { + var hashlink = CryptoJS.HmacSHA256( + queryPath, + CryptoJS.enc.Hex.parse(WORDKEY) + ); + hashlink = hashlink.toString(CryptoJS.enc.Hex); + + //return "&hash=" + hashlink; + + return (queryPath.indexOf("?") > -1 ? "&hash=" : "?hash=") + hashlink; +}; + +export default async function ApiProxy(req, res) { + var searchString = req.query.searchString; + var pageNumber = req.query.pageNumber || 1; + var token = await getToken(); + + var orderby = req.query.orderby || "createdon"; + var fieldSort = req.query.fieldSort || "desc"; + var showNumberOfRecords = req.query.showNumberOfRecords || 10; + + var queryUrl = + "incidents?$select=caseorigincode,pinswg_publishtoweb,modifiedon,description,numberofchildincidents,_accountid_value,_customerid_value,_pinswg_associatedlpa_value,_ownerid_value,pinswg_appealcasetype,statuscode,ticketnumber,title, _primarycontactid_value,pinswg_lpareference,pinswg_appellantagent,pinswg_appellantfirstname,pinswg_appellantlastname&$expand=primarycontactid($select=fullname)&$orderby=" + + orderby + + " " + + fieldSort + + "&$count=true" + + (typeof pageNumber != "undefined" + ? "&$skiptoken=" + '' + : ""); + + console.log( + "\n==========================================\n", + "\nnew appeal search ", + "\n\nQuery url: " + queryUrl, + "\n\nRelay link: " + WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), + "\n==========================================\n" + ); + + var apiResponse = axios + .get( + WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), + azureHeadersPagedCustom(token.access_token, showNumberOfRecords) + ) + .then(({ data }) => { + var dataStr; + _.has(data, "@odata.nextLink") == true && + ((dataStr = JSON.stringify(data["@odata.nextLink"])), + (data["@odata.nextLink"] = dataStr.split("/v8.2/")[1])); + res.status(200).json(data); + }) + .catch((error) => { + consoleLogger(error); + res.status(400).json(error); + }); + + return apiResponse; +} diff --git a/pages/storage.js b/pages/storage.js deleted file mode 100644 index 2c79953d..00000000 --- a/pages/storage.js +++ /dev/null @@ -1,100 +0,0 @@ -import { DefaultAzureCredential } from "@azure/identity"; -import { BlobServiceClient, ContainerClient } from "@azure/storage-blob"; -import { v4 as uuidv4 } from "uuid"; -import { wrapper } from "../store/store"; - -import { getIP } from "../actions"; - -const ShowStorage = (props) => { - const { container } = props; - //console.log("Containers:"); - // // for await (const container of blobServiceClient.listContainers()) { - // // console.log(`- ${container.name}`); - // // } - - console.log(container); - - return
hello
; -}; - -export const getServerSideProps = wrapper.getServerSideProps( - (store) => async (ctx) => { - const { query, req, res } = ctx; - getIP(req); - - console.log( - "have therese?:", - process.env.AZURE_TENANT_ID, - process.env.AZURE_CLIENT_ID, - process.env.AZURE_CLIENT_SECRET - ); - - const creds = new DefaultAzureCredential(); - - const blobServiceClient = new BlobServiceClient( - `https://pedwdev.blob.core.windows.net`, - creds - ); - - const containerName = `CAS-999999-22222`; - - const containerClient = new ContainerClient( - `https://pedwdev.blob.core.windows.net/${containerName}`, - creds - ); - - //const createContainerResponse = await containerClient.create(); - //console.log( - // `Created container ${containerName} successfully`, - // createContainerResponse.requestId - // ); - - console.log("Containers:"); - for await (const container of blobServiceClient.listContainers()) { - console.log(`- ${container.name}`); - } - - const appealID = uuidv4(); - const uploadOptions = { - metadata: { reviewer: "john", reviewDate: "2022-04-01" }, - tags: { owner: appealID }, - }; - // for (let index = 0; index < 7; index++) { - // // Create a blob - // const content = "hello"; - // const blobName = appealID + "/BOBnewblob" + new Date().getTime(); - // const blockBlobClient = - // containerClient.getBlockBlobClient(blobName); - - // //const uploadBlobResponse = await blockBlobClient.upload( - // // content, - // // Buffer.byteLength(content), - // // uploadOptions - // // ); - - // const uploadBlobResponse = await blockBlobClient.uploadFile( - // "public/assets/images/HM-Govt-Supplier-to-Govt.jpg" - // // uploadOptions - // ); - - // const tags = { - // owner: appealID, - // }; - - // //console.log(tags); - // //await blockBlobClient.setTags(tags); - - // //console.log( - // // `Uploaded block blob ${blobName} successfully`, - // // uploadBlobResponse.requestId - // // ); - // } - - console.log("Blobs:"); - for await (const blob of containerClient.listBlobsFlat()) { - console.log(`- ${blob.name}`); - } - } -); - -export default ShowStorage; diff --git a/pages/storageAdmin.js b/pages/storageAdmin.js deleted file mode 100644 index 9ccec358..00000000 --- a/pages/storageAdmin.js +++ /dev/null @@ -1,759 +0,0 @@ -import CookieBanner from "../components/cookieBanner"; -import Footer from "../components/footer"; -import Header from "../components/header"; -import { DefaultAzureCredential } from "@azure/identity"; -import { BlobServiceClient, ContainerClient } from "@azure/storage-blob"; -import { v4 as uuidv4 } from "uuid"; -import { - createContainerSas, - listContainers, - listBlobHierarchical, - listContainersForUser, -} from "../actions/azurestorage"; -import Head from "next/head"; -import Link from "next/link"; -import { PrismaClient } from "@prisma/client"; -import { - bytesToSize, - getThumbnailIconByExtension, - updateLinks, -} from "../components/utils"; -import { - consoleLogger, - getIP, - sendRepCompleteMessage, - deleteMyRepresentationsFromBlob, - deleteAwaitingSubmissionsFromBlob, -} from "../actions"; -import CryptoJS from "crypto-js"; -import { useState } from "react"; - -function getUpToLastSlash(url) { - let lastSlash = url.lastIndexOf("/"); - - url = url.substring(0, lastSlash); - - url = - url.lastIndexOf("/files") > 0 - ? url.substring(0, url.lastIndexOf("/files")) - : url; - return url; -} - -async function streamToString(readableStream) { - return new Promise((resolve, reject) => { - const chunks = []; - readableStream.on("data", (data) => { - chunks.push(data.toString()); - }); - readableStream.on("end", () => { - resolve(chunks.join("")); - }); - readableStream.on("error", reject); - }); -} - -function formatTimeSince(date) { - if (!date) return "Unknown date"; - - const now = new Date(); - const diffMs = now - date; - const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - - if (diffDays < 7) { - return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`; - } else { - const diffWeeks = Math.floor(diffDays / 7); - return `${diffWeeks} week${diffWeeks !== 1 ? "s" : ""} ago`; - } -} - -function formatBlobDates(blob) { - const created = blob.createdOn ? new Date(blob.createdOn) : null; - const modified = blob.lastModified ? new Date(blob.lastModified) : null; - - if (!created && !modified) return "Unknown"; - - // If both exist and are the same - if (created && modified && created.getTime() === modified.getTime()) { - return `${created.toLocaleString("en-GB")} (${formatTimeSince( - created - )})`; - } - - // If modified exists and is newer than created - if (created && modified && modified.getTime() > created.getTime()) { - return `Created: ${created.toLocaleString("en-GB")} (${formatTimeSince( - created - )}), Modified: ${modified.toLocaleString("en-GB")} (${formatTimeSince( - modified - )})`; - } - - // Only one date exists - const date = modified || created; - return `${date.toLocaleString("en-GB")} (${formatTimeSince(date)})`; -} - -const deleteRepItem = (containerID, caseID, repfile) => { - deleteMyRepresentationsFromBlob(containerID, caseID, repfile).then( - (data) => { - console.log("======== rep deleted ========="); - return data; - } - ); -}; - -// const deleteCaseItem = (containerID, caseID) => { -// deleteAwaitingSubmissionsFromBlob(containerID, caseID).then((data) => { -// return data; -// }); -// }; - -const deleteCaseItem = async (containerID, caseID, setData) => { - const result = await deleteAwaitingSubmissionsFromBlob(containerID, caseID); - - if (result) { - setData((prevData) => - prevData.map((user) => ({ - ...user, - containers: user.containers.map((c) => - c.container === containerID - ? { - ...c, - folders: c.folders.filter( - (f) => f.folderPath !== caseID - ), - } - : c - ), - })) - ); - } -}; - -export default function StoragePage(props) { - const { userData, repCompleteCount } = props; - const [data, setData] = useState(props.data); - const [showRepJson, setShowRepJson] = useState(true); - const [showTmpFile, setShowTmpFile] = useState(true); - const [whichTab, setWhichTab] = useState("storage"); - - const toggleAll = () => { - const newState = !(showRepJson || showTmpFile); - setShowRepJson(newState); - setShowTmpFile(newState); - }; - return ( -
- - Storage Account Browser - -
- -
-
-
-
-

Contents

- -
-

Storage Account Contents

- {repCompleteCount > 0 && ( -

- Number of Orphaned Reps:{" "} - {repCompleteCount} -

- )} - - {/* Toggle buttons */} -
- - - -
- - {data.map((user) => ( -
- {user.containers.map((c) => ( -
-
-

- {c.container} -{" "} - -

- {folder.folderPath || - "Root"} -

- {/* {folder.hasRepJson && ( -

- rep.json file found:{" "} - {folder.repJsonBlob.name} -

- )} */} - {folder.hasTmpFile && ( - <> - - - )} - {folder.hasRepJson && ( -
- {" "} - - {folder.repComplete && ( - - )} - {folder.repComplete && ( - - )} -
- )} - -
    - {folder.blobs.map( - (blob) => ( -
  • - - { - blob.name - }{" "} - ( - {bytesToSize( - blob.contentLength || - blob.size - )} - - ) - {" "} - {blob.lastModified && ( - - {/* { - blob.displayDate - } */} - {formatBlobDates( - blob - )} - - )} -
  • - ) - )} -
-
- ))} -
-
- ))} -
- ))} -
-
-

User Accounts

-

- Number of accounts created:{" "} - {userData.length} -

-
-
-
- Name -
-
- Last accessed -
-
- - {userData.map((user) => ( -
-
- - Last accessed: {user.created}{" "} -
-
- ))} -
-
-
-
-
-
- - ); -} - -export async function getServerSideProps({ req }) { - // Load allowed IPs from env and trim spaces - const ALLOWED_IPS = process.env.ALLOWED_IPS - ? process.env.ALLOWED_IPS.split(",").map((ip) => ip.trim()) - : []; - - // Always allow localhost addresses - const LOCALHOST_IPS = ["127.0.0.1", "::1"]; - - // Get IP address from headers or socket - const forwarded = req.headers["x-forwarded-for"]; - const ip = - typeof forwarded === "string" - ? forwarded.split(",")[0] - : req.socket.remoteAddress; - - console.log("Visitor IP:", ip); - - // Check whitelist + localhost - if (![...ALLOWED_IPS, ...LOCALHOST_IPS].includes(ip)) { - return { - redirect: { - destination: "/403", // custom "Access Denied" page - permanent: false, - }, - }; - } - const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME; - - const creds = new DefaultAzureCredential(); - - const globalForPrisma = global; - - const prisma = globalForPrisma.prisma || new PrismaClient(); - - const blobServiceClient = new BlobServiceClient( - `https://${accountName}.blob.core.windows.net`, - creds - ); - - const users = await prisma.user.findMany(); - - const hashAPIPath = (queryPath, WORDKEY) => { - var hashlink = CryptoJS.HmacSHA256( - queryPath, - CryptoJS.enc.Hex.parse(WORDKEY) - ); - hashlink = hashlink.toString(CryptoJS.enc.Hex); - return hashlink; - }; - - let data = await Promise.all( - users.map(async (user) => { - const containers = await listContainersForUser( - blobServiceClient, - `${user.id}`, - user.email - ); - - const containersWithHashes = await Promise.all( - containers.map(async (container) => { - const groupedByFolder = await container.blobs.reduce( - async (accP, blob) => { - // Because we'll do async read, reduce needs to be async, so accP is a Promise - const acc = await accP; - - const folderPath = - getUpToLastSlash(blob.name.trim()) || ""; - - const hashedfilepath = hashAPIPath( - "/api/file/downloadblob?container=" + - container.container + - "&casefolderID=" + - folderPath + - "&blobname=" + - blob.name - .substring( - blob.name.lastIndexOf("/") + 1 - ) - .trim(), - process.env.HASHKEY - ); - - const fileNameOnly = blob.name.substring( - blob.name.lastIndexOf("/") + 1 - ); - const isTmpFile = fileNameOnly.startsWith("TMP-"); - - const blobWithHash = { - ...blob, - hashedfilepath, - isTmpFile, - }; - - let folderGroup = acc.find( - (f) => f.folderPath === folderPath - ); - if (!folderGroup) { - folderGroup = { - folderPath, - blobs: [], - hasRepJson: false, - repJsonBlob: null, - repComplete: false, - hasTmpFile: false, - }; - acc.push(folderGroup); - } - - folderGroup.blobs.push(blobWithHash); - - if (isTmpFile) { - folderGroup.hasTmpFile = true; - } - - // If this blob ends with rep.json, fetch and parse JSON - if (blob.name.toLowerCase().endsWith("rep.json")) { - folderGroup.hasRepJson = true; - folderGroup.repJsonBlob = blobWithHash; - - // Read blob content from Azure Storage - const containerClient = - blobServiceClient.getContainerClient( - container.container - ); - const blockBlobClient = - containerClient.getBlockBlobClient( - blob.name - ); - - try { - const downloadResponse = - await blockBlobClient.download(0); - const downloaded = await streamToString( - downloadResponse.readableStreamBody - ); - - const json = JSON.parse(downloaded); - folderGroup.repComplete = Boolean( - json.repComplete - ); - } catch (err) { - console.error( - "Error reading or parsing rep.json", - err - ); - folderGroup.repComplete = false; - } - } - - return acc; - }, - Promise.resolve([]) - ); - - return { - ...container, - folders: groupedByFolder, - }; - }) - ); - - return containersWithHashes.length > 0 - ? { - id: user.id, - email: user.email, - containers: containersWithHashes, - } - : null; - }) - ); - - let userData = await Promise.all( - users - .filter((user) => user.email && user.email.trim() !== "") - .map(async (user) => ({ - id: user.id, - email: user.email, - created: user.emailVerified?.toLocaleString("en-GB") || "", - })) - ); - - let repCompleteCount = 0; - - data = data.filter(Boolean); - - data.forEach((user) => { - user.containers.forEach((container) => { - container.folders.forEach((folder) => { - if (folder.repComplete === true) { - repCompleteCount++; - } - }); - }); - }); - - return { - props: { - data: data, - userData: userData, - repCompleteCount, - }, - }; -}