Merged PR 1646: sprint 8 tasks and app error fix

Related work items: #14627, #16943, #16944
This commit is contained in:
Robert Bond
2025-05-28 13:16:02 +00:00
10 changed files with 788 additions and 70 deletions
+21
View File
@@ -156,6 +156,27 @@ const Breadcrumbs = (props) => {
</li>
</>
)}
{router.pathname == "/myportal/contactus" && (
<>
<li className="govuk-breadcrumbs__list-item">
<Link
href={
router.locale != "en"
? "/" +
router.locale +
"/fymhorth"
: "/myportal"
}
className="govuk-breadcrumbs__link"
>
{t("common:breadcrumb-my-portal")}
</Link>
</li>
<li className="govuk-breadcrumbs__list-item">
Contact us
</li>
</>
)}
{router.pathname == "/addresssearchresults" && (
<>
{" "}
+12
View File
@@ -1047,6 +1047,12 @@ const CaseSummary = (props) => {
"case:watch-this-case-link"
)}
</button>
<button className="govuk-link mailNotification">
Send me
email
updates
</button>
{/* <button
onClick={
handleOpenModal
@@ -1105,6 +1111,12 @@ const CaseSummary = (props) => {
"case:watch-this-case-link"
)}
</button>
<button className="govuk-link mailNotification">
Send me email
updates
</button>
{/* <button
onClick={handleOpenModal}
>
+5
View File
@@ -1,3 +1,6 @@
import Link from "next/link";
import { useRouter } from "next/router";
export function capitalizeFirstLetter(val) {
return String(val).charAt(0).toUpperCase() + String(val).slice(1);
}
@@ -44,6 +47,8 @@ export function hasOwnPropertyAndNotNull(obj, property) {
export function linkedCasesList(linkedCasesReference) {
//console.log(linkedCasesReference);
const router = useRouter();
linkedCasesReference = linkedCasesReference.value || [];
return (
+2 -2
View File
@@ -175,8 +175,8 @@ let CaseSearch = (props) => {
<Link
href={
router.locale == "cy"
? "/cyswllt"
: "/contactus"
? "/fymhorth/cyswllt"
: "/myportal/contactus"
}
className="govuk-button withChevron govuk-button-cta"
>
+89
View File
@@ -100,6 +100,35 @@ const SearchResults = (props) => {
});
};
const selectEmailNotifications = (
emailaddress,
loggedInUser,
incidentID,
appealType
) => {
let updateBody = {
"pinswg_WatchedCase@odata.bind": "/incidents(" + incidentID + ")",
"pinswg_Contact@odata.bind": "/contacts(" + loggedInUser + ")",
"pinswg_appealcasetype": +appealType,
};
console.log("Email signed up");
// createWatchedCases(updateBody)
// .then((data) => data)
// .then(() => {
// getWatchedCasesProxy(props.accountDetails.loggedinUserId).then(
// (data) => {
// setWatchedCases(data),
// getDetailsProxy(data, "myWatchedCases").then(
// (data) => {
// setWatchedCasesDetails(data);
// }
// );
// }
// );
// });
};
const isWatchedCase = (whichIncident) => {
let isWatched = jsonpath({
path:
@@ -114,6 +143,20 @@ const SearchResults = (props) => {
return isWatched;
};
const isEmailNotifications = (whichIncident) => {
let isWatched = jsonpath({
path:
"$..value[?(@ && @._pinswg_watchedcase_value=='" +
whichIncident +
"')]",
json: watchedCases,
eval: true,
});
return isWatched;
};
const deleteItem = (caseID, topThreeType) => {
let cookies = parseCookies();
topThreeType == "watchedCases" &&
@@ -600,6 +643,28 @@ const SearchResults = (props) => {
<span className="eye"></span>
</button>
)}
<button
className="govuk-summary-list__actionLink mailLink"
title="Email about changes"
onClick={() => {
confirm(
" Do you want to be sent email updates for " +
item.title +
"?"
) &&
selectEmailNotifications(
props.accountDetails
.accountDetails
.emailaddress1,
props.accountDetails
.loggedinUserId,
item.incidentid,
item.pinswg_appealcasetype
);
}}
>
<span className="mailicon icon"></span>
</button>
</dd>
)}
@@ -642,6 +707,30 @@ const SearchResults = (props) => {
>
<span className="eye"></span>
</button>
<button
className="govuk-summary-list__actionLink mailLink"
title="Email about changes"
onClick={() => {
confirm(
" Do you want to be sent email updates for " +
item.title +
"?"
) &&
selectEmailNotifications(
props
.accountDetails
.accountDetails
.emailaddress1,
props
.accountDetails
.loggedinUserId,
item.incidentid,
item.pinswg_appealcasetype
);
}}
>
<span className="mailicon icon"></span>
</button>
</dd>
)}
</div>
+1
View File
@@ -12,6 +12,7 @@ module.exports = {
"*": ["common", "search"],
"/": ["common", "home", "case"],
"/contactus": ["newappeal", "account"],
"/myportal/contactus": ["newappeal", "account"],
"/error": ["common", "home", "myportal"],
"/furtherdetails": ["common", "home", "myportal"],
"/myportal": ["myportal", "common", "home", "case"],
+6 -65
View File
@@ -71,76 +71,17 @@ export default async function ApiProxy(req, res) {
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 = {
plq: "ec48450c-7e26-ec11-a97f-00224800e98c", //PET
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",
appadvice: "cc3d987e-7c26-ec11-a97f-00224800e98c",
repadvice: "cc3d987e-7c26-ec11-a97f-00224800e98c",
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",
other: "8ffabad9-7c26-ec11-a97f-00224800e98c", // Casework
postdec: "1357e457-7e26-ec11-a97f-00224800e98c", // SLT Team
comp: "1357e457-7e26-ec11-a97f-00224800e98c", // SLT Team
};
let teamId = teamMap[contactValue];
+31 -2
View File
@@ -7,7 +7,6 @@ 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,
@@ -296,6 +295,8 @@ let ContactUs = (props) => {
// Only called if validation passes
sendContactEmail(formValues);
createCRMTask(formValues);
formValues.typeOfContact == "plq" &&
sendPETEmail(formValues, props.petEmail);
console.log("Form submitted:", formValues);
};
@@ -329,6 +330,33 @@ let ContactUs = (props) => {
}
};
const sendPETEmail = async (formValues, petEmail) => {
const reference = "PEDW-CONTACT-US-PET";
const templateId = "56bbdcf9-d63d-48e9-a794-3e253dd7498b";
const templateIdCY = "00b794d3-29aa-434d-b696-0ef0b0337d79";
console.log(petEmail);
try {
const res = await sendEmail(
templateId,
petEmail,
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>
@@ -339,7 +367,7 @@ let ContactUs = (props) => {
<Header />
<div className="govuk-width-container">
<Breadcrumbs />
<ServiceBanner />
<ServiceBanner props={props} />
<main
className="govuk-main-wrapper govuk-!-padding-top-9 govuk-main-wrapper--auto-spacing"
id="main-content"
@@ -468,6 +496,7 @@ const mapStateToProps = (state) => {
return {
typeOfContactValue,
initialValues: email ? { contactEmail: email } : {},
petEmail: process.env.NEXT_PUBLIC_PETEMAIL,
};
};
+545
View File
@@ -0,0 +1,545 @@
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/myportal/servicebanner";
import Link from "next/link";
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);
formValues.typeOfContact == "plq" &&
sendPETEmail(formValues, props.petEmail);
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.",
});
}
};
const sendPETEmail = async (formValues, petEmail) => {
const reference = "PEDW-CONTACT-US-PET";
const templateId = "56bbdcf9-d63d-48e9-a794-3e253dd7498b";
const templateIdCY = "00b794d3-29aa-434d-b696-0ef0b0337d79";
console.log(petEmail);
try {
const res = await sendEmail(
templateId,
petEmail,
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 props={props} />
<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 {
accountDetails: state.accountDetails,
typeOfContactValue,
initialValues: email ? { contactEmail: email } : {},
petEmail: process.env.NEXT_PUBLIC_PETEMAIL,
};
};
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)
);
+76 -1
View File
@@ -2469,6 +2469,8 @@ ul#sharePageLinks.active {
display: block;
}
.eye {
/* margin: 90px; */
width: 13px;
@@ -2494,9 +2496,79 @@ ul#sharePageLinks.active {
top: 3px;
background-color: #aa1111;
}
&.mailLink {
width: 24px;
display: block;
margin-top: 5px;
}
.icon {
margin: 1px;
background-color: #fff;
border: 1px solid #aa1111;
display: inline-block;
position: relative;
vertical-align: top;
margin-top: 0px;
margin-left: -3px;
}
.icon:after,
.icon:before {
background: #fff;
border: 1px solid #aa1111;
content: "";
position: absolute;
}
.icon:hover,
.icon:hover:after,
.icon:hover:before {
background-color: #fff;
}
.mailicon {
height: 11px;
top: 0px;
width: 17px;
margin-top: 2px;
}
.mailicon:after,
.mailicon:before {
border-top: none;
top: 4px;
width: 8px;
}
.mailicon:after {
left: -1px;
-webkit-transform: rotate(25deg);
-moz-transform: rotate(25deg);
-ms-transform: rotate(25deg);
-o-transform: rotate(25deg);
transform: rotate(25deg);
}
.mailicon:before {
right: -1px;
-webkit-transform: rotate(-25deg);
-moz-transform: rotate(-25deg);
-ms-transform: rotate(-25deg);
-o-transform: rotate(-25deg);
transform: rotate(-25deg);
}
}
// modal timeout
.modal {
@@ -2748,7 +2820,8 @@ fade {
}
.govuk-link.watchCase {
.govuk-link.watchCase,
.govuk-link.mailNotification {
background-color: transparent;
border: none;
cursor: pointer;
@@ -2763,6 +2836,8 @@ fade {
}
}
.fieldArrayLink {
border: none;
background: no-repeat;