diff --git a/components/newappeal/buildfield.js b/components/newappeal/buildfield.js
index 5af2c197..29e6e919 100644
--- a/components/newappeal/buildfield.js
+++ b/components/newappeal/buildfield.js
@@ -204,7 +204,7 @@ export default function BuildField(props) {
uploadCount={uploadCount}
setFileCount={setFileCount}
setUploadCount={setUploadCount}
- props={props?.props?.props}
+ props={props?.props?.props?.props}
/>
),
diff --git a/components/newappeal/constants.js b/components/newappeal/constants.js
new file mode 100644
index 00000000..1d094f96
--- /dev/null
+++ b/components/newappeal/constants.js
@@ -0,0 +1,2 @@
+// components/newappeal/constants.js
+export const SECTION_COMPLETE = 9999;
diff --git a/components/newappeal/newAppealFlow.js b/components/newappeal/newAppealFlow.js
new file mode 100644
index 00000000..b8a78111
--- /dev/null
+++ b/components/newappeal/newAppealFlow.js
@@ -0,0 +1,48 @@
+// components/newappeal/NewAppealFlow.js
+import React from "react";
+import BuildCheckSection from "./buildchecksection";
+import BuildSection from "./buildsection";
+import CompleteAppeal from "./complete";
+import { SECTION_COMPLETE } from "./constants";
+
+export default function NewAppealFlow({
+ formXML,
+ sectionCount,
+ currentSection,
+ onSubmit,
+ formTitle,
+ mandatoryFieldsData,
+ propsForChildren,
+ docsOffline,
+}) {
+ if (currentSection === SECTION_COMPLETE) {
+ return ;
+ }
+
+ if (currentSection === sectionCount + 1) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/components/newappeal/newAppealPageShell.js b/components/newappeal/newAppealPageShell.js
new file mode 100644
index 00000000..b2c13561
--- /dev/null
+++ b/components/newappeal/newAppealPageShell.js
@@ -0,0 +1,112 @@
+// components/newappeal/NewAppealPageShell.js
+import React from "react";
+import Head from "next/head";
+import { useRouter } from "next/router";
+import useTranslation from "next-translate/useTranslation";
+
+import Breadcrumbs from "../breadcrumbs";
+import CookieBanner from "../cookieBanner";
+import Footer from "../footer";
+import Header from "../header";
+import TimeOut from "../timeout";
+import ServiceBanner from "../myportal/servicebanner";
+
+export default function NewAppealPageShell({
+ appealtypes,
+ formTitle,
+ footerLinks,
+ children,
+ propsForFooter,
+ propsForServiceBanner,
+}) {
+ const router = useRouter();
+ const { t } = useTranslation();
+
+ return (
+
+
+
+ {t("newappeal:page-title")} - {t("common:service-name")}
+
+
+ {/* Keep your existing meta structure */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/lib/forms/parseFormXml.js b/lib/forms/parseFormXml.js
new file mode 100644
index 00000000..2f045417
--- /dev/null
+++ b/lib/forms/parseFormXml.js
@@ -0,0 +1,35 @@
+// lib/forms/parseFormXml.js
+import xpath from "xpath";
+
+/**
+ * NOTE: Your current page uses DOMParser, which is available in the browser.
+ * For SSR parsing you'd usually use xmldom or a similar package.
+ *
+ * This module supports BOTH:
+ * - Browser: pass DOMParser output doc
+ * - Server: if you later add xmldom, pass its doc here
+ */
+
+export function getSectionCount(doc) {
+ const tabs = xpath.select("/form/tabs/tab[*]", doc);
+ return Array.isArray(tabs) ? tabs.length : 0;
+}
+
+export function getTabTitles(doc) {
+ // same XPath you had
+ const titleAttrs = xpath.select(
+ "//form/tabs/tab[*]/labels/label[@description]/@description",
+ doc
+ );
+
+ if (!Array.isArray(titleAttrs)) return [];
+ // Attribute nodes differ by parser; be defensive:
+ return titleAttrs
+ .map((n) => (typeof n?.value === "string" ? n.value : n?.toString?.()))
+ .filter(Boolean);
+}
+
+export function getTabs(doc) {
+ const tabs = xpath.select("//form/tabs/tab[*]", doc);
+ return Array.isArray(tabs) ? tabs : [];
+}
diff --git a/lib/forms/readFormXml.js b/lib/forms/readFormXml.js
new file mode 100644
index 00000000..5278f7f2
--- /dev/null
+++ b/lib/forms/readFormXml.js
@@ -0,0 +1,24 @@
+// lib/forms/readFormXml.js
+import fs from "fs";
+import path from "path";
+
+/**
+ * Reads /data/formsxml/{appealtypes}.xml and normalises it.
+ * Returns { xmlStr } or throws on missing file.
+ */
+export function readFormXml(appealtypes) {
+ if (!appealtypes) throw new Error("readFormXml: appealtypes is required");
+
+ const configDirectory = path.resolve(process.cwd(), "data/formsxml");
+ const filePath = path.join(configDirectory, `${appealtypes}.xml`);
+
+ let xmlStr = fs.readFileSync(filePath, "utf8");
+
+ // Keep existing normalisation behaviour
+ xmlStr = xmlStr.replace(/\t/g, "");
+ xmlStr = xmlStr.replace(/\n/g, "");
+ xmlStr = xmlStr.replace(/> <"/g, "><");
+ xmlStr = xmlStr.toString();
+
+ return { xmlStr, filePath };
+}
diff --git a/lib/myportal/hydrateMyPortalAppealStore.js b/lib/myportal/hydrateMyPortalAppealStore.js
new file mode 100644
index 00000000..e83dfd54
--- /dev/null
+++ b/lib/myportal/hydrateMyPortalAppealStore.js
@@ -0,0 +1,82 @@
+// lib/myportal/hydrateMyPortalAppealStore.js
+import {
+ setContainerID,
+ setLoggedInUserEmail,
+ setLoggedInUserId,
+ setAccountDetails,
+} from "../../store/accountDetails/action";
+
+import {
+ setAppealLPA,
+ setAppealType,
+ setAppealTypeID,
+ setCaseReference as setCaseReferenceAction,
+ setFilesForAppeal,
+} from "../../store/appealType/action";
+
+import {
+ setAwaitingSubmissionDetails,
+ setAwaitingSubmissionFromBlob,
+} from "../../store/awaitingSubmission/action";
+
+import { setCurrentView, setLocale } from "../../store/currentView/action";
+import { setForm } from "../../store/formData/action";
+
+/**
+ * Hydrates Redux store for resume flow in /myportal/[appealtypes]
+ */
+export function hydrateMyPortalAppealStore(store, ctx, data) {
+ const { query, locale } = ctx;
+
+ const {
+ session,
+ loggedInUser,
+ loggedInUserEmail,
+ appealTypeData,
+ mandatoryFieldsData,
+ pickListData,
+ blobList,
+ blobProgress,
+ accountDetails,
+ awaitingSubmissionFromBlob,
+ xmlStr,
+ } = data;
+
+ // Keep existing behaviour: write locale into store
+ store.dispatch(setLocale(locale));
+
+ // Build caseReference in the same shape you already use
+ const caseReference = {
+ ticketnumber: query.casereference,
+ incidentid: query.inid, // as per your file
+ caseDetails: blobProgress,
+ };
+
+ // Optional awaiting submission state setup
+ if (awaitingSubmissionFromBlob) {
+ store.dispatch(
+ setAwaitingSubmissionFromBlob(awaitingSubmissionFromBlob)
+ );
+ store.dispatch(
+ setAwaitingSubmissionDetails(awaitingSubmissionFromBlob)
+ );
+ store.dispatch(
+ setCurrentView({
+ viewName: "Awaiting Submission",
+ viewKey: "awaitingSubmissionDetails",
+ })
+ );
+ }
+
+ store.dispatch(setLoggedInUserId(loggedInUser));
+ store.dispatch(setLoggedInUserEmail(loggedInUserEmail));
+ store.dispatch(setAppealLPA(query.lpa));
+ store.dispatch(setAppealTypeID(query.apt));
+ store.dispatch(setCaseReferenceAction(caseReference));
+ store.dispatch(setForm(xmlStr, mandatoryFieldsData, pickListData));
+ store.dispatch(setAppealType(appealTypeData));
+ store.dispatch(setContainerID(session.user.id));
+ store.dispatch(setAccountDetails(accountDetails));
+
+ store.dispatch(setFilesForAppeal(blobList));
+}
diff --git a/lib/myportal/loadMyPortalAppealPage.js b/lib/myportal/loadMyPortalAppealPage.js
new file mode 100644
index 00000000..a1d321c8
--- /dev/null
+++ b/lib/myportal/loadMyPortalAppealPage.js
@@ -0,0 +1,91 @@
+// lib/myportal/loadMyPortalAppealPage.js
+import { getSession } from "next-auth/react";
+import {
+ getAppealsTypesForNewAppeal,
+ getAwaitingSubmissionFromBlob,
+ getFilesFromBlob,
+ getIP,
+ getMandatoryFields,
+ getPickLists,
+ getProgressFromBlob,
+ getPersonalAccount,
+} from "../../actions";
+
+import { readFormXml } from "../forms/readFormXml";
+import { requireQueryParams } from "../routing/requireQueryParams";
+
+/**
+ * Loader for "resume appeal" in /myportal/[appealtypes]
+ */
+export async function loadMyPortalAppealPage(ctx) {
+ const { query, req } = ctx;
+
+ getIP(req);
+
+ // Required params for resume flow (adjust if your routes always provide them)
+ // casereference is the main identifier here
+ const required = requireQueryParams(query, [
+ "appealtypes",
+ "apt",
+ "casereference",
+ ]);
+ if (!required.ok) {
+ return { redirect: required.redirect };
+ }
+
+ const session = await getSession(ctx);
+ if (!session) {
+ return {
+ redirect: {
+ destination: "/auth/signin",
+ permanent: false,
+ },
+ };
+ }
+
+ const { cookies } = req;
+ const loggedInUser = cookies?.pinsUser;
+ const loggedInUserIdent = session.user.id;
+ const loggedInUserEmail = session.user.email;
+
+ const accountDetails = await getPersonalAccount(loggedInUser);
+
+ // Fetch config + blob assets in parallel
+ const [appealTypeData, mandatoryFieldsData, pickListData, blobList] =
+ await Promise.all([
+ getAppealsTypesForNewAppeal(),
+ getMandatoryFields(query.appealtypes),
+ getPickLists(query.appealtypes),
+ getFilesFromBlob(loggedInUserIdent, query.casereference),
+ ]);
+
+ const blobProgress = await getProgressFromBlob(
+ loggedInUserIdent,
+ query.casereference
+ );
+
+ // Optional: awaiting-submission view setup when query.key exists
+ let awaitingSubmissionFromBlob = null;
+ if (Object.prototype.hasOwnProperty.call(query, "key")) {
+ awaitingSubmissionFromBlob = await getAwaitingSubmissionFromBlob(
+ session.user.id
+ );
+ }
+
+ const { xmlStr } = readFormXml(query.appealtypes);
+
+ return {
+ session,
+ loggedInUser,
+ loggedInUserIdent,
+ loggedInUserEmail,
+ appealTypeData,
+ mandatoryFieldsData,
+ pickListData,
+ blobList,
+ blobProgress,
+ accountDetails,
+ awaitingSubmissionFromBlob,
+ xmlStr,
+ };
+}
diff --git a/lib/newappeal/hydrateNewAppealStore.js b/lib/newappeal/hydrateNewAppealStore.js
new file mode 100644
index 00000000..23ca1e43
--- /dev/null
+++ b/lib/newappeal/hydrateNewAppealStore.js
@@ -0,0 +1,48 @@
+// lib/newappeal/hydrateNewAppealStore.js
+import {
+ setContainerID,
+ setLoggedInUserEmail,
+ setLoggedInUserId,
+ setAccountDetails,
+} from "../../store/accountDetails/action";
+import {
+ setAppealLPA,
+ setAppealType,
+ setAppealTypeID,
+ // IMPORTANT: avoid name collision in your page by importing as Action
+ setCaseReference as setCaseReferenceAction,
+} from "../../store/appealType/action";
+import { setForm } from "../../store/formData/action";
+
+/**
+ * Dispatches all actions needed to hydrate Redux store for the page.
+ */
+export function hydrateNewAppealStore(store, query, data) {
+ const {
+ session,
+ loggedInUser,
+ loggedInUserEmail,
+ appealTypeData,
+ mandatoryFieldsData,
+ pickListData,
+ blobProgress,
+ accountDetails,
+ xmlStr,
+ } = data;
+
+ const caseReference = {
+ ticketnumber: query.id,
+ incidentid: query.id,
+ caseDetails: blobProgress,
+ };
+
+ store.dispatch(setLoggedInUserId(loggedInUser));
+ store.dispatch(setLoggedInUserEmail(loggedInUserEmail));
+ store.dispatch(setAppealLPA(query.lpa));
+ store.dispatch(setAppealTypeID(query.apt));
+ store.dispatch(setCaseReferenceAction(caseReference));
+ store.dispatch(setForm(xmlStr, mandatoryFieldsData, pickListData));
+ store.dispatch(setAppealType(appealTypeData));
+ store.dispatch(setContainerID(session.user.id));
+ store.dispatch(setAccountDetails(accountDetails));
+}
diff --git a/lib/newappeal/loadNewAppealPage.js b/lib/newappeal/loadNewAppealPage.js
new file mode 100644
index 00000000..786e7c43
--- /dev/null
+++ b/lib/newappeal/loadNewAppealPage.js
@@ -0,0 +1,69 @@
+// lib/newappeal/loadNewAppealPage.js
+import { getSession } from "next-auth/react";
+import {
+ getAppealsTypesForNewAppeal,
+ getIP,
+ getMandatoryFields,
+ getPickLists,
+ getProgressFromBlob,
+ getPersonalAccount,
+} from "../../actions";
+import { readFormXml } from "../forms/readFormXml";
+import { requireQueryParams } from "../routing/requireQueryParams";
+
+/**
+ * Loads everything needed for New Appeal SSR.
+ * Returns { redirect } on auth/query issues, otherwise returns a data object.
+ */
+export async function loadNewAppealPage(ctx) {
+ const { query, req } = ctx;
+
+ // Preserve existing behaviour
+ getIP(req);
+
+ // Validate required params early (adjust keys if needed)
+ const required = requireQueryParams(query, ["appealtypes", "apt", "id"]);
+ if (!required.ok) {
+ return { redirect: required.redirect };
+ }
+
+ const session = await getSession(ctx);
+ if (!session) {
+ return {
+ redirect: {
+ destination: "/auth/signin",
+ permanent: false,
+ },
+ };
+ }
+
+ const { cookies } = req;
+ const loggedInUser = cookies?.pinsUser; // existing cookie usage
+ const loggedInUserIdent = session.user.id;
+ const loggedInUserEmail = session.user.email;
+
+ const [appealTypeData, mandatoryFieldsData, pickListData] =
+ await Promise.all([
+ getAppealsTypesForNewAppeal(),
+ getMandatoryFields(query.appealtypes),
+ getPickLists(query.appealtypes),
+ ]);
+
+ const blobProgress = await getProgressFromBlob(loggedInUserIdent, query.id);
+ const accountDetails = await getPersonalAccount(loggedInUser);
+
+ const { xmlStr } = readFormXml(query.appealtypes);
+
+ return {
+ session,
+ loggedInUser,
+ loggedInUserIdent,
+ loggedInUserEmail,
+ appealTypeData,
+ mandatoryFieldsData,
+ pickListData,
+ blobProgress,
+ accountDetails,
+ xmlStr,
+ };
+}
diff --git a/lib/routing/requireQueryParams.js b/lib/routing/requireQueryParams.js
new file mode 100644
index 00000000..f8ede185
--- /dev/null
+++ b/lib/routing/requireQueryParams.js
@@ -0,0 +1,21 @@
+// lib/routing/requireQueryParams.js
+// Simple query validator for getServerSideProps
+
+export function requireQueryParams(query, requiredKeys = []) {
+ const missing = requiredKeys.filter((k) => !query?.[k]);
+ if (missing.length === 0) return { ok: true };
+
+ // Redirect to a generic error page you already have
+ // (you can swap this destination later)
+ const destination = `/error?missing=${encodeURIComponent(
+ missing.join(",")
+ )}`;
+ return {
+ ok: false,
+ redirect: {
+ destination,
+ permanent: false,
+ },
+ missing,
+ };
+}