diff --git a/actions/azurestorage.js b/actions/azurestorage.js index e4cedfe4..ea104db1 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -361,7 +361,9 @@ export const createAppealPDFBlob = async ( console.log("blobName:", blobName); const blockBlobClient = containerClient.getBlockBlobClient(blobName); - const uploadBlobResponse = await blockBlobClient.uploadStream(content); + const uploadBlobResponse = Buffer.isBuffer(content) + ? await blockBlobClient.uploadData(content) + : await blockBlobClient.uploadStream(content); console.log(uploadBlobResponse); let whichLocation = "846040000"; // Default value diff --git a/actions/services/documentDirectService.js b/actions/services/documentDirectService.js index d867ce35..567ebc7b 100644 --- a/actions/services/documentDirectService.js +++ b/actions/services/documentDirectService.js @@ -265,20 +265,25 @@ export const generateAppealPDF = async ( containerID, casefolderID, appealType, - fileList + fileList, + options = {} ) => { Object.assign(formValues, { "filesList": fileList }); - var queryUrl = "/api/file/generateappealpdf?appealType=" + appealType; + var queryUrl = + "/api/file/generateappealpdf?appealType=" + + appealType + + (options.download ? "&download=true" : ""); const hashedUrl = await buildHashedQueryUrl(queryUrl); const config = { method: "post", url: hashedUrl, - data: formValues + data: formValues, + ...(options.download ? { responseType: "blob" } : {}) }; try { diff --git a/components/case/representation/representationCompleteSubmit.js b/components/case/representation/representationCompleteSubmit.js index f471dfca..fc819e03 100644 --- a/components/case/representation/representationCompleteSubmit.js +++ b/components/case/representation/representationCompleteSubmit.js @@ -292,7 +292,8 @@ const RepCompleteSubmit = (props) => { const pdfBlob = await generateRepPDF( repFormData, containerID, - caseRef + caseRef, + { download: true } ); if (pdfBlob) { diff --git a/components/newappeal/buildchecksection.js b/components/newappeal/buildchecksection.js index 5c30d0ff..6285306f 100644 --- a/components/newappeal/buildchecksection.js +++ b/components/newappeal/buildchecksection.js @@ -124,6 +124,8 @@ let BuildCheckSection = (props) => { const [confirmSections, setConfirmSections] = useState(false); const [finaliseAppealProcess, setFinaliseAppealProcess] = useState(false); + const [downloadAppealForm, setDownloadAppealForm] = useState(false); + const [downloadInProgress, setDownloadInProgress] = useState(false); const FieldsTranslations = (label) => { const router = useRouter(); @@ -183,6 +185,65 @@ let BuildCheckSection = (props) => { : setConfirmSections(false); }; + const triggerAppealDownload = async () => { + if (downloadInProgress) { + return; + } + + try { + setDownloadInProgress(true); + + const uniqueArray = pdfObj.filesList.filter( + (value, index, self) => + index === self.findIndex((t) => t.name === value.name) + ); + + pdfObj.filesList = uniqueArray; + + console.log(pdfObj); + + const containerID = + props.accountDetails?.containerID || + props.props?.accountDetails?.containerID || + props.props?.props?.accountDetails?.containerID; + const caseRef = + props?.currentView?.caseReference?.ticketnumber || + props?.appealType?.caseReference?.ticketnumber; + + if (!containerID || !caseRef) { + console.error( + "Appeal Form download skipped: missing containerID or case reference", + { containerID, caseRef } + ); + return; + } + + const pdfBlob = await generateAppealPDF( + pdfObj, + containerID, + caseRef, + router.query.appealtypes, + pdfObj.filesList, + { download: true } + ); + + if (pdfBlob) { + const blobUrl = window.URL.createObjectURL(pdfBlob); + const anchor = document.createElement("a"); + anchor.href = blobUrl; + anchor.download = `${"appealForm"}.pdf`; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + window.URL.revokeObjectURL(blobUrl); + } + } catch (error) { + console.error("Appeal Form download failed", error); + } finally { + setDownloadInProgress(false); + } + }; + let ShowDocLinks = getDocLink(docsOffline); return ( @@ -280,6 +341,30 @@ let BuildCheckSection = (props) => { <>
+
+
+ { + setDownloadAppealForm( + e.target.checked + ); + }} + /> + +
+
{ } className="govuk-button" data-module="govuk-button" - onClick={() => { + onClick={async () => { + await triggerAppealDownload(); finaliseAppeal(); }} > diff --git a/components/pdftemplates/appealRow_pdf.js b/components/pdftemplates/appealRow_pdf.js index a0b731bc..9164c508 100644 --- a/components/pdftemplates/appealRow_pdf.js +++ b/components/pdftemplates/appealRow_pdf.js @@ -4,7 +4,7 @@ import xpath from "xpath"; import { bytesToSize, getPickListLabel, - getDocumentTypeFromFilename, + getDocumentTypeFromFilename } from "../../components/utils"; import fieldLookup from "../../data/crmfieldlookuptranslations.json"; @@ -22,7 +22,7 @@ export const AppealRow_pdf = (props) => { sectionCount, onSubmit, updateCurrentSection, - mandatoryFieldsData, + mandatoryFieldsData } = props; const styles = StyleSheet.create({ @@ -30,23 +30,23 @@ export const AppealRow_pdf = (props) => { backgroundColor: "white", padding: 20, fontSize: 16, - paddingBottom: 50, + paddingBottom: 50 }, header: { fontSize: 25, fontFamily: "Helvetica", flexDirection: "row", - justifyContent: "space-between", + justifyContent: "space-between" }, subHeader: { - fontSize: 20, + fontSize: 20 }, logoImage: { width: "200pt" }, questionBullet: { width: "5%", fontSize: 12 }, questionText: { width: "35%", fontSize: 12, - marginRight: 5, + marginRight: 5 }, questionTextMid: { width: "80%", fontSize: 12 }, questionTextWide: { width: "95%", fontSize: 12, marginBottom: 5 }, @@ -60,7 +60,7 @@ export const AppealRow_pdf = (props) => { paddingLeft: 10, backgroundColor: "white", display: "flex", - flexWrap: "wrap", + flexWrap: "wrap" }, questionAnswerMid: { color: "#757575", @@ -72,7 +72,7 @@ export const AppealRow_pdf = (props) => { paddingLeft: 10, backgroundColor: "white", display: "flex", - flexWrap: "wrap", + flexWrap: "wrap" }, questionAnswerWide: { color: "#757575", @@ -81,26 +81,26 @@ export const AppealRow_pdf = (props) => { border: 1, borderColor: "#eee", backgroundColor: "white", - padding: 5, + padding: 5 }, questionAnswerRow: { width: "100%", flex: 1, flexDirection: "row", - flexGrow: 1, + flexGrow: 1 }, questionAnswerColLeft: { - width: "40%", + width: "40%" }, questionAnswerColRight: { padding: 5, - width: "40%", + width: "40%" }, sectionContainer: { backgroundColor: "#F8F8F8", padding: 10, marginTop: 20, - fontSize: 15, + fontSize: 15 }, pageNumber: { position: "absolute", @@ -109,15 +109,15 @@ export const AppealRow_pdf = (props) => { left: 0, right: 0, textAlign: "center", - color: "grey", + color: "grey" }, documentList: { - fontSize: 10, + fontSize: 10 }, richArea: { fontSize: 12, - flexDirection: "column", - }, + flexDirection: "column" + } }); const parser = new DOMParser(); @@ -151,7 +151,7 @@ export const AppealRow_pdf = (props) => { let formObj = jsonpath({ path: "$['" + appealtypes + "']", json: fieldLookup, - eval: true, + eval: true }); let labelTrans = label; @@ -211,7 +211,7 @@ export const AppealRow_pdf = (props) => { @@ -243,7 +243,7 @@ export const AppealRow_pdf = (props) => { @@ -251,7 +251,7 @@ export const AppealRow_pdf = (props) => { wrap={false} style={{ flexDirection: "row", - marginBottom: 10, + marginBottom: 10 }} > { ? fieldCopy.split("|")[1] : fieldCopy; - const transData = require("../../locales/en/" + - (typeof fieldCopy != "undefined" - ? fieldCopy.split(":")[0] - : "common") + - ".json"); + const transData = require( + "../../locales/en/" + + (typeof fieldCopy != "undefined" + ? fieldCopy.split(":")[0] + : "common") + + ".json" + ); let transCopy = typeof fieldCopy != "undefined" @@ -316,7 +318,7 @@ export const AppealRow_pdf = (props) => { @@ -324,7 +326,7 @@ export const AppealRow_pdf = (props) => { wrap={false} style={{ flexDirection: "row", - marginBottom: 10, + marginBottom: 10 }} > { { wrap={false} style={{ flexDirection: "row", - marginBottom: 10, + marginBottom: 10 }} > @@ -420,16 +422,15 @@ export const AppealRow_pdf = (props) => { //const blobList = props.filesList || []; - const blobList = Array.isArray( - props.filesList?.value - ) - ? props.filesList.value - : []; + const blobList = + props.filesList.hasOwnProperty("value") + ? props.filesList.value + : props.filesList || []; return ( @@ -437,7 +438,7 @@ export const AppealRow_pdf = (props) => { wrap={false} style={{ flexDirection: "row", - marginBottom: 10, + marginBottom: 10 }} > { @@ -497,7 +498,7 @@ export const AppealRow_pdf = (props) => { wrap={false} style={{ flexDirection: "row", - marginBottom: 10, + marginBottom: 10 }} > { {(props.props.hasOwnProperty( @@ -549,14 +550,14 @@ export const AppealRow_pdf = (props) => { style={{ flexDirection: "row", - marginBottom: 10, + marginBottom: 10 }} > {tenant?.pinswg_agriculturaltenantname_firstname && @@ -579,7 +580,7 @@ export const AppealRow_pdf = (props) => { Notice @@ -604,7 +605,7 @@ export const AppealRow_pdf = (props) => { @@ -612,7 +613,7 @@ export const AppealRow_pdf = (props) => { wrap={false} style={{ flexDirection: "row", - marginBottom: 10, + marginBottom: 10 }} > { wrap={false} style={{ flexDirection: "row", - marginBottom: 10, + marginBottom: 10 }} > diff --git a/locales/cy/newappeal.json b/locales/cy/newappeal.json index b3e69546..21695e44 100644 --- a/locales/cy/newappeal.json +++ b/locales/cy/newappeal.json @@ -99,6 +99,7 @@ "new-appeal-check-confirm-paragraph-one": "Rwy’n deall y gallwch ddefnyddio’r wybodaeth a roddais at ddibenion swyddogol mewn cysylltiad â Deddf Cynllunio Gwlad a Thref 1990 a gall manylion gan gynnwys fy enw, disgrifiad o’r safle a'm datganiad achos ymddangos ar-lein. Trwy gyflwyno'r ffurflen hon, rwy'n cytuno i'r wybodaeth a roddaf gael ei defnyddio yn y modd hwn.", "new-appeal-check-confirm-paragraph-two": "Mae'r data personol a ddarparwyd gennych ar y ffurflen hon yn cael ei chasglu a'i phrosesu wedi hynny yn unol â thelerau ein cofrestriad o dan Ddeddf Diogelu Data 1998. Mae rhagor o wybodaeth am y Polisi Diogelu Data ar gael yn ", "new-appeal-check-confirm-paragraph-two-link": "https://www.llyw.cymru/hysbysiad-preifatrwydd-llywodraeth-cymru", + "new-appeal-check-download-label": "Hoffwn lawrlwytho copi o'r ffurflen ar gyfer fy nghofnodion neu i'w hanfon ymlaen at yr Awdurdod Cynllunio Lleol.", "new-appeal-complete-heading-1": "Cyflwynwyd yr apêl wedi'i gwblhau", "new-appeal-complete-paragraph-1": "Rydym wedi anfon e-bost cadarnhau atoch.", "new-appeal-complete-heading-2": "Beth fydd yn digwydd nesaf", diff --git a/locales/en/newappeal.json b/locales/en/newappeal.json index f6e31eeb..6548f801 100644 --- a/locales/en/newappeal.json +++ b/locales/en/newappeal.json @@ -99,6 +99,7 @@ "new-appeal-check-confirm-paragraph-one": "I understand that you may use the information I have given for official purposes in connection with the Town and Country Planning Act 1990 and details including my name, the site description and my statement of case may appear online. By submitting this form I am agreeing to the use of the information I provide in this way.", "new-appeal-check-confirm-paragraph-two": "The gathering and subsequent processing of the personal data supplied by you in this form, is in accordance with the terms of our registration under the Data Protection Act 1998. Further information about Data Protection Policy can be found at", "new-appeal-check-confirm-paragraph-two-link": "https://gov.wales/welsh-government-privacy-notice", + "new-appeal-check-download-label": "I wish to download a copy of the form for my records or to forward to the Local Planning Authority.", "new-appeal-complete-heading-1": "Appeal Submission complete", "new-appeal-complete-paragraph-1": "We have sent you a confirmation email.", "new-appeal-complete-heading-2": "What happens next", diff --git a/pages/api/endpoint/gethash_api.js b/pages/api/endpoint/gethash_api.js index 37363f64..1524bce0 100644 --- a/pages/api/endpoint/gethash_api.js +++ b/pages/api/endpoint/gethash_api.js @@ -11,7 +11,13 @@ ApiProxy.get(async (req, res) => { return res.status(401).json(); } - const queryPath = req.query.path; + const rawQueryPath = req.query.path; + + const queryPath = + typeof rawQueryPath === "string" + ? rawQueryPath.split("?")[0] + : rawQueryPath; + const allowedPrefix = [ "/api/endpoint/getportallogin_api", "/api/endpoint/deletemyrepresentations_api", @@ -36,7 +42,7 @@ ApiProxy.get(async (req, res) => { return res.status(400).json(); } - return res.status(200).json({ hash: hashAPIPath(queryPath) }); + return res.status(200).json({ hash: hashAPIPath(rawQueryPath) }); }); export default ApiProxy; diff --git a/pages/api/file/generateappealpdf.js b/pages/api/file/generateappealpdf.js index ae796ee5..5915501a 100644 --- a/pages/api/file/generateappealpdf.js +++ b/pages/api/file/generateappealpdf.js @@ -113,6 +113,7 @@ import { other_pdf } from "../../../components/pdftemplates/other_pdf"; export default async function handler(req, res) { var checkHash = req.query.hash; var appealType = req.query.appealType; + var isDownload = req.query?.download === "true"; let appealBodyObj = typeof req.body === "string" ? JSON.parse(req.body) : req.body; @@ -134,7 +135,11 @@ export default async function handler(req, res) { return res.status(400).json(); } - checkquerypath = checkquerypath + "?appealType=" + appealType; + checkquerypath = + checkquerypath + + "?appealType=" + + appealType + + (isDownload ? "&download=true" : ""); if (hashAPIPath(checkquerypath) != "&hash=" + checkHash) { return res.status(400).json(); @@ -196,6 +201,19 @@ export default async function handler(req, res) { // ReactPDF.renderToStream(); + const streamToBuffer = async (readableStream) => { + return new Promise((resolve, reject) => { + const chunks = []; + readableStream.on("data", (chunk) => { + chunks.push( + Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) + ); + }); + readableStream.on("end", () => resolve(Buffer.concat(chunks))); + readableStream.on("error", reject); + }); + }; + const renderedPDF = await ReactPDF.renderToStream( ); @@ -214,11 +232,22 @@ export default async function handler(req, res) { ("0" + day).slice(-2) + "_-_Appeal_Form"; + const renderedPDFBuffer = await streamToBuffer(renderedPDF); + await createAppealPDFBlob( - renderedPDF, + renderedPDFBuffer, containerID, blobProgress.pinswg_name || blobProgress.caseObj.ticketnumber ).then((data) => { + if (isDownload) { + const filename = `${pdfFileName}.pdf`; + res.setHeader("Content-Type", "application/pdf"); + res.setHeader( + "Content-Disposition", + `attachment; filename="${filename}"` + ); + return res.status(200).send(renderedPDFBuffer); + } return res.status(200).json({ status: "success", data: data,