diff --git a/.env.local b/.env.local index bda06a64..37374e1a 100644 --- a/.env.local +++ b/.env.local @@ -105,9 +105,12 @@ AZURE_CLIENT_ID = $CLIENT_ID AZURE_TENANT_ID = $TENANT AZURE_CLIENT_SECRET = $CLIENT_SECRET AZURE_STORAGE_ACCOUNT_NAME= "pedwdev" +AZURE_STORAGE_ACCOUNT_KEY = JS3ZIXianhYhB1lZrHB+bXbdzaLlON0alCyKrFAb+jVEwr9iqo2dty92EBxwnufNFM+CSN/fKX2R+AStDn0byg== + AZURE_PEDW_STORAGE_ENDPOINT = https://pedwdev.blob.core.windows.net AZURE_PEDW_CONTAINER = "pedwapplications" +AZURE_PEDW_QUEUE_ENDPOINT = https://$AZURE_STORAGE_ACCOUNT_NAME.queue.core.windows.net diff --git a/actions/azurestorage.js b/actions/azurestorage.js index e1d33ea6..ff4b7302 100644 --- a/actions/azurestorage.js +++ b/actions/azurestorage.js @@ -1,4 +1,6 @@ -import { v4 as uuidv4 } from "uuid"; +import { DefaultAzureCredential } from "@azure/identity"; +import _ from "lodash"; +import { hashAPIPath } from "."; const { ContainerClient, BlockBlobClient, @@ -6,13 +8,25 @@ const { BlobSASPermissions, ContainerSASPermissions, generateBlobSASQueryParameters, + SASProtocol, } = require("@azure/storage-blob"); -import { DefaultAzureCredential } from "@azure/identity"; -import { consoleLogger, conLog, hashAPIPath } from "."; -import _ from "lodash"; +const { + QueueServiceClient, + AccountSASResourceTypes, + AccountSASServices, + AccountSASPermissions, + QueueSASPermissions, + QueueSASSignatureValues, + generateAccountSASQueryParameters, + generateQueueSASQueryParameters, + StorageSharedKeyCredential, + QueueClient, +} = require("@azure/storage-queue"); const STORAGE_PATH = process.env.AZURE_PEDW_STORAGE_ENDPOINT; const STORAGE_CONTAINER = process.env.AZURE_PEDW_CONTAINER; +const QUEUE_PATH = process.env.AZURE_PEDW_QUEUE_ENDPOINT; + const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME; export const createContainerSas = async (containerName) => { @@ -611,8 +625,6 @@ export const getProgressBlobs = async (containerName, caseReference) => { }, ]).reverse()[0]; - //console.log("blobObjwwwww:", blobObj); - return blobObj; }; @@ -675,13 +687,124 @@ export const getRepslobs = async (containerName, caseReference) => { blobObj.push(blob); } - // blobObj = _.sortBy(blobObj, [ - // function (o) { - // return o.lastModified; - // }, - // ]).reverse()[0]; - - console.log("blobObjwwwww:", blobObj); + //console.log("blobObjwwwww:", blobObj); return blobObj; }; + +export const createQueueSas = async (queueName) => { + // Get environment variables + const account = process.env.AZURE_STORAGE_ACCOUNT_NAME; + const accountKey = process.env.AZURE_STORAGE_ACCOUNT_KEY; + + const sharedKeyCredential = new StorageSharedKeyCredential( + account, + accountKey + ); + + // Best practice: create time limits + const TEN_MINUTES = 10 * 60 * 1000; + const NOW = new Date(); + + // Best practice: set start time a little before current time to + // make sure any clock issues are avoided + const TEN_MINUTES_AFTER_NOW = new Date(NOW.valueOf() + TEN_MINUTES); + + var resource_types = new AccountSASResourceTypes(); + resource_types.container = true; + resource_types.object = true; + resource_types.service = true; + + var permission = new QueueSASPermissions(); + permission.read = true; + permission.write = true; + permission.delete = true; + permission.list = true; + permission.add = true; + permission.update = true; + permission.process = true; + + var sas_signature_val = { + expiresOn: TEN_MINUTES_AFTER_NOW, + permissions: permission, + queueName: queueName, + }; + + const sasToken = generateQueueSASQueryParameters( + sas_signature_val, + sharedKeyCredential + ); + //console.log("sass token:", sasToken); + + return sasToken.toString(); +}; + +export const createCaseCompleteMessage = async ( + containerName, + caseReference +) => { + const whichQueue = "pedw-submitted-applications"; + const queueToken = await createQueueSas(whichQueue); + + const sasUrl = `${QUEUE_PATH}?${queueToken}`; + const queueServiceClient = new QueueServiceClient(sasUrl); + + const message = { + containerName: containerName, + casepath: + containerName + + "/" + + caseReference + + "/" + + caseReference + + "_appeal.json", + casecontentpath: containerName + "/" + caseReference + "/case", + filespath: containerName + "/" + caseReference + "/files", + + // uploadUrl: fileRecord.uploadUrl, + // filename: fileRecord.file.name, + // fileSize: fileRecord.file.size, + }; + + const sendMessageResponse = await queueServiceClient + .getQueueClient(whichQueue) + .sendMessage( + JSON.stringify( + `${JSON.stringify( + message + )}` + ) + ); + + return sendMessageResponse; +}; + +export const createRepCompleteMessage = async ( + containerName, + caseReference +) => { + const whichQueue = "pedw-submitted-representations"; + const queueToken = await createQueueSas(whichQueue); + + const sasUrl = `${QUEUE_PATH}?${queueToken}`; + const queueServiceClient = new QueueServiceClient(sasUrl); + + const message = { + containerName: containerName, + caseref: caseReference, + reppath: containerName + "/" + caseReference + "/", + filespath: containerName + "/" + caseReference + "/files", + }; + + const sendMessageResponse = await queueServiceClient + .getQueueClient(whichQueue) + .sendMessage( + JSON.stringify( + `${JSON.stringify( + message + )}` + ) + ); + + return sendMessageResponse; +}; diff --git a/actions/index.js b/actions/index.js index 660bf3bf..0f439d7c 100644 --- a/actions/index.js +++ b/actions/index.js @@ -1491,3 +1491,49 @@ export const getPortalLogin = (emailAddress) => { return JSON.stringify(error); }); }; + +export const sendCaseCompleteMessage = async (containerID, caseReference) => { + var queryUrl = + "/api/file/createappealcompletemessage_api?container=" + + containerID + + "&tempcaseref=" + + caseReference; + + var config = { + method: "get", + url: queryUrl, + }; + + //console.log(data); + + return axios(config) + .then((res) => { + return res.data; + }) + .catch((error) => { + //console.log("API call error", error); + }); +}; + +export const sendRepCompleteMessage = async (containerID, caseReference) => { + var queryUrl = + "/api/file/createrepcompletemessage_api?container=" + + containerID + + "&tempcaseref=" + + caseReference; + + var config = { + method: "get", + url: queryUrl, + }; + + //console.log(data); + + return axios(config) + .then((res) => { + return res.data; + }) + .catch((error) => { + //console.log("API call error", error); + }); +}; diff --git a/components/case/representation/index.js b/components/case/representation/index.js index 685d2a44..d8aaa898 100644 --- a/components/case/representation/index.js +++ b/components/case/representation/index.js @@ -19,6 +19,8 @@ import { setRepresentationSubmit, setSubmitBack, setRepresentationSubmitConfirmation, + setRepresentationMessageSent, + setRepresentationReset, } from "../../../store/currentView/action"; import RepCapacitySelection from "./representationCapacitySelection"; import RepDetails from "./representationDetails"; @@ -65,6 +67,7 @@ let MakeRepresentation = (props) => { setSubmitBack, handleSubmit, setRepresentationSubmitConfirmation, + setRepresentationMessageSent, } = props; const formObj = props.props.form; let setCaseQueryObj = props[currentType] || {}; @@ -388,9 +391,13 @@ let MakeRepresentation = (props) => { setRepresentationSubmit={setRepresentationSubmit} currentView={currentView} setSubmitBack={setSubmitBack} + setRepresentationMessageSent={ + setRepresentationMessageSent + } setRepresentationSubmitConfirmation={ setRepresentationSubmitConfirmation } + props={props} /> ) : ( <> @@ -453,6 +460,12 @@ const mapDispatchToProps = (dispatch) => { setRepresentationSubmitConfirmation: () => { dispatch(setRepresentationSubmitConfirmation(true)); }, + setRepresentationMessageSent: (messageSent) => { + dispatch(setRepresentationMessageSent(messageSent)); + }, + setRepresentationReset: () => { + dispatch(setRepresentationReset()); + }, }; }; diff --git a/components/case/representation/representationComplete.js b/components/case/representation/representationComplete.js index 6900d915..4dff8f5b 100644 --- a/components/case/representation/representationComplete.js +++ b/components/case/representation/representationComplete.js @@ -1,19 +1,11 @@ +import useTranslation from "next-translate/useTranslation"; import Link from "next/link"; import { useRouter } from "next/router"; -import useTranslation from "next-translate/useTranslation"; -import { Field, reduxForm } from "redux-form"; -import { - RenderPickList, - RenderTextfield, - RenderRadioList, - RenderMultiline, -} from "./representationElements"; -import { - setRepresentationCapacity, - setRepresentationSubmit, - setSubmitBack, -} from "../../../store/currentView/action"; +import { useEffect } from "react"; + import { useDropzone } from "react-dropzone"; +import { sendEmail, sendRepCompleteMessage } from "../../../actions"; +import { setRepresentationReset } from "../../../store/currentView/action"; const RepComplete = (props) => { let { t } = useTranslation(); @@ -28,6 +20,7 @@ const RepComplete = (props) => { currentView, setSubmitBack, setRepresentationSubmitConfirmation, + setRepresentationMessageSent, } = props; const { acceptedFiles, getRootProps, getInputProps } = useDropzone(); const files = acceptedFiles.map((file) => ( @@ -36,6 +29,28 @@ const RepComplete = (props) => { )); + useEffect(() => { + const reference = "PEDW-REPRESENTATION-COMPLETE"; + const templateId = "c49785e7-3217-40b6-b33f-dffc4e58d5c1"; + const emailAddress = + props.props.props.props.accountDetails.accountDetails.emailaddress1; + const personalisation = { + "caseReference": + props.props.currentView.caseReference.currentReference, + "emailAddress": + props.props.props.props.accountDetails.accountDetails + .emailaddress1, + "linkExpiry": 10 * 60, + }; + + props.props.currentView.representationMessageSent != true && + (sendRepCompleteMessage( + props.props.props.props.accountDetails.containerID, + props.props.currentView.caseReference.currentReference + ), + sendEmail(templateId, emailAddress, personalisation, reference), + setRepresentationMessageSent(true)); + }); return (
@@ -64,7 +79,12 @@ const RepComplete = (props) => {
- Continue + setRepresentationReset()} + className="govuk-button" + > + Continue +
diff --git a/components/case/representation/representationCompleteSubmit.js b/components/case/representation/representationCompleteSubmit.js index 9630e02e..ac3d741e 100644 --- a/components/case/representation/representationCompleteSubmit.js +++ b/components/case/representation/representationCompleteSubmit.js @@ -1,22 +1,8 @@ -import Link from "next/link"; -import { useRouter } from "next/router"; +import _ from "lodash"; import useTranslation from "next-translate/useTranslation"; -import { Field, reduxForm } from "redux-form"; -import { - RenderPickList, - RenderTextfield, - RenderRadioList, - RenderMultiline, -} from "./representationElements"; -import { - setRepresentationCapacity, - setRepresentationSubmit, - setSubmitBack, - setRepresentationSubmitConfirmation, -} from "../../../store/currentView/action"; +import { useRouter } from "next/router"; import { useDropzone } from "react-dropzone"; import RepComplete from "./representationComplete"; -import _ from "lodash"; const RepCompleteSubmit = (props) => { let { t } = useTranslation(); @@ -31,6 +17,8 @@ const RepCompleteSubmit = (props) => { currentView, setSubmitBack, setRepresentationSubmitConfirmation, + setRepresentationMessageSent, + setRepresentationReset, } = props; const { acceptedFiles, getRootProps, getInputProps } = useDropzone(); const files = acceptedFiles.map((file) => ( @@ -44,7 +32,11 @@ const RepCompleteSubmit = (props) => { return ( <> {currentView.representationSubmitConfirmation == true ? ( - + ) : (
diff --git a/components/myportal/topthree_reps.js b/components/myportal/topthree_reps.js index 93dbce5d..ed2080a5 100644 --- a/components/myportal/topthree_reps.js +++ b/components/myportal/topthree_reps.js @@ -1,27 +1,24 @@ -import Link from "next/link"; -import _ from "lodash"; -import { useRouter } from "next/router"; import useTranslation from "next-translate/useTranslation"; -import { setCurrentReference } from "../../store/currentView/action"; -import { connect } from "react-redux"; +import Link from "next/link"; +import { useRouter } from "next/router"; import { parseCookies } from "nookies"; +import { connect } from "react-redux"; import { - getWatchedCasesProxy, - getPortalModuleDetailsProxy, - deleteWatchedCases, deleteAwaitingSubmissions, + deleteWatchedCases, getAwaitingSubmissionProxy, + getPortalModuleDetailsProxy, + getWatchedCasesProxy, } from "../../actions"; -import jsonpath from "jsonpath"; -import transLookup from "../../data/lookuptranslations.json"; -import { - setWatchedCases, - setWatchedCasesDetails, -} from "../../store/watchedCases/action"; import { setAwaitingSubmission, setAwaitingSubmissionDetails, } from "../../store/awaitingSubmission/action"; +import { setCurrentReference } from "../../store/currentView/action"; +import { + setWatchedCases, + setWatchedCasesDetails, +} from "../../store/watchedCases/action"; import { getFormCollectionByID } from "../utils"; const TopThree = (props) => { @@ -139,8 +136,16 @@ const TopThree = (props) => {
diff --git a/components/newappeal/complete.js b/components/newappeal/complete.js index 7d915d0a..25e2aaba 100644 --- a/components/newappeal/complete.js +++ b/components/newappeal/complete.js @@ -1,13 +1,15 @@ -import jsonpath from "jsonpath"; import useTranslation from "next-translate/useTranslation"; import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect } from "react"; import { connect } from "react-redux"; import { formValueSelector } from "redux-form"; -import xpath from "xpath"; -import { updateCase, patchCase, sendEmail } from "../../actions"; -import data from "../../data/collections.json"; +import { + patchCase, + sendCaseCompleteMessage, + sendEmail, + updateCase, +} from "../../actions"; import { setCurrentSection } from "../../store/appealType/action"; import { getFormCollectionByID } from "../utils"; @@ -60,6 +62,11 @@ let CompleteAppeal = (props) => { }; sendEmail(templateId, emailAddress, personalisation, reference); + sendCaseCompleteMessage( + props.appealType.caseReference.ticketnumber, + props.props.accountDetails.loggedinUserEmail + ); + console.log("patching////////"); patchCase(incidentId); }, [ @@ -135,13 +142,6 @@ let CompleteAppeal = (props) => { Back to My Portal

- -

- - What did you think of this service? - - (takes 30 seconds) -

); diff --git a/package.json b/package.json index 5885aa2e..bcb4c370 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@azure/identity": "^2.0.5", "@azure/storage-blob": "^12.12.0", + "@azure/storage-queue": "^12.11.0", "@googlemaps/react-wrapper": "^1.1.24", "@magic-sdk/admin": "^1.4.0", "@next-auth/prisma-adapter": "^1.0.4", diff --git a/pages/api/email/notify.js b/pages/api/email/notify.js index 2a0d5d9b..65235bc0 100644 --- a/pages/api/email/notify.js +++ b/pages/api/email/notify.js @@ -8,6 +8,8 @@ export default async function ApiProxy(req, res) { const notifyClient = new NotifyClient(process.env.NOTIFY_API_KEY); //const emailReplyToId = process.env.EMAIL_REPLY_TO_ID; + console.log("///////////////\n Sending email \ns//////////////"); + notifyClient .sendEmail(data.templateId, data.emailAddress, { personalisation: data.personalisation, diff --git a/pages/api/file/createappealcompletemessage_api.js b/pages/api/file/createappealcompletemessage_api.js new file mode 100644 index 00000000..390a4ddc --- /dev/null +++ b/pages/api/file/createappealcompletemessage_api.js @@ -0,0 +1,37 @@ +import { hashAPIPath } from "../../../actions"; +import { + getBlobs, + createCaseCompleteMessage, +} from "../../../actions/azurestorage"; + +import nextConnect from "next-connect"; +import middleware from "../middleware/middleware"; + +const ApiProxy = nextConnect(); +ApiProxy.use(middleware); + +ApiProxy.get(async (req, res) => { + var containerName = req.query.container; + var tempCaseRef = req.query.tempcaseref; + + console.log("/////Create Case Message:\n", tempCaseRef, "\n//////////////"); + + // console.log(hashAPIPath(checkquerypath), checkHash); + // console.log(hashAPIPath(checkquerypath) == "&hash=" + checkHash); + + //if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { + await createCaseCompleteMessage(containerName, tempCaseRef).then((data) => { + return res.status(200).json(data); + }); + // } else { + // return res.status(400).json(); + // } +}); + +export const config = { + api: { + bodyParser: false, + }, +}; + +export default ApiProxy; diff --git a/pages/api/file/createrepcompletemessage_api.js b/pages/api/file/createrepcompletemessage_api.js new file mode 100644 index 00000000..1edfb765 --- /dev/null +++ b/pages/api/file/createrepcompletemessage_api.js @@ -0,0 +1,41 @@ +import { hashAPIPath } from "../../../actions"; +import { + getBlobs, + createRepCompleteMessage, +} from "../../../actions/azurestorage"; + +import nextConnect from "next-connect"; +import middleware from "../middleware/middleware"; + +const ApiProxy = nextConnect(); +ApiProxy.use(middleware); + +ApiProxy.get(async (req, res) => { + var containerName = req.query.container; + var tempCaseRef = req.query.tempcaseref; + + console.log( + "/////Create Rep complete Message:\n", + tempCaseRef, + "\n//////////////" + ); + + // console.log(hashAPIPath(checkquerypath), checkHash); + // console.log(hashAPIPath(checkquerypath) == "&hash=" + checkHash); + + //if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { + await createRepCompleteMessage(containerName, tempCaseRef).then((data) => { + return res.status(200).json(data); + }); + // } else { + // return res.status(400).json(); + // } +}); + +export const config = { + api: { + bodyParser: false, + }, +}; + +export default ApiProxy; diff --git a/pages/api/file/getrepsblob.js b/pages/api/file/getrepsblob.js index 008f86a9..215b12ce 100644 --- a/pages/api/file/getrepsblob.js +++ b/pages/api/file/getrepsblob.js @@ -27,7 +27,7 @@ ApiProxy.get(async (req, res) => { //if (hashAPIPath(checkquerypath) == "&hash=" + checkHash) { const blobObj = await getRepslobs(containerName, casefolderID) .then((data) => { - console.log("ertyuikjhgfg", data); + console.log("reps blob", data); return downloadAllRepsFiles(containerName, data); }) diff --git a/store/currentView/action.js b/store/currentView/action.js index 6d7be4c1..9d6601bd 100644 --- a/store/currentView/action.js +++ b/store/currentView/action.js @@ -8,6 +8,8 @@ export const currentViewActionTypes = { SETCURRENTLINKEDCASES: "SETCURRENTLINKEDCASES", SETCURRENTPAGE: "SETCURRENTPAGE", SETSHOWREPS: "SETSHOWREPS", + SETREPRESENTATIONMESSAGESENT: "SETREPRESENTATIONMESSAGESENT", + SETREPRESENTATIONRESET: "SETREPRESENTATIONRESET", }; export const getCurrentViewObj = () => (dispatch) => { @@ -60,6 +62,24 @@ export const setRepresentationSubmitConfirmation = }); }; +export const setRepresentationMessageSent = + (representationMessageSent) => (dispatch) => { + return dispatch({ + type: currentViewActionTypes.SETREPRESENTATIONMESSAGESENT, + representationMessageSent: representationMessageSent, + }); + }; + +export const setRepresentationReset = () => (dispatch) => { + return dispatch({ + type: currentViewActionTypes.SETREPRESENTATIONRESET, + representationCapacity: {}, + representationSubmit: "", + representationSubmitConfirmation: {}, + representationMessageSent: false, + }); +}; + export const setCurrentLinkedCases = (linkedCaseReferences) => (dispatch) => { return dispatch({ type: currentViewActionTypes.SETCURRENTLINKEDCASES, diff --git a/store/currentView/reducer.js b/store/currentView/reducer.js index 6cd5e550..b5a8a8be 100644 --- a/store/currentView/reducer.js +++ b/store/currentView/reducer.js @@ -6,6 +6,7 @@ const currentViewInitialState = { representationCapacity: {}, representationSubmit: "", representationSubmitConfirmation: {}, + representationMessageSent: false, linkedCaseReferences: {}, currentPage: 1, showReps: false, @@ -45,6 +46,20 @@ export default function reducer(state = currentViewInitialState, action) { representationSubmitConfirmation: action.representationSubmitConfirmation, }; + case currentViewActionTypes.SETREPRESENTATIONMESSAGESENT: + return { + ...state, + representationMessageSent: action.representationMessageSent, + }; + + case currentViewActionTypes.SETREPRESENTATIONRESET: + return { + ...state, + representationCapacity: {}, + representationSubmit: "", + representationSubmitConfirmation: {}, + representationMessageSent: false, + }; case currentViewActionTypes.SETCURRENTPAGE: return { ...state, diff --git a/styles/sass/patterns/_card.scss b/styles/sass/patterns/_card.scss index 452eddf4..2e061087 100644 --- a/styles/sass/patterns/_card.scss +++ b/styles/sass/patterns/_card.scss @@ -12,10 +12,12 @@ @media screen and (min-width: 600px) and (max-width: 900px) { .card { flex-basis: 45.99%; + // Every card in the first row needs horizontal margin &:nth-child(2n-1) { margin-right: 3%; } + // Every card in the second row needs horizontal margin &:nth-child(2n-2) { margin-left: 3%; @@ -40,6 +42,7 @@ &:nth-child(2n-2) { margin-left: 3%; } + &.card-3 { flex-basis: 31.1%; min-height: 300px; @@ -87,15 +90,18 @@ html[data-useragent*="MSIE 10.0"] .flex-container { *:last-child { margin-bottom: 0; } + a, a:active, a:visited { color: $blue; font-weight: bold; text-decoration: none; + &:hover { color: $lightblue; } + &.govuk-button { color: $white; // &:focus { @@ -117,6 +123,7 @@ html[data-useragent*="MSIE 10.0"] .flex-container { .card-heading a { color: $govuk-link-colour; margin-top: 0; + &:focus { color: govuk-colour("black", $legacy: "black"); outline: 3px solid transparent; @@ -178,6 +185,7 @@ html[data-useragent*="MSIE 10.0"] .flex-container { } } } + &.clear { background-color: govuk-colour("white", $legacy: "white"); border: none; @@ -252,12 +260,13 @@ div.cardModuleItem:last-of-type { .cardModuleReference { display: flex; + b { min-width: 122px; + margin-right: 10px; } - a { - margin-left: 10px; - } + + a {} } .cardModuleRemoveCase { @@ -299,4 +308,4 @@ div.cardModuleItem:last-of-type { // box-shadow: 0 2px 0 #0b0c0c; } } -} +} \ No newline at end of file