Merged PR 1645: 14949 activity from contact us

Related work items: #16290, #16291, #16938
This commit is contained in:
Robert Bond
2025-05-27 17:16:21 +00:00
7 changed files with 767 additions and 15 deletions
+40 -14
View File
@@ -1896,22 +1896,30 @@ export const sendEmail = async (
// personalisation,
// reference
// );
var queryUrl = "/api/email/notify";
// var queryUrl = "/api/email/notify";
var config = {
method: "post",
url: queryUrl,
data: mailData,
};
// var config = {
// method: "post",
// url: queryUrl,
// data: mailData,
// };
return axios(config)
.then((res) => {
//console.log("posted ", res.data);
return res.data;
})
.catch((error) => {
consoleLogger(error);
});
// return axios(config)
// .then((res) => {
// //console.log("posted ", res.data);
// return res.data;
// })
// .catch((error) => {
// consoleLogger(error);
// });
try {
const response = await axios.post("/api/email/notify", mailData);
return response.data;
} catch (error) {
consoleLogger(error); // Optional custom logging
throw error; // ✅ Ensure the calling function sees the error
}
};
export const getNotice = () => {
@@ -2117,3 +2125,21 @@ export const getAddressSearch = async (searchString) => {
return ErrResponse;
});
};
export const createCRMTask = async (formValues) => {
var queryUrl = "/api/endpoint/createcrmtask_api";
var data = formValues;
var config = {
method: "post",
url: queryUrl,
data: data,
};
try {
const res = await axios(config);
return res.data;
} catch (error) {
consoleLogger(error);
}
};
+1 -1
View File
@@ -60,6 +60,7 @@ const Header = (props) => {
"/myportal/searchresults",
"/myportal/representation",
"/myportal/viewall",
"/contactus",
];
const hasContactLink = [
@@ -280,7 +281,6 @@ const Header = (props) => {
}}
>
{" "}
wwww
{t("common:available-in")}
</Link>
</div>
+10
View File
@@ -1,5 +1,6 @@
import useTranslation from "next-translate/useTranslation";
import { useRouter } from "next/router";
import Link from "next/link";
export default function CaseComment() {
let { t } = useTranslation();
@@ -24,6 +25,15 @@ export default function CaseComment() {
<p className="govuk-body-s">
{t("home:casescomment-card-paragraph-two")}
</p>
<div className="govuk-form-group">
<Link
href={router.locale == "cy" ? "/cyswllt" : "/contactus"}
className="govuk-button withChevron govuk-button-cta"
>
Contact us
</Link>
</div>
</div>
</div>
);
+12
View File
@@ -171,6 +171,18 @@ let CaseSearch = (props) => {
>
{t("myportal:searchcases-card-advanced-link")}
</Link>
<div className="govuk-form-group govuk-!-margin-top-5">
<Link
href={
router.locale == "cy"
? "/cyswllt"
: "/contactus"
}
className="govuk-button withChevron govuk-button-cta"
>
Contact us
</Link>
</div>
</div>
</form>
</div>
+1
View File
@@ -11,6 +11,7 @@ module.exports = {
"pages": {
"*": ["common", "search"],
"/": ["common", "home", "case"],
"/contactus": ["newappeal", "account"],
"/error": ["common", "home", "myportal"],
"/furtherdetails": ["common", "home", "myportal"],
"/myportal": ["myportal", "common", "home", "case"],
+188
View File
@@ -0,0 +1,188 @@
/**
* @swagger
* /api/endpoint/createcase_api:
* post:
* tags: [Case]
* description: Create case
* summary: Phase 2
* parameters:
* - name: body
* in: requestbody
* required: true
* example: {}
* schema:
* type: object
* - name: contactid
* in: query
* required: true
* example: pinswg_planningappeals78s
* schema:
* type: string
* - name: appealTypeId
* in: query
* required: true
* example: pinswg_planningappeals78id
* schema:
* type: string
* - name: containername
* in: query
* required: true
* example: pinswg_planningappeals78id
* schema:
* type: string
* - name: lpaID
* in: query
* required: true
* example: pinswg_planningappeals78id
* schema:
* type: string
* responses:
* 200:
* description: Success
*/
import axios from "axios";
import CryptoJS from "crypto-js";
import _ from "lodash";
import { getToken } from "../../../actions";
import { getCaseBlob } from "../../../actions/azurestorage";
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 createTaskBody = req.body;
var queryUrl = "tasks";
var token = await getToken();
let contactValue = createTaskBody.typeOfContact;
// const whichTeam = (contactValue) => {
// switch (contactValue) {
// case "plq":
// //Questions about Planning legislation or policy
// //PD - Via Email - Planning.Directorate@gov.wales
// return;
// break;
// case "ipq":
// //Questions or comments about applications for major infrastructure projects
// //Infrastructure
// return "7fb9e598-7d26-ec11-a97f-00224800e98c";
// break;
// case "pcpq":
// //Questions or comments about planning and environmental appeals
// //Casework
// return "8ffabad9-7c26-ec11-a97f-00224800e98c";
// break;
// case "pi":
// //Query about using the Casework Portal
// // Digital Support Team - PEDW
// return "5554882f-4606-ec11-a97f-00224800d374";
// break;
// case "appadvice":
// //Advice How to submit an appeal
// //R&D
// return "5554882f-4606-ec11-a97f-00224800d374";
// break;
// case "repadvice":
// //Advice How to comment on an appeal or application
// //R&D
// return "5554882f-4606-ec11-a97f-00224800d374";
// break;
// case "ceq":
// //Queries about forthcoming Hearing or Inquiry event
// //Inspector Scheduling
// return "4fddd7e8-7d26-ec11-a97f-00224800e98c";
// break;
// case "other":
// //Other enquiry for PEDW
// //Casework
// return "8ffabad9-7c26-ec11-a97f-00224800e98c";
// break;
// case "postdec":
// //Issues or questions about a case after the decision is issued
// //SLT Team
// return "cc3d987e-7c26-ec11-a97f-00224800e98c";
// break;
// case "comp":
// //General Complaints of Compliments
// //SLT Team
// return "cc3d987e-7c26-ec11-a97f-00224800e98c";
// break;
// default:
// // code block
// }
// };
// let teamId = whichTeam(contactValue);
const teamMap = {
ipq: "7fb9e598-7d26-ec11-a97f-00224800e98c", // Infrastructure
pcpq: "8ffabad9-7c26-ec11-a97f-00224800e98c", // Casework
pi: "5554882f-4606-ec11-a97f-00224800d374", // Digital Support Team
appadvice: "5554882f-4606-ec11-a97f-00224800d374",
repadvice: "5554882f-4606-ec11-a97f-00224800d374",
ceq: "4fddd7e8-7d26-ec11-a97f-00224800e98c", // Inspector Scheduling
other: "8ffabad9-7c26-ec11-a97f-00224800e98c",
postdec: "cc3d987e-7c26-ec11-a97f-00224800e98c", // SLT Team
comp: "cc3d987e-7c26-ec11-a97f-00224800e98c",
};
let teamId = teamMap[contactValue];
const payload = {
"subject": `Contact Us Enquiry - ${createTaskBody.contactSubject}`,
"description": `From: ${createTaskBody.contactEmail}\n\n${createTaskBody.contactbody}`,
"scheduledstart": new Date().toISOString(),
"scheduledend": new Date(
new Date().getTime() + 60 * 60000 * 24
).toISOString(), // 30 mins
...(teamId && { "ownerid@odata.bind": `/teams(${teamId})` }),
};
//console.log("/////Create Task:\n", payload, "\n//////////////");
var config = {
method: "post",
url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl),
headers: {
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
"Accept": "application/json",
"Prefer": 'odata.include-annotations="*",return=representation',
"Authorization": "Bearer " + token.access_token,
"Content-Type": "application/json",
},
data: JSON.stringify(payload),
};
//console.log("/////Create Task config:\n", config, "\n//////////////");
try {
const { data } = await axios(config);
return res.status(200).json(data);
} catch (error) {
console.error(
"CRM Task Creation Error:",
error.response?.data || error
);
return res
.status(400)
.json({ error: "Failed to create CRM task", details: error });
}
}
+515
View File
@@ -0,0 +1,515 @@
import useTranslation from "next-translate/useTranslation";
import Head from "next/head";
import { useRouter } from "next/router";
import Breadcrumbs from "../components/breadcrumbs";
import CookieBanner from "../components/cookieBanner";
import Footer from "../components/footer";
import Header from "../components/header";
import ServiceBanner from "../components/servicebanner";
import Link from "next/link";
import { PickList } from "../components/elements";
import {
Field,
change,
formValueSelector,
reduxForm,
touch,
SubmissionError,
} from "redux-form";
import { connect, submitError } from "react-redux";
import { createCRMTask, sendEmail } from "../actions";
import { useState } from "react";
const RenderPickList = ({
datafieldname,
picklistData,
id,
name,
label,
input,
errorMsg,
meta: { touched, error },
}) => {
let { t } = useTranslation();
const required = (value) => {
return value || value == 0
? undefined
: t("newappeal:is-required-label");
};
return (
<>
<div
className={
touched && error
? "govuk-form-group govuk-!-margin-bottom-1 govuk-form-group--error"
: "govuk-form-group govuk-!-margin-bottom-1"
}
>
<label className="govuk-label " htmlFor={id} id={id + "_label"}>
{label}
</label>
{touched && error && (
<span id={id + "-error"} className="govuk-error-message">
<span className="govuk-visually-hidden">Error:</span>{" "}
{error}
</span>
)}
<select
{...input}
className="govuk-select govuk-input--width-20"
id={id}
name={id}
>
<option value="" disabled defaultValue>
{t("newappeal:select-default")}
</option>
<option value="plq">Planning legislation query</option>
<option value="ipq">Infrastructure procedure query</option>
<option value="pcpq">
Planning casework procedure query
</option>
<option value="pi">Portal Issues</option>
<option value="appadvice">Appeal submission advice</option>
<option value="repadvice">
Representation submission advice
</option>
<option value="ceq">Casework Events Query</option>
<option value="other">Any Other Enquiries</option>
<option value="postdec">Post Decision</option>
<option value="comp">Complaints/Complements</option>
</select>
</div>
</>
);
};
const RenderMultiline = ({
id,
className,
rows,
cols,
datafieldname,
name,
label,
input,
errorMsg,
meta: { touched, error },
...custom
}) => {
return (
<>
<div
className={
touched && error
? "govuk-form-group govuk-form-group--error"
: "govuk-form-group "
}
>
<label className="govuk-label " htmlFor={id} id={id + "_label"}>
{label}
</label>
{touched && error && (
<span id={id + "-error"} className="govuk-error-message">
<span className="govuk-visually-hidden">Error:</span>{" "}
{errorMsg || error}
</span>
)}
<textarea
id={id}
name={id}
rows={rows}
cols={cols}
className={className}
{...input}
maxLength={
custom.hasOwnProperty("maxFieldLength")
? custom.maxFieldLength
? custom.maxFieldLength
: 800
: 800
}
></textarea>
</div>
</>
);
};
const RenderTextfield = ({
id,
className,
rows,
datafieldname,
name,
label,
input,
errorMsg,
meta: { touched, error },
...custom
}) => {
return (
<>
<div
className={
touched && error
? "govuk-form-group govuk-form-group--error"
: "govuk-form-group "
}
>
<label className="govuk-label " htmlFor={id} id={id + "_label"}>
{label}
</label>
{touched && error && (
<span id={id + "-error"} className="govuk-error-message">
<span className="govuk-visually-hidden">Error:</span>{" "}
{errorMsg || error}
</span>
)}
<input
{...input}
type={"email"}
className={className}
name={id || name}
id={id || name}
maxLength={
custom.hasOwnProperty("maxFieldLength")
? custom.maxFieldLength
? custom.maxFieldLength
: 100
: 200
}
pattern={
custom.hasOwnProperty("pattern")
? custom.pattern
: undefined
}
/>
</div>
</>
);
};
const contactPurpose = (contactValue) => {
let contactPurposeBody;
switch (contactValue) {
case "plq":
contactPurposeBody = (
<p className="govuk-body">
Questions about Planning legislation or policy
</p>
);
break;
case "ipq":
contactPurposeBody = (
<p className="govuk-body">
Questions or comments about applications for major
infrastructure projects
</p>
);
break;
case "pcpq":
contactPurposeBody = (
<p className="govuk-body">
Questions or comments about planning and environmental
appeals
</p>
);
break;
case "pi":
contactPurposeBody = (
<p className="govuk-body">
Query about using the Casework Portal
</p>
);
break;
case "appadvice":
contactPurposeBody = (
<p className="govuk-body">Advice How to submit an appeal</p>
);
break;
case "repadvice":
contactPurposeBody = (
<p className="govuk-body">
Advice How to comment on an appeal or application
</p>
);
break;
case "ceq":
contactPurposeBody = (
<p className="govuk-body">
Queries about forthcoming Hearing or Inquiry event
</p>
);
break;
case "other":
contactPurposeBody = (
<p className="govuk-body">Other enquiry for PEDW</p>
);
break;
case "postdec":
contactPurposeBody = (
<p className="govuk-body">
Issues or questions about a case after the decision is
issued
</p>
);
break;
case "comp":
contactPurposeBody = (
<p className="govuk-body">General Complaints of Compliments</p>
);
break;
default:
// code block
}
return contactPurposeBody;
};
let t;
let ContactUs = (props) => {
const { footerLinks, pages } = props;
let { t } = useTranslation();
const [formSubmitted, setFormSubmitted] = useState(false);
const router = useRouter();
const { locale } = router;
const required = (value) =>
value ? undefined : t("newappeal:is-required-label");
const emailRegex =
/^(?:[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}|(?:\[(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-zA-Z0-9-]*[a-zA-Z0-9]:.+)])$/;
const email = (value) =>
value && !emailRegex.test(value)
? t("account:invalid-email-address-errormsg")
: undefined;
const onSubmit = async (formValues) => {
// Only called if validation passes
sendContactEmail(formValues);
createCRMTask(formValues);
console.log("Form submitted:", formValues);
};
const sendContactEmail = async (formValues) => {
Object.assign(formValues, {
"contactSubject": contactPurpose(formValues.typeOfContact).props
.children,
});
const reference = "PEDW-CONTACT-US";
const templateId = "69228c5f-8500-40ff-a6c5-95a9434c3ec8";
const templateIdCY = "b8f1f76d-4ab9-4a2f-b294-e3623c6095a3";
try {
const res = await sendEmail(
router.locale !== "en" ? templateIdCY : templateId,
formValues.contactEmail,
formValues,
reference
);
console.log("Email sent successfully.");
setFormSubmitted(true);
} catch (error) {
console.error("Email sending failed:", error);
alert("Something went wrong. Please try again later.");
// Required: inform redux-form this is a submit error
throw new SubmissionError({
_error: "Failed to send email. Please try again later.",
});
}
};
return (
<div>
<Head>
<title>{t("common:service-name")}</title>
</Head>
<div id="page_wrapper">
<CookieBanner />
<Header />
<div className="govuk-width-container">
<Breadcrumbs />
<ServiceBanner />
<main
className="govuk-main-wrapper govuk-!-padding-top-9 govuk-main-wrapper--auto-spacing"
id="main-content"
role="main"
>
<div className="govuk-grid-row">
<div className="govuk-grid-column-full">
<h2 className="govuk-heading-l govuk-!-margin-bottom-7">
Contact us{" "}
</h2>
<div className="wgForm">
{formSubmitted ? (
<div className="govuk-panel govuk-panel--confirmation">
<h1 className="govuk-panel__title">
Thank you for your enquiry
</h1>
<div className="govuk-panel__body">
Well get back to you as soon as
we can.
</div>
</div>
) : (
<>
<p className="govuk-body">
Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Nulla congue leo ac quam
pulvinar, id vulputate libero
volutpat. Maecenas ut odio id
nisl laoreet dictum. Nam
faucibus, leo vitae egestas
lacinia, enim tortor bibendum
lorem, ut tincidunt lacus sapien
quis lorem. Aenean interdum
dolor augue, ac cursus lectus
suscipit vitae.{" "}
</p>
<form
onSubmit={props.handleSubmit(
onSubmit
)}
>
<Field
name={"contactEmail"}
id={"contactEmail"}
label="Your email address"
component={RenderTextfield}
validate={[required, email]}
className="govuk-input govuk-input--width-20"
type="email"
/>
<Field
name="typeOfContact"
id="typeOfContact"
component={RenderPickList}
label="What do you want to contact us about?"
validate={[required]}
/>
<div className="govuk-form-group">
{props.typeOfContactValue && (
<div>
{contactPurpose(
props.typeOfContactValue
)}
</div>
)}
</div>
<Field
name="contactbody"
id="contactbody"
component={RenderMultiline}
label="What do you want to ask?"
rows={10}
cols={80}
validate={[required]}
/>
<button
type="submit"
className="govuk-button"
>
Send your question
</button>
<button
type="button"
className="govuk-button govuk-button--secondary govuk-!-margin-left-5"
onClick={props.reset}
>
Clear form
</button>
</form>
</>
)}
</div>
</div>
</div>
</main>
</div>
<Footer footerLinks={footerLinks} ticketnumber={props} />
</div>
</div>
);
};
const selector = formValueSelector("contactUsForm"); // <-- same as form name
// ContactUs = connect((state) => {
// const typeOfContactValue = selector(state, "typeOfContact");
// return {
// typeOfContactValue,
// initialValues: {
// contactEmail:
// state.accountDetails.accountDetails?.emailaddress1 || "",
// },
// };
// })(ContactUs);
const mapDispatchToProps = (dispatch) => ({});
const mapStateToProps = (state) => {
const typeOfContactValue = selector(state, "typeOfContact");
const email = state.accountDetails.accountDetails?.emailaddress1;
return {
typeOfContactValue,
initialValues: email ? { contactEmail: email } : {},
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(
reduxForm({
form: "contactUsForm",
enableReinitialize: true,
destroyOnUnmount: false,
touchOnBlur: true,
touchOnChange: true,
onSubmitFail: (errors, dispatch, subnittError, props) => {
if (errors) {
// Touch all fields with errors so validation messages appear
const errorFields = Object.keys(errors);
dispatch(touch("contactUsForm", ...errorFields));
// Scroll to first error
const firstErrorField = errorFields[0];
const errorElement = document.querySelector(
`[name="${firstErrorField}"]`
);
if (
errorElement &&
typeof errorElement.scrollIntoView === "function"
) {
errorElement.scrollIntoView({
behavior: "smooth",
block: "center",
});
errorElement.focus({ preventScroll: true });
} else {
console.warn(
"Could not find element for error field:",
firstErrorField
);
}
}
console.log("Redux Form onSubmitFail:", submitError);
},
})(ContactUs)
);