Merged PR 2242: Refactor: New Appeal flow structural cleanup (Slices 1–8, no behaviour change)
## Summary This PR merges the completed new-appeal refactor stream (Slices 1–8) into SIPS-Development. This refactor was executed in controlled slices with regression validation at each step. ## Scope Structural and readability improvements only: - XML/form derivation extraction - payload and file helper extraction - side-effect facade introduction - BuildSection and BuildCheckSection decomposition - BuildCheckRow formatter map refactor - nested prop boundary cleanup - start-flow cleanup (CreateCase / AboutYou) ## Behaviour No intended behavioural changes. The following were explicitly preserved: - S78 journey behaviour - payload shapes and field names/ids - HASCAS mapping logic - appellant/agent branching - validation rules and messages - redirect/query parameters (`lpa`, `apt`, `id`) - navigation and side-effect sequencing - EN/CY output parity ## Validation Full regression has been performed on: - start flow (CreateCase / AboutYou) - save and resume flows - file upload handling - check answers rendering - submit/finalisation sequence - confirmation flow - CRM insertion path - EN/CY parity Additional checks: - docsOffline branch behaviour - completion and partial-save email paths - negative-path validation scenarios - lint (warnings baseline unchanged) ## Risk Low: - changes are structural only - no business logic changes - no contract changes ## Rollback Safe rollback via reverting this merge commit. ## Notes This refactor reduces coupling and prepares the new-appeal flow for future appeal-type expansion. Related work items: #22570, #22576, #22577, #22583, #22586, #22587, #22588, #22590
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
# Contributing with AI (Refactor Branch — PEDW FrontEnd)
|
||||
|
||||
## Purpose
|
||||
|
||||
This guide defines how engineers and AI agents should work safely and efficiently in this refactor branch.
|
||||
|
||||
This branch focuses on **safe, behaviour-preserving refactor of the new appeal flow**.
|
||||
|
||||
---
|
||||
|
||||
## Start Here (Required Order)
|
||||
|
||||
1. Read `.clinerules/refactor-branch-rules.md`
|
||||
2. Read `GUARDRAILS.md`
|
||||
3. Read core context:
|
||||
- `context/refactor-branch-charter.md`
|
||||
- `context/newappeal-refactor-guardrails.md`
|
||||
- `context/architecture.md`
|
||||
- `context/domain-flows.md`
|
||||
|
||||
Only read additional files when directly relevant.
|
||||
|
||||
Do **not** load all context, templates, or memory-bank files by default.
|
||||
|
||||
---
|
||||
|
||||
## Core Principle
|
||||
|
||||
This is a **refactor branch**, not a feature branch.
|
||||
|
||||
- Preserve current live S78 behaviour
|
||||
- Do not introduce business-rule changes unless explicitly requested
|
||||
- Prefer small, safe, reversible changes
|
||||
- Focus on structure, not behaviour
|
||||
|
||||
---
|
||||
|
||||
## Standard AI-Assisted Workflow
|
||||
|
||||
### 1. Scope
|
||||
|
||||
- Define requirement
|
||||
- Confirm non-goals
|
||||
- Identify impacted flow areas
|
||||
- Identify risk level
|
||||
|
||||
### 2. Plan
|
||||
|
||||
- Keep plan small and incremental
|
||||
- Identify safe boundaries
|
||||
- Avoid mixing refactor + feature work
|
||||
|
||||
### 3. Implement
|
||||
|
||||
- Make the smallest viable change
|
||||
- Prefer extraction over rewrite
|
||||
- Preserve interfaces and payload shapes
|
||||
|
||||
### 4. Validate
|
||||
|
||||
Minimum required:
|
||||
|
||||
- `npm run lint`
|
||||
- targeted manual checks
|
||||
- EN/CY parity checks (if user-facing)
|
||||
- negative-path checks for sensitive flows
|
||||
|
||||
### 5. Document (only when needed)
|
||||
|
||||
- update `memory-bank/change-log.md` for non-trivial work
|
||||
- record decisions or pitfalls if useful
|
||||
|
||||
---
|
||||
|
||||
## Templates (Use Only When Needed)
|
||||
|
||||
Templates are optional tools, not default context.
|
||||
|
||||
Use **only one** template if needed:
|
||||
|
||||
- feature implementation
|
||||
- bug fix
|
||||
- refactor
|
||||
- debugging
|
||||
- code review
|
||||
|
||||
Do not load all templates.
|
||||
Do not treat templates as baseline context.
|
||||
|
||||
---
|
||||
|
||||
## Context Loading Rule (Critical)
|
||||
|
||||
Always use the **minimum required context**.
|
||||
|
||||
### Default context for this branch:
|
||||
|
||||
- `context/refactor-branch-charter.md`
|
||||
- `context/newappeal-refactor-guardrails.md`
|
||||
- `context/architecture.md`
|
||||
- `context/domain-flows.md`
|
||||
|
||||
### Only load additional files when:
|
||||
|
||||
- working with integrations → `integration-map.md`
|
||||
- reviewing test gaps → `test-coverage-map.md`
|
||||
- performing release work → `runbook.md`
|
||||
- working on specific planned sequence → `context/plans/*`
|
||||
|
||||
Never load:
|
||||
|
||||
- all context files
|
||||
- all templates
|
||||
- onboarding or overview docs
|
||||
|
||||
---
|
||||
|
||||
## Branching Protocol
|
||||
|
||||
This repository currently uses different branch rules depending on the work type.
|
||||
|
||||
### For BAU / normal delivery work
|
||||
|
||||
- create work branches from `SIPS-Development`
|
||||
|
||||
### For new appeal refactor work
|
||||
|
||||
- use the `refactor` branch as the base branch
|
||||
- do refactor implementation either:
|
||||
- directly on `refactor`, if that is the agreed working model, or
|
||||
- on a short-lived feature branch created from `refactor`
|
||||
|
||||
Do not perform refactor implementation directly on `SIPS-Development`.
|
||||
|
||||
If unsure which branch model applies, stop and confirm before making changes.
|
||||
|
||||
---
|
||||
|
||||
## No-Break Rules (Always Enforce)
|
||||
|
||||
- Do not weaken auth/session behaviour
|
||||
- Do not weaken security headers/CSP
|
||||
- Keep EN/CY behaviour aligned
|
||||
- Do not expose secrets or personal data
|
||||
- Protect save/resume/upload/submit journeys
|
||||
|
||||
---
|
||||
|
||||
## PR Minimum (AI-Assisted)
|
||||
|
||||
Every change must include:
|
||||
|
||||
- Scope summary
|
||||
- Files changed
|
||||
- Risk notes (auth/data/i18n/a11y)
|
||||
- Validation evidence
|
||||
- Rollback plan
|
||||
|
||||
---
|
||||
|
||||
## Handling Unknowns
|
||||
|
||||
If unsure:
|
||||
|
||||
1. Choose the safer option
|
||||
2. Do not change behaviour
|
||||
3. call out assumptions clearly
|
||||
4. log open questions if needed
|
||||
|
||||
---
|
||||
|
||||
## Summary Rule
|
||||
|
||||
If in doubt:
|
||||
|
||||
> Preserve behaviour, reduce risk, keep changes small.
|
||||
@@ -0,0 +1,95 @@
|
||||
# Default Rules (Reference Only)
|
||||
|
||||
## Important
|
||||
|
||||
This file is **not the active rule set for this refactor branch**.
|
||||
|
||||
Active working rules are defined in:
|
||||
|
||||
- `.clinerules/refactor-branch-rules.md`
|
||||
- `context/refactor-branch-charter.md`
|
||||
- `context/newappeal-refactor-guardrails.md`
|
||||
|
||||
Do not load this file by default.
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
This file provides general repository-wide guidance and may be referenced when needed for broader context.
|
||||
|
||||
---
|
||||
|
||||
## Repository Mission
|
||||
|
||||
Deliver safe, accessible, bilingual (EN/CY), and reliable public-service functionality without regressing core journeys.
|
||||
|
||||
---
|
||||
|
||||
## General Priorities
|
||||
|
||||
1. Safety and security of user data
|
||||
2. Stability of public-facing journeys
|
||||
3. Accessibility and bilingual parity
|
||||
4. Maintainability of code
|
||||
5. Controlled, safe delivery
|
||||
|
||||
---
|
||||
|
||||
## General Coding Guidance
|
||||
|
||||
- Follow existing repo patterns
|
||||
- Keep page-level logic thin
|
||||
- Move reusable logic into `components/`, `actions/`, or `lib/`
|
||||
- Avoid broad refactors in feature or bugfix work
|
||||
- Prefer small, focused changes
|
||||
|
||||
---
|
||||
|
||||
## Architecture Guardrails (General)
|
||||
|
||||
- Do not bypass auth/session handling
|
||||
- Do not weaken security controls
|
||||
- Preserve relay request integrity
|
||||
- Maintain EN/CY route parity
|
||||
- Keep Redux hydration/persistence stable
|
||||
|
||||
---
|
||||
|
||||
## Validation Expectations (General)
|
||||
|
||||
- linting must pass
|
||||
- user-facing changes must be verified
|
||||
- accessibility and EN/CY checks required
|
||||
- sensitive flows require negative-path checks
|
||||
|
||||
---
|
||||
|
||||
## When To Use This File
|
||||
|
||||
Only reference this file when:
|
||||
|
||||
- broader repository rules are required
|
||||
- behaviour conflicts are unclear
|
||||
- no branch-specific guidance exists
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
This file is background guidance only.
|
||||
|
||||
For this refactor branch:
|
||||
|
||||
- follow branch-specific rules first
|
||||
- prioritise behaviour preservation
|
||||
- keep context minimal and focused
|
||||
|
||||
## Branching Guidance (Reference)
|
||||
|
||||
Branching depends on work type:
|
||||
|
||||
- BAU work typically branches from `SIPS-Development`
|
||||
- refactor-stream work for the new appeal flow uses `refactor` as the base branch
|
||||
|
||||
Follow branch-specific rules first.
|
||||
@@ -0,0 +1,228 @@
|
||||
# Refactor Branch Rules (Active)
|
||||
|
||||
## Purpose
|
||||
|
||||
This file defines the active working rules for this refactor branch.
|
||||
|
||||
This branch is focused on **safe, behaviour-preserving refactor of the new appeal flow**.
|
||||
|
||||
---
|
||||
|
||||
## Core Rule (Highest Priority)
|
||||
|
||||
Do not change live S78 behaviour unless explicitly instructed.
|
||||
|
||||
Refactor = improve structure, not behaviour.
|
||||
|
||||
---
|
||||
|
||||
## Primary Objectives
|
||||
|
||||
1. Make the new appeal flow easier to understand and maintain
|
||||
2. Reduce risk when making future changes
|
||||
3. Prepare the system for multiple appeal types
|
||||
4. Improve separation of concerns (UI, workflow, data, integrations)
|
||||
|
||||
---
|
||||
|
||||
## Non-Negotiable Rules
|
||||
|
||||
- Preserve all current user journeys:
|
||||
- start appeal
|
||||
- save and exit
|
||||
- resume appeal
|
||||
- upload documents
|
||||
- check answers
|
||||
- submit appeal
|
||||
- confirmation
|
||||
|
||||
- Do not:
|
||||
- change payload structures
|
||||
- change validation rules
|
||||
- change business logic
|
||||
- hardcode logic that is currently dynamic/config-driven
|
||||
- mix refactor with feature work
|
||||
|
||||
---
|
||||
|
||||
## Refactor Approach
|
||||
|
||||
Always:
|
||||
|
||||
1. Understand current behaviour first
|
||||
2. Identify smallest safe change
|
||||
3. Prefer extraction over rewrite
|
||||
4. Keep public interfaces stable
|
||||
5. Make changes easy to review and revert
|
||||
|
||||
---
|
||||
|
||||
## Change Size Guidance
|
||||
|
||||
- Prefer small PRs (<400 LOC where possible)
|
||||
- Avoid large multi-concern changes
|
||||
- Split work into safe slices
|
||||
|
||||
---
|
||||
|
||||
## Context Usage Rules (Critical for Efficiency)
|
||||
|
||||
### Default context (only load these):
|
||||
|
||||
- `context/refactor-branch-charter.md`
|
||||
- `context/newappeal-refactor-guardrails.md`
|
||||
- `context/architecture.md`
|
||||
- `context/domain-flows.md`
|
||||
|
||||
### Only load additional context when needed:
|
||||
|
||||
- integrations → `integration-map.md`
|
||||
- testing gaps → `test-coverage-map.md`
|
||||
- release concerns → `runbook.md`
|
||||
- specific work plan → `context/plans/*`
|
||||
|
||||
### Never load by default:
|
||||
|
||||
- all context files
|
||||
- onboarding or overview docs
|
||||
- all templates
|
||||
- memory-bank contents
|
||||
|
||||
---
|
||||
|
||||
## Templates Usage
|
||||
|
||||
Templates are optional helpers.
|
||||
|
||||
- Use only ONE template when needed
|
||||
- Do not load all templates
|
||||
- Do not treat templates as baseline context
|
||||
|
||||
---
|
||||
|
||||
## Safety Rules
|
||||
|
||||
- Do not weaken authentication or session logic
|
||||
- Do not weaken security headers or middleware
|
||||
- Do not expose secrets or personal data
|
||||
- Preserve EN/CY parity
|
||||
- Maintain accessibility standards
|
||||
|
||||
---
|
||||
|
||||
## Testing & Validation Expectations
|
||||
|
||||
Minimum:
|
||||
|
||||
- `npm run lint`
|
||||
- targeted manual verification
|
||||
- EN/CY checks (if user-facing)
|
||||
- negative-path checks (for sensitive flows)
|
||||
|
||||
Before changing critical logic:
|
||||
|
||||
- add or update regression protection where possible
|
||||
|
||||
---
|
||||
|
||||
## When Unsure
|
||||
|
||||
If any uncertainty exists:
|
||||
|
||||
1. Choose the safest option
|
||||
2. Do not change behaviour
|
||||
3. Call out assumptions
|
||||
4. Keep the change minimal
|
||||
|
||||
---
|
||||
|
||||
## Definition of Success
|
||||
|
||||
A successful change:
|
||||
|
||||
- preserves behaviour
|
||||
- reduces complexity
|
||||
- improves clarity or structure
|
||||
- is small and safe to merge
|
||||
- does not introduce regression risk
|
||||
|
||||
---
|
||||
|
||||
## One-Line Rule
|
||||
|
||||
If in doubt:
|
||||
|
||||
> Keep behaviour the same, reduce risk, and make the smallest safe change.
|
||||
|
||||
---
|
||||
|
||||
## Documentation Rule (Minimal and Targeted)
|
||||
|
||||
Only document information that is necessary to safely understand or change the system later.
|
||||
|
||||
Document:
|
||||
|
||||
- key decisions (why something was changed)
|
||||
- non-obvious behaviour
|
||||
- risks or constraints (what must not be changed)
|
||||
- important assumptions
|
||||
|
||||
Do not document:
|
||||
|
||||
- obvious code behaviour
|
||||
- step-by-step implementation details
|
||||
- temporary or experimental work
|
||||
- duplicated explanations across files
|
||||
|
||||
Prefer short, focused notes over long explanations.
|
||||
|
||||
If unsure:
|
||||
|
||||
> Will someone break the system in the future if this is not written down?
|
||||
|
||||
If yes → document it
|
||||
If no → do not document it
|
||||
|
||||
Documentation should be minimal, high-signal, and never outweigh the value of the code itself.
|
||||
|
||||
## Branch Safety Rule
|
||||
|
||||
This refactor stream uses the `refactor` branch as its working base branch.
|
||||
|
||||
For refactor work:
|
||||
|
||||
- do all implementation from the `refactor` branch or a short-lived feature branch created from `refactor`
|
||||
- do not implement refactor work directly on `SIPS-Development`
|
||||
|
||||
`SIPS-Development` remains the BAU integration branch.
|
||||
Refactor changes may be merged into `SIPS-Development` only when proven safe.
|
||||
|
||||
If branch context is unclear, stop and confirm before making changes.
|
||||
|
||||
## Branch Model
|
||||
|
||||
- `SIPS-Development` = BAU branch
|
||||
- `refactor` = refactor integration branch
|
||||
- optional short-lived working branches for individual slices should be created from `refactor`
|
||||
|
||||
## Regression Safety Rule (Critical)
|
||||
|
||||
This is a live system. All refactor work must prove behaviour is unchanged.
|
||||
|
||||
Before completing any slice:
|
||||
|
||||
- verify core journey behaviour has not changed
|
||||
- verify protected flows still work:
|
||||
- save and exit
|
||||
- resume appeal
|
||||
- upload documents
|
||||
- check answers
|
||||
- submit appeal
|
||||
- confirmation
|
||||
|
||||
- verify EN/CY parity for any affected areas
|
||||
|
||||
A slice is NOT complete until regression behaviour is confirmed.
|
||||
|
||||
If regression cannot be confidently ruled out:
|
||||
→ do not proceed to next slice
|
||||
@@ -0,0 +1,15 @@
|
||||
# Bugfix Workflow
|
||||
|
||||
1. Clarify the bug symptoms and expected behavior.
|
||||
2. Inspect the relevant files and execution path.
|
||||
3. Identify the most likely root cause.
|
||||
4. Implement the smallest safe fix.
|
||||
5. Check for:
|
||||
- side effects
|
||||
- regression risk
|
||||
- validation gaps
|
||||
- missing error handling
|
||||
6. Summarize:
|
||||
- root cause
|
||||
- fix
|
||||
- preventive improvements worth noting
|
||||
@@ -0,0 +1,17 @@
|
||||
# Code Review Workflow
|
||||
|
||||
Review the relevant code or changes for:
|
||||
- correctness
|
||||
- edge cases
|
||||
- data integrity
|
||||
- security concerns
|
||||
- performance concerns
|
||||
- maintainability
|
||||
- readability
|
||||
- consistency with repo conventions
|
||||
|
||||
Return:
|
||||
1. key findings
|
||||
2. impact / severity where useful
|
||||
3. suggested improvements
|
||||
4. strengths worth preserving
|
||||
@@ -0,0 +1,18 @@
|
||||
# Database Migration Workflow
|
||||
|
||||
1. Understand the desired schema or data change.
|
||||
2. Inspect current data model, queries, and dependent code.
|
||||
3. Identify:
|
||||
- forward migration steps
|
||||
- backward compatibility concerns
|
||||
- rollback considerations
|
||||
4. Implement or propose the minimal safe migration path.
|
||||
5. Review for:
|
||||
- data integrity
|
||||
- ordering issues
|
||||
- application compatibility
|
||||
- deployment timing risk
|
||||
6. Summarize:
|
||||
- schema/data changes
|
||||
- impacted code paths
|
||||
- rollout / rollback notes
|
||||
@@ -0,0 +1,17 @@
|
||||
# Debugging Workflow
|
||||
|
||||
1. Capture the symptom, error, or incorrect behavior.
|
||||
2. Inspect the relevant path and recent changes.
|
||||
3. Form the most likely hypotheses.
|
||||
4. Narrow the cause using the smallest practical evidence checks.
|
||||
5. Propose or implement the minimal safe fix.
|
||||
6. Review for:
|
||||
- hidden assumptions
|
||||
- unhandled states
|
||||
- logging or observability gaps
|
||||
- regression risk
|
||||
7. Summarize:
|
||||
- symptom
|
||||
- cause
|
||||
- fix
|
||||
- prevention ideas
|
||||
@@ -0,0 +1,10 @@
|
||||
# Documentation Update Workflow
|
||||
|
||||
1. Identify what changed in the code or workflow.
|
||||
2. Inspect the affected docs and support files.
|
||||
3. Update only the relevant sections.
|
||||
4. Keep the docs concise, factual, and current.
|
||||
5. Summarize:
|
||||
- what changed
|
||||
- which docs were updated
|
||||
- anything still uncertain
|
||||
@@ -0,0 +1,16 @@
|
||||
# Feature Implementation Workflow
|
||||
|
||||
1. Understand the requirement and desired outcome.
|
||||
2. Inspect the relevant files, patterns, and data flows.
|
||||
3. Propose a short implementation plan for non-trivial work.
|
||||
4. Implement the smallest maintainable change that satisfies the requirement.
|
||||
5. Review for:
|
||||
- correctness
|
||||
- edge cases
|
||||
- error handling
|
||||
- compatibility with existing behavior
|
||||
- maintainability
|
||||
6. Summarize:
|
||||
- what changed
|
||||
- why
|
||||
- any follow-up considerations
|
||||
@@ -0,0 +1,13 @@
|
||||
# Planning Workflow
|
||||
|
||||
1. Clarify the objective.
|
||||
2. Inspect the relevant code areas and constraints.
|
||||
3. Identify implementation options.
|
||||
4. Compare trade-offs.
|
||||
5. Recommend the most practical path.
|
||||
6. Return:
|
||||
- objective summary
|
||||
- proposed plan
|
||||
- risks
|
||||
- dependencies
|
||||
- suggested execution order
|
||||
@@ -0,0 +1,16 @@
|
||||
# PR Review Workflow
|
||||
|
||||
Review the pull request or change set for:
|
||||
- correctness
|
||||
- scope discipline
|
||||
- regression risk
|
||||
- compatibility impact
|
||||
- tests or validation gaps
|
||||
- security and performance concerns
|
||||
- maintainability
|
||||
|
||||
Return:
|
||||
1. summary of change intent
|
||||
2. key risks
|
||||
3. requested changes
|
||||
4. optional improvements
|
||||
@@ -0,0 +1,16 @@
|
||||
# Refactor Workflow
|
||||
|
||||
1. Understand the current behavior that must be preserved.
|
||||
2. Inspect the relevant modules and dependencies.
|
||||
3. Identify safe refactor boundaries.
|
||||
4. Propose a scoped plan.
|
||||
5. Refactor incrementally.
|
||||
6. Review for:
|
||||
- preserved behavior
|
||||
- reduced complexity
|
||||
- improved readability
|
||||
- regression risk
|
||||
7. Summarize:
|
||||
- what was simplified
|
||||
- what was preserved
|
||||
- remaining debt
|
||||
@@ -0,0 +1,15 @@
|
||||
# Release Readiness Workflow
|
||||
|
||||
1. Review the scope of changes.
|
||||
2. Check:
|
||||
- build readiness
|
||||
- environment assumptions
|
||||
- configuration changes
|
||||
- database implications
|
||||
- CI/CD impact
|
||||
- rollback expectations
|
||||
3. Identify release risks and missing checks.
|
||||
4. Summarize:
|
||||
- release blockers
|
||||
- risks
|
||||
- final recommended checks
|
||||
+2
-6
@@ -52,11 +52,7 @@ pages/holding-min.html
|
||||
pages/holding.html
|
||||
|
||||
# Cline / AI-assistant governance artifacts (keep local-only)
|
||||
.clinerules
|
||||
.clinerules/
|
||||
CONTRIBUTING_AI.md
|
||||
GUARDRAILS.md
|
||||
|
||||
ai-prompts/
|
||||
workflows/
|
||||
AI_CONTEXT.md
|
||||
|
||||
pages/baracuda.min.html
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# AI Context — PEDW FrontEnd
|
||||
|
||||
## Overview
|
||||
|
||||
PEDW FrontEnd is a Next.js 14 (Pages Router) bilingual (EN/CY) public-service portal for planning case search, case detail/document viewing, and authenticated portal workflows (appeals, representations, watchlists, dashboard views).
|
||||
|
||||
## Architecture
|
||||
|
||||
- Monorepo-style Next.js app with UI routes in `pages/` and API handlers in `pages/api/**`.
|
||||
- Redux (`next-redux-wrapper` + `redux-persist`) for shared client/server state.
|
||||
- `next-auth` for auth/session with Prisma adapter.
|
||||
- Integration-heavy BFF pattern: API routes proxy to relay/CRM and storage/notify services.
|
||||
|
||||
## Key components
|
||||
|
||||
- `components/breadcrumbs.js`: central breadcrumb/back-link behavior.
|
||||
- `components/case/summary.js`: case-detail tabs, representation eligibility logic, watch/email actions.
|
||||
- `actions/index.js`: shared API utilities, token/header/hash helpers, many side-effect wrappers.
|
||||
- `pages/api/endpoint/**`: relay-backed business-data APIs.
|
||||
- `pages/api/file/**`: blob/document/PDF/upload endpoints.
|
||||
|
||||
## Data model boundaries
|
||||
|
||||
- Prisma (`prisma/schema.prisma`) stores auth/session entities only (`User`, `Account`, `Session`, `VerificationToken`) on SQL Server.
|
||||
- Business/case data is external (Dynamics/relay path), not persisted as Prisma domain models here.
|
||||
|
||||
## API/integration patterns
|
||||
|
||||
- Many endpoint handlers compute HMAC hash query params (`hash`) for relay-bound requests.
|
||||
- Token acquisition + OData headers are reused across proxy handlers.
|
||||
- File endpoints often validate request hash for sensitive blob actions.
|
||||
- GOV.UK Notify used for email, including language-specific template selection in some flows.
|
||||
|
||||
## Infrastructure/tooling
|
||||
|
||||
- Scripts: `npm run dev`, `npm run build`, `npm start`, `npm run lint`.
|
||||
- CI/CD artifacts present: `azure-pipelines.yml`, `Jenkinsfile`, `Dockerfile` (active production source-of-truth not explicit in repo).
|
||||
- Security controls: `middleware.js` CSP/runtime headers + `next.config.js` security headers.
|
||||
|
||||
## Development constraints
|
||||
|
||||
- Preserve auth/session behavior in `pages/api/auth/[...nextauth].js`.
|
||||
- Do not weaken CSP/security headers in `middleware.js` or `next.config.js`.
|
||||
- Keep EN/CY parity across routes and locale resources (`i18n.js`, `locales/**`, rewrites).
|
||||
- Treat relay hash behavior as security-sensitive; keep path/hash compatibility stable.
|
||||
- Avoid logging secrets/personal data in auth/email/file/account flows.
|
||||
@@ -0,0 +1,83 @@
|
||||
# PEDW FrontEnd Guardrails
|
||||
|
||||
## Purpose
|
||||
|
||||
This document is a fast pre-flight checklist to reduce regressions in high-risk areas. Use it before coding, before opening a PR, and before merge.
|
||||
|
||||
## Non-Negotiable Guardrails
|
||||
|
||||
1. **Auth/session integrity**
|
||||
- Do not bypass `next-auth` flow in `pages/api/auth/[...nextauth].js`.
|
||||
- Preserve secure redirect behavior and cookie/session settings.
|
||||
2. **Security headers and CSP**
|
||||
- Do not weaken `middleware.js` CSP/header behavior or `next.config.js` security headers without explicit rationale.
|
||||
3. **Data model safety**
|
||||
- Treat `prisma/schema.prisma` as source-of-truth for auth/account persistence.
|
||||
4. **Bilingual parity (EN/CY)**
|
||||
- Any user-facing route/content change must validate both locales.
|
||||
- Keep `i18n.js`, `locales/`, and `next.config.js` rewrites aligned.
|
||||
5. **Public-service reliability**
|
||||
- Avoid breaking core flows: search, case, account, myportal, admin.
|
||||
|
||||
## Sensitive Flow Protection
|
||||
|
||||
Apply extra checks for:
|
||||
|
||||
- `pages/api/auth/**`
|
||||
- `pages/api/file/**`
|
||||
- `pages/api/email/**`
|
||||
- `pages/api/endpoint/*_api.js` handling account/user data
|
||||
|
||||
## Relay Hash Integrity Rules (CRM-bound APIs)
|
||||
|
||||
Apply when changing relay-bound portal endpoints and helpers (notably `pages/api/endpoint/**` and `actions/index.js`):
|
||||
|
||||
1. Keep relay endpoint configuration sourced from `API_ROOT`.
|
||||
2. Preserve path-hash generation behavior:
|
||||
- hash input must be request path/query (excluding domain),
|
||||
- appended hash parameter must remain compatible with relay expectations.
|
||||
3. Do not change hash algorithm/key usage contract without coordinated relay change.
|
||||
4. Treat hash validation failures as security-relevant negative paths; verify graceful rejection handling.
|
||||
5. Never log hash key material or sensitive request payloads.
|
||||
|
||||
Required for sensitive changes:
|
||||
|
||||
- Negative-path validation (unauthorized, invalid input, malformed payload).
|
||||
- Safe logging (no secrets/tokens/personal data in cleartext).
|
||||
- Explicit rollback steps.
|
||||
|
||||
## Relay Policy Change Guardrail (timeouts/retries/logging)
|
||||
|
||||
When changing shared relay forwarding policy (for example in `pages/api/middleware/relayForwarding.js`), treat this as an operationally sensitive change even if endpoint contracts are unchanged.
|
||||
|
||||
Minimum required before merge:
|
||||
|
||||
1. Complete the relay governance gate in `context/runbook.md` (Relay Hardening Rollout Playbook).
|
||||
2. Attach non-prod smoke evidence for:
|
||||
- deterministic non-retry classes (`400`, `401`, `403`, `404`)
|
||||
- transient retry classes (`429`, `503`, timeout/network-transient)
|
||||
3. Confirm structured redacted logging and duplicate-log suppression behavior.
|
||||
4. Provide fast mitigation + rollback path (`RELAY_RETRY_MAX=0` and commit-revert path).
|
||||
|
||||
## Pre-PR Quick Checklist
|
||||
|
||||
- [ ] Ran `npm run lint` (or documented why unavailable)
|
||||
- [ ] Verified changed routes/APIs manually
|
||||
- [ ] Verified EN + CY behavior for impacted user-facing flow
|
||||
- [ ] Performed accessibility smoke checks (keyboard, focus, labels, headings)
|
||||
- [ ] Added risk notes (auth/data/i18n/a11y)
|
||||
- [ ] Updated `memory-bank/change-log.md` for non-trivial changes
|
||||
|
||||
## If Assumptions Are Unclear
|
||||
|
||||
1. Record assumptions in PR notes.
|
||||
2. Add unresolved items to `memory-bank/open-questions.md`.
|
||||
3. Choose the safer behavior and clearly mark as temporary.
|
||||
|
||||
## Related Docs
|
||||
|
||||
- `.clinerules`
|
||||
- `CONTRIBUTING_AI.md`
|
||||
- `context/runbook.md`
|
||||
- `context/integration-map.md`
|
||||
- `memory-bank/README.md`
|
||||
@@ -262,7 +262,7 @@ const RenderRadio = ({
|
||||
{
|
||||
["documentCheckList_" +
|
||||
id]:
|
||||
custom.requiredDocumentLabel,
|
||||
custom.requiredDocumentLabel
|
||||
}
|
||||
))
|
||||
: delete docListObj[
|
||||
@@ -297,7 +297,7 @@ function Radiofield(props) {
|
||||
formProps,
|
||||
parentFieldShowOnValue,
|
||||
validation,
|
||||
hint,
|
||||
hint
|
||||
} = props;
|
||||
|
||||
const router = useRouter();
|
||||
@@ -438,22 +438,25 @@ let AboutYou = (props) => {
|
||||
|
||||
const router = useRouter();
|
||||
const { locale } = router;
|
||||
const isWelsh = router.locale == "cy";
|
||||
|
||||
const {
|
||||
onSubmit,
|
||||
handleSubmit,
|
||||
appellantAgentValue,
|
||||
setFormStep,
|
||||
updateField,
|
||||
updateField
|
||||
} = props;
|
||||
|
||||
const [isAgent, setIsAgent] = useState(false);
|
||||
const isAgentSelected = appellantAgentValue == "846040001";
|
||||
const isAppellantSelected = appellantAgentValue == "846040000";
|
||||
|
||||
const yesNo = router.locale == "cy" ? ["Ydw", "Na"] : ["Yes", "No"];
|
||||
const appellantAgent =
|
||||
router.locale == "cy" ? ["Apelydd", "Asiant"] : ["Appellant", "Agent"];
|
||||
const contactPref =
|
||||
router.locale == "cy" ? ["Ebost", "Post"] : ["Email", "Post"];
|
||||
const yesNo = isWelsh ? ["Ydw", "Na"] : ["Yes", "No"];
|
||||
const appellantAgent = isWelsh
|
||||
? ["Apelydd", "Asiant"]
|
||||
: ["Appellant", "Agent"];
|
||||
const contactPref = isWelsh ? ["Ebost", "Post"] : ["Email", "Post"];
|
||||
|
||||
const required = (value) =>
|
||||
value ? undefined : t("newappeal:is-required-label");
|
||||
@@ -493,7 +496,7 @@ let AboutYou = (props) => {
|
||||
};
|
||||
|
||||
const handlePartialUpdate = () => {
|
||||
if (appellantAgentValue == "846040001" && isAgent == false) {
|
||||
if (isAgentSelected && isAgent == false) {
|
||||
updateField("caseForm", "pinswg_appellantfirstname", "");
|
||||
updateField("caseForm", "pinswg_appellantlastname", "");
|
||||
updateField("caseForm", "pinswg_appellantemailaddress", "");
|
||||
@@ -570,7 +573,7 @@ let AboutYou = (props) => {
|
||||
errorMsg={t("newappeal:select-an-option-label")}
|
||||
hint={t("newappeal:about-you-who-are-you-hint-label")}
|
||||
/>
|
||||
{appellantAgentValue == "846040000" && (
|
||||
{isAppellantSelected && (
|
||||
<>
|
||||
<h2 className="govuk-heading-s">
|
||||
{t("newappeal:about-you-appellant-contact-details")}
|
||||
@@ -668,7 +671,7 @@ let AboutYou = (props) => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{appellantAgentValue == "846040001" && (
|
||||
{isAgentSelected && (
|
||||
<>
|
||||
<h2 className="govuk-heading-s">
|
||||
{t("newappeal:about-you-appellant-contact-details")}
|
||||
@@ -891,15 +894,15 @@ const mapStateToProps = (state) => {
|
||||
pinswg_appellantaddress:
|
||||
state.accountDetails.accountDetails.address1_composite,
|
||||
pinswg_appellantphonenumber:
|
||||
state.accountDetails.accountDetails.telephone1,
|
||||
},
|
||||
state.accountDetails.accountDetails.telephone1
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
updateField: (form, field, newValue) =>
|
||||
dispatch(change(form, field, newValue)),
|
||||
dispatch(change(form, field, newValue))
|
||||
};
|
||||
};
|
||||
|
||||
@@ -915,7 +918,7 @@ AboutYou = connect((state) => {
|
||||
const appellantAgentValue = selector(state, "pinswg_appellantagent");
|
||||
|
||||
return {
|
||||
appellantAgentValue,
|
||||
appellantAgentValue
|
||||
};
|
||||
})(AboutYou);
|
||||
|
||||
@@ -928,6 +931,6 @@ export default connect(
|
||||
reduxForm({
|
||||
form: "caseForm",
|
||||
enableReinitialize: true, // this is needed!!
|
||||
destroyOnUnmount: false,
|
||||
destroyOnUnmount: false
|
||||
})(AboutYou)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const CheckSectionHeading = ({ title }) => {
|
||||
return <h1 className="govuk-heading-m">{title}</h1>;
|
||||
};
|
||||
|
||||
export default CheckSectionHeading;
|
||||
@@ -0,0 +1,63 @@
|
||||
const ConfirmationCheckboxGroup = ({
|
||||
downloadAppealForm,
|
||||
onDownloadChange,
|
||||
onConfirmChange,
|
||||
downloadLabel,
|
||||
confirmLabel,
|
||||
paragraphOne,
|
||||
paragraphTwo,
|
||||
paragraphTwoLink
|
||||
}) => {
|
||||
return (
|
||||
<div className="govuk-form-group ">
|
||||
<fieldset className="govuk-fieldset">
|
||||
<div className="govuk-checkboxes govuk-checkboxes--small">
|
||||
<div className="govuk-checkboxes__item">
|
||||
<input
|
||||
name="pinswg_downloadAppealForm"
|
||||
id="pinswg_downloadAppealForm"
|
||||
type="checkbox"
|
||||
className="govuk-checkboxes__input"
|
||||
checked={downloadAppealForm}
|
||||
onChange={onDownloadChange}
|
||||
/>
|
||||
<label
|
||||
className="govuk-label govuk-checkboxes__label"
|
||||
htmlFor="pinswg_downloadAppealForm"
|
||||
>
|
||||
{downloadLabel}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="govuk-checkboxes govuk-checkboxes--small">
|
||||
<div className="govuk-checkboxes__item">
|
||||
<input
|
||||
name="pinswg_checkconfirmation"
|
||||
id="pinswg_checkconfirmation"
|
||||
type="checkbox"
|
||||
className="govuk-checkboxes__input"
|
||||
value=""
|
||||
onChange={onConfirmChange}
|
||||
/>
|
||||
<label
|
||||
className="govuk-label govuk-checkboxes__label"
|
||||
htmlFor="pinswg_checkconfirmation"
|
||||
>
|
||||
{confirmLabel}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div>
|
||||
<p className="govuk-body">{paragraphOne}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="govuk-body">
|
||||
{paragraphTwo} {paragraphTwoLink}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationCheckboxGroup;
|
||||
@@ -0,0 +1,19 @@
|
||||
const DocsOfflineNotice = ({ noticeText, returnLink }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="govuk-warning-text ">
|
||||
<span className="govuk-warning-text__icon" aria-hidden="true">
|
||||
!
|
||||
</span>
|
||||
<strong className="govuk-warning-text__text govuk-!-margin-top-2">
|
||||
<span className="govuk-visually-hidden">Notice</span>
|
||||
{noticeText}
|
||||
{" "}
|
||||
</strong>
|
||||
</div>{" "}
|
||||
{returnLink}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocsOfflineNotice;
|
||||
@@ -0,0 +1,11 @@
|
||||
const FinalisingStatePanel = ({ heading, label, waitLabel }) => {
|
||||
return (
|
||||
<div className="">
|
||||
<h3 className="govuk-heading-m govuk-!-margin-top-9">{heading}</h3>
|
||||
<p className="govuk-body">{label}</p>
|
||||
<p className="govuk-body textloading">{waitLabel}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FinalisingStatePanel;
|
||||
@@ -0,0 +1,5 @@
|
||||
const SectionReviewList = ({ children }) => {
|
||||
return <dl className="govuk-summary-list ">{children}</dl>;
|
||||
};
|
||||
|
||||
export default SectionReviewList;
|
||||
@@ -0,0 +1,5 @@
|
||||
const SubmitActionsPanel = ({ children }) => {
|
||||
return <div className="govuk-button-group">{children}</div>;
|
||||
};
|
||||
|
||||
export default SubmitActionsPanel;
|
||||
@@ -0,0 +1,31 @@
|
||||
const ErrorSummary = ({ hasErrors, errorItems }) => {
|
||||
return hasErrors ? (
|
||||
<div
|
||||
className="govuk-error-summary"
|
||||
aria-labelledby="error-summary-title"
|
||||
role="alert"
|
||||
tabIndex="-1"
|
||||
data-module="govuk-error-summary"
|
||||
>
|
||||
<h2 className="govuk-error-summary__title" id="error-summary-title">
|
||||
The following fields have a problem :
|
||||
</h2>
|
||||
<div className="govuk-error-summary__body">
|
||||
<ul className="govuk-list govuk-error-summary__list">
|
||||
{errorItems.map((item, i) => (
|
||||
<li key={i}>
|
||||
<a href={"#" + item.field + "_label"}>
|
||||
{" "}
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorSummary;
|
||||
@@ -0,0 +1,5 @@
|
||||
const PrimaryActionButtons = ({ children }) => {
|
||||
return <div className="govuk-button-group">{children}</div>;
|
||||
};
|
||||
|
||||
export default PrimaryActionButtons;
|
||||
@@ -0,0 +1,14 @@
|
||||
const SaveStatusIndicator = ({ text }) => {
|
||||
return (
|
||||
<span className=" progress-save-active">
|
||||
{text}
|
||||
<img
|
||||
className="data-loading-icon"
|
||||
src="/assets/images/loading.gif"
|
||||
alt={text}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaveStatusIndicator;
|
||||
@@ -0,0 +1,5 @@
|
||||
const SectionHeader = ({ title }) => {
|
||||
return <h1 className="govuk-heading-m ">{title}</h1>;
|
||||
};
|
||||
|
||||
export default SectionHeader;
|
||||
@@ -0,0 +1,12 @@
|
||||
const SidebarProgressPanel = ({ children }) => {
|
||||
return (
|
||||
<div className="govuk-grid-column-one-third">
|
||||
<div className="at-nav-container">
|
||||
{children}
|
||||
<hr className="govuk-section-break govuk-section-break--m govuk-section-break--visible" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarProgressPanel;
|
||||
@@ -2,23 +2,34 @@ import { JSONPath as jsonpath } from "jsonpath-plus";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import { useRouter } from "next/router";
|
||||
import { connect } from "react-redux";
|
||||
import xpath from "xpath";
|
||||
import {
|
||||
getPickListLabel,
|
||||
getRadioLabel,
|
||||
bytesToSize,
|
||||
getDocumentTypeFromFilename,
|
||||
getThumbnailIconByExtension,
|
||||
getThumbnailIconByExtension
|
||||
} from "../../components/utils";
|
||||
import fieldLookup from "../../data/crmfieldlookuptranslations.json";
|
||||
|
||||
import {
|
||||
setCurrentSection,
|
||||
setFormComplete,
|
||||
setFormComplete
|
||||
} from "../../store/appealType/action";
|
||||
import {
|
||||
parseRowOuterHTML,
|
||||
parseXml,
|
||||
selectCurrentSectionRows,
|
||||
selectRowDataFieldName,
|
||||
selectRowDocumentTypeCode,
|
||||
selectRowFieldType,
|
||||
selectRowLabelDescriptions
|
||||
} from "../../lib/newappeal/formDerivation";
|
||||
|
||||
let BuildCheckRow = (props) => {
|
||||
let { t } = useTranslation();
|
||||
const legacyProps = props.props;
|
||||
const legacyFormState = legacyProps.form;
|
||||
const legacyFormData = legacyProps.formData;
|
||||
|
||||
const router = useRouter();
|
||||
const { locale } = router;
|
||||
@@ -30,18 +41,12 @@ let BuildCheckRow = (props) => {
|
||||
onSubmit,
|
||||
updateCurrentSection,
|
||||
setCurrentSection,
|
||||
mandatoryFieldsData,
|
||||
mandatoryFieldsData
|
||||
} = props;
|
||||
|
||||
const parser = new DOMParser();
|
||||
var doc = parser.parseFromString(props.formXML, "text/xml");
|
||||
var doc = parseXml(props.formXML);
|
||||
|
||||
var rowXML = xpath.select(
|
||||
"/form/tabs/tab[" +
|
||||
whichSection +
|
||||
"]/columns//sections/section/rows/row",
|
||||
doc
|
||||
);
|
||||
var rowXML = selectCurrentSectionRows(doc, whichSection);
|
||||
|
||||
function formatDate(dateObj) {
|
||||
var m = new Date(dateObj);
|
||||
@@ -66,7 +71,7 @@ let BuildCheckRow = (props) => {
|
||||
let formObj = jsonpath({
|
||||
path: "$['" + appealtypes + "']",
|
||||
json: fieldLookup,
|
||||
eval: true,
|
||||
eval: true
|
||||
});
|
||||
|
||||
let labelTrans =
|
||||
@@ -74,7 +79,7 @@ let BuildCheckRow = (props) => {
|
||||
? jsonpath({
|
||||
path: '$..[?(@ && @.value=="' + label + '")].value_cy',
|
||||
json: formObj,
|
||||
eval: true,
|
||||
eval: true
|
||||
})
|
||||
: label;
|
||||
return labelTrans;
|
||||
@@ -91,29 +96,23 @@ let BuildCheckRow = (props) => {
|
||||
return (
|
||||
<>
|
||||
{Object.keys(rowXML).map((key, index) => {
|
||||
var doc = parser.parseFromString(
|
||||
rowXML[key].outerHTML,
|
||||
"text/xml"
|
||||
);
|
||||
var doc = parseRowOuterHTML(rowXML[key].outerHTML);
|
||||
if (_.isEmpty(rowXML[key].innerHTML)) {
|
||||
("");
|
||||
} else {
|
||||
var rows = xpath.select("//label/@description", doc);
|
||||
var fieldtype = xpath.select("//@classid", doc);
|
||||
var datafieldname = xpath.select("//@datafieldname", doc);
|
||||
var rows = selectRowLabelDescriptions(doc);
|
||||
var fieldtype = selectRowFieldType(doc);
|
||||
var datafieldname = selectRowDataFieldName(doc);
|
||||
const isRequiredField = jsonpath({
|
||||
path:
|
||||
"$..value[?(@ && @.LogicalName=='" +
|
||||
datafieldname[0].value +
|
||||
"')]",
|
||||
json: mandatoryFieldsData,
|
||||
eval: true,
|
||||
eval: true
|
||||
});
|
||||
|
||||
var documentTypeCode = xpath.select(
|
||||
"//@ishareDocumentCode",
|
||||
doc
|
||||
);
|
||||
var documentTypeCode = selectRowDocumentTypeCode(doc);
|
||||
|
||||
// if (datafieldname[0].value.indexOf("fileUpload") > 0) {
|
||||
// isRequiredField.push({
|
||||
@@ -142,66 +141,54 @@ let BuildCheckRow = (props) => {
|
||||
// datafieldname[0].value
|
||||
// ] != "undefined"
|
||||
// ) {
|
||||
return (
|
||||
<div className="govuk-summary-list__row" key={key}>
|
||||
<dt className="govuk-summary-list__key">
|
||||
{FieldsTranslations(rows[0].value)}
|
||||
</dt>
|
||||
const fieldTypeValue = fieldtype[0].value;
|
||||
const fieldValue =
|
||||
legacyFormState["appealForm"].values[
|
||||
datafieldname[0].value
|
||||
];
|
||||
|
||||
<dd className="govuk-summary-list__value">
|
||||
{fieldtype[0].value ==
|
||||
"{E0DECE4B-6666-4a8f-A065-082708572369}" ? (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: props.props.form[
|
||||
"appealForm"
|
||||
].values[datafieldname[0].value],
|
||||
}}
|
||||
/>
|
||||
) : fieldtype[0].value ==
|
||||
"{5B773807-9FB2-42db-97C3-7A91EFF8ADFF}" ? (
|
||||
formatDate(
|
||||
props.props.form["appealForm"].values[
|
||||
datafieldname[0].value
|
||||
]
|
||||
)
|
||||
) : fieldtype[0].value ==
|
||||
"{3EF39988-22BB-4f0b-BBBE-64B5A3748AEE}" ? (
|
||||
getPickListLabel(
|
||||
props.props.formData.pickListData,
|
||||
datafieldname[0].value,
|
||||
props.props.form["appealForm"].values[
|
||||
datafieldname[0].value
|
||||
]
|
||||
)
|
||||
) : fieldtype[0].value ==
|
||||
"{07FAC785-CD58-4f9f-ABB3-4B7DDC6ED5ED}" ? (
|
||||
getRadioLabel(
|
||||
props.props.formData.pickListData,
|
||||
datafieldname[0].value,
|
||||
props.props.form["appealForm"].values[
|
||||
datafieldname[0].value
|
||||
]
|
||||
)
|
||||
) : fieldtype[0].value ==
|
||||
"{67FAC785-CD58-4f9f-ABB3-4B7DDC6ED5ED}" ? (
|
||||
props.props.form["appealForm"].values[
|
||||
datafieldname[0].value
|
||||
] == "false" ? (
|
||||
router.locale == "cy" ? (
|
||||
"Nac ydw"
|
||||
) : (
|
||||
"No"
|
||||
)
|
||||
) : router.locale == "cy" ? (
|
||||
"Oes"
|
||||
) : (
|
||||
"Yes"
|
||||
)
|
||||
) : fieldtype[0].value ==
|
||||
"{16D63FD6-119B-4353-BDCA-18358721C3FE}" ? (
|
||||
<>
|
||||
{props.filesArray.map((blob, i) => (
|
||||
const renderFileUploadValue = () => (
|
||||
<>
|
||||
{props.filesArray.map((blob, i) => (
|
||||
<div key={i}>
|
||||
{getDocumentTypeFromFilename(blob.name) ===
|
||||
documentTypeCode[0].value && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start"
|
||||
}}
|
||||
className="govuk-!-margin-bottom-2"
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
minWidth: "50",
|
||||
width: "50"
|
||||
}}
|
||||
src={getThumbnailIconByExtension(
|
||||
blob.name
|
||||
)}
|
||||
alt={blob.name}
|
||||
width="50"
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
maxWidth: "85%"
|
||||
}}
|
||||
className="govuk-body govuk-!-font-size-14 govuk-!-margin-left-2"
|
||||
>
|
||||
{blob.name} -{" "}
|
||||
{bytesToSize(blob.size)}{" "}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<br />
|
||||
{typeof fieldValue != "undefined" &&
|
||||
fieldValue.map(
|
||||
(blob, i) =>
|
||||
isObjectAndNotEmpty(blob) && (
|
||||
<div key={i}>
|
||||
{getDocumentTypeFromFilename(
|
||||
blob.name
|
||||
@@ -212,14 +199,14 @@ let BuildCheckRow = (props) => {
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems:
|
||||
"flex-start",
|
||||
"flex-start"
|
||||
}}
|
||||
className="govuk-!-margin-bottom-2"
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
minWidth: "50",
|
||||
width: "50",
|
||||
width: "50"
|
||||
}}
|
||||
src={getThumbnailIconByExtension(
|
||||
blob.name
|
||||
@@ -229,7 +216,7 @@ let BuildCheckRow = (props) => {
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
maxWidth: "85%",
|
||||
maxWidth: "85%"
|
||||
}}
|
||||
className="govuk-body govuk-!-font-size-14 govuk-!-margin-left-2"
|
||||
>
|
||||
@@ -241,124 +228,96 @@ let BuildCheckRow = (props) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<br />
|
||||
{typeof props.props.form["appealForm"]
|
||||
.values[datafieldname[0].value] !=
|
||||
"undefined" &&
|
||||
props.props.form[
|
||||
"appealForm"
|
||||
].values[
|
||||
datafieldname[0].value
|
||||
].map(
|
||||
(blob, i) =>
|
||||
isObjectAndNotEmpty(
|
||||
blob
|
||||
) && (
|
||||
<div key={i}>
|
||||
{getDocumentTypeFromFilename(
|
||||
blob.name
|
||||
) ===
|
||||
documentTypeCode[0]
|
||||
.value && (
|
||||
<div
|
||||
style={{
|
||||
display:
|
||||
"flex",
|
||||
alignItems:
|
||||
"flex-start",
|
||||
}}
|
||||
className="govuk-!-margin-bottom-2"
|
||||
>
|
||||
<img
|
||||
style={{
|
||||
minWidth:
|
||||
"50",
|
||||
width: "50",
|
||||
}}
|
||||
src={getThumbnailIconByExtension(
|
||||
blob.name
|
||||
)}
|
||||
alt={
|
||||
blob.name
|
||||
}
|
||||
width="50"
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
maxWidth:
|
||||
"85%",
|
||||
}}
|
||||
className="govuk-body govuk-!-font-size-14 govuk-!-margin-left-2"
|
||||
>
|
||||
{
|
||||
blob.name
|
||||
}{" "}
|
||||
-{" "}
|
||||
{bytesToSize(
|
||||
blob.size
|
||||
)}{" "}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
) : fieldtype[0].value ==
|
||||
"{0273EDBD-AC1D-40d3-9FB2-095C621B552D}" ? (
|
||||
typeof props.props.form["appealForm"]
|
||||
.values[datafieldname[0].value] !=
|
||||
"undefined" &&
|
||||
props.props.form["appealForm"].values[
|
||||
datafieldname[0].value
|
||||
] != null ? (
|
||||
props.props.form["appealForm"].values[
|
||||
datafieldname[0].value
|
||||
].map((tenant, index) => {
|
||||
console.log(
|
||||
typeof tenant.pinswg_owners_firstname
|
||||
);
|
||||
return typeof tenant.pinswg_owners_firstname !=
|
||||
"undefined" ||
|
||||
typeof tenant.pinswg_agriculturaltenantname_firstname !=
|
||||
"undefined" ? (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<div className="govuk-!-width-one-half">
|
||||
{tenant?.pinswg_agriculturaltenantname_firstname &&
|
||||
tenant.pinswg_agriculturaltenantname_firstname +
|
||||
" " +
|
||||
tenant.pinswg_agriculturaltenantname_lastname}
|
||||
|
||||
{tenant?.pinswg_owners_firstname &&
|
||||
tenant.pinswg_owners_firstname +
|
||||
" " +
|
||||
tenant.pinswg_owners_lastname}
|
||||
</div>
|
||||
<div className="govuk-!-width-one-half">
|
||||
Served:{" "}
|
||||
{formatDate(
|
||||
tenant.dateserved
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
);
|
||||
})
|
||||
) : (
|
||||
"N/A"
|
||||
)
|
||||
) : (
|
||||
props.props.form["appealForm"].values[
|
||||
datafieldname[0].value
|
||||
]
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const renderTenantValue = () =>
|
||||
typeof fieldValue != "undefined" && fieldValue != null
|
||||
? fieldValue.map((tenant, index) => {
|
||||
console.log(
|
||||
typeof tenant.pinswg_owners_firstname
|
||||
);
|
||||
return typeof tenant.pinswg_owners_firstname !=
|
||||
"undefined" ||
|
||||
typeof tenant.pinswg_agriculturaltenantname_firstname !=
|
||||
"undefined" ? (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: "flex"
|
||||
}}
|
||||
>
|
||||
<div className="govuk-!-width-one-half">
|
||||
{tenant?.pinswg_agriculturaltenantname_firstname &&
|
||||
tenant.pinswg_agriculturaltenantname_firstname +
|
||||
" " +
|
||||
tenant.pinswg_agriculturaltenantname_lastname}
|
||||
|
||||
{tenant?.pinswg_owners_firstname &&
|
||||
tenant.pinswg_owners_firstname +
|
||||
" " +
|
||||
tenant.pinswg_owners_lastname}
|
||||
</div>
|
||||
<div className="govuk-!-width-one-half">
|
||||
Served:{" "}
|
||||
{formatDate(tenant.dateserved)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
);
|
||||
})
|
||||
: "N/A";
|
||||
|
||||
const formatterMap = {
|
||||
"{E0DECE4B-6666-4a8f-A065-082708572369}": () => (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: fieldValue
|
||||
}}
|
||||
/>
|
||||
),
|
||||
"{5B773807-9FB2-42db-97C3-7A91EFF8ADFF}": () =>
|
||||
formatDate(fieldValue),
|
||||
"{3EF39988-22BB-4f0b-BBBE-64B5A3748AEE}": () =>
|
||||
getPickListLabel(
|
||||
legacyFormData.pickListData,
|
||||
datafieldname[0].value,
|
||||
fieldValue
|
||||
),
|
||||
"{07FAC785-CD58-4f9f-ABB3-4B7DDC6ED5ED}": () =>
|
||||
getRadioLabel(
|
||||
legacyFormData.pickListData,
|
||||
datafieldname[0].value,
|
||||
fieldValue
|
||||
),
|
||||
"{67FAC785-CD58-4f9f-ABB3-4B7DDC6ED5ED}": () =>
|
||||
fieldValue == "false"
|
||||
? router.locale == "cy"
|
||||
? "Nac ydw"
|
||||
: "No"
|
||||
: router.locale == "cy"
|
||||
? "Oes"
|
||||
: "Yes",
|
||||
"{16D63FD6-119B-4353-BDCA-18358721C3FE}":
|
||||
renderFileUploadValue,
|
||||
"{0273EDBD-AC1D-40d3-9FB2-095C621B552D}":
|
||||
renderTenantValue
|
||||
};
|
||||
|
||||
const defaultFormatter = () => fieldValue;
|
||||
const renderFieldValue =
|
||||
formatterMap[fieldTypeValue] || defaultFormatter;
|
||||
return (
|
||||
<div className="govuk-summary-list__row" key={key}>
|
||||
<dt className="govuk-summary-list__key">
|
||||
{FieldsTranslations(rows[0].value)}
|
||||
</dt>
|
||||
|
||||
<dd className="govuk-summary-list__value">
|
||||
{renderFieldValue()}
|
||||
</dd>
|
||||
|
||||
<dd className="govuk-summary-list__actions">
|
||||
@@ -397,12 +356,12 @@ const mapDispatchToProps = (dispatch) => {
|
||||
updateCurrentSection: (whichSection) => {
|
||||
dispatch(setCurrentSection(whichSection));
|
||||
dispatch(setFormComplete("true"));
|
||||
},
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
count: state.count,
|
||||
count: state.count
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(BuildCheckRow);
|
||||
|
||||
@@ -12,15 +12,26 @@ import {
|
||||
setFormComplete
|
||||
} from "../../store/appealType/action";
|
||||
import BuildCheckRow from "./buildcheckrow";
|
||||
|
||||
import { generateAppealPDF } from "../../actions/services/documentService";
|
||||
import { sendCaseCompleteMessage } from "../../actions/services/portalService";
|
||||
import {
|
||||
bytesToSize,
|
||||
getThumbnailIconByExtension,
|
||||
updateLinks,
|
||||
getDocLink
|
||||
} from "../utils";
|
||||
import {
|
||||
dedupeFilesListByName,
|
||||
removeDuplicatesByKey
|
||||
} from "../../lib/newappeal/fileListHelpers";
|
||||
import {
|
||||
generateAppealPDFEffect,
|
||||
sendCaseCompleteMessageEffect
|
||||
} from "../../lib/newappeal/journeyEffects";
|
||||
import CheckSectionHeading from "./buildCheckSection/CheckSectionHeading";
|
||||
import ConfirmationCheckboxGroup from "./buildCheckSection/ConfirmationCheckboxGroup";
|
||||
import DocsOfflineNotice from "./buildCheckSection/DocsOfflineNotice";
|
||||
import FinalisingStatePanel from "./buildCheckSection/FinalisingStatePanel";
|
||||
import SectionReviewList from "./buildCheckSection/SectionReviewList";
|
||||
import SubmitActionsPanel from "./buildCheckSection/SubmitActionsPanel";
|
||||
|
||||
let BuildCheckSection = (props) => {
|
||||
useEffect(() => {
|
||||
@@ -44,6 +55,11 @@ let BuildCheckSection = (props) => {
|
||||
docsOffline
|
||||
} = props;
|
||||
|
||||
const legacyProps = props.props;
|
||||
const legacyAppealType = legacyProps.appealType;
|
||||
const legacyFormState = legacyProps.form;
|
||||
const legacyAccountDetails = legacyProps.accountDetails;
|
||||
|
||||
const parser = new DOMParser();
|
||||
var doc = parser.parseFromString(props.formXML, "text/xml");
|
||||
var titles = xpath.select(
|
||||
@@ -78,7 +94,7 @@ let BuildCheckSection = (props) => {
|
||||
|
||||
let buildAppealFilesArray = [];
|
||||
//console.log("attached docs: - ", buildAppealFilesArray);
|
||||
buildAppealFilesArray = props.props.appealType.fileList?.value[0] || [];
|
||||
buildAppealFilesArray = legacyAppealType.fileList?.value[0] || [];
|
||||
//let filesUploadObj = buildAppealFilesArray;
|
||||
// let filesUploadObj = props.appealType.fileList.hasOwnProperty("value")
|
||||
// ? props.appealType.fileList.value[0]
|
||||
@@ -97,28 +113,18 @@ let BuildCheckSection = (props) => {
|
||||
});
|
||||
//console.log(buildAppealFilesArray);
|
||||
|
||||
function removeDuplicates(arr, key) {
|
||||
const seen = new Set();
|
||||
return arr.filter((item) => {
|
||||
const value = item[key];
|
||||
if (seen.has(value)) {
|
||||
return false; // Duplicate found
|
||||
} else {
|
||||
seen.add(value); // Add value to the set
|
||||
return true; // Keep the item
|
||||
}
|
||||
});
|
||||
}
|
||||
buildAppealFilesArray = removeDuplicatesByKey(
|
||||
buildAppealFilesArray,
|
||||
"name"
|
||||
);
|
||||
|
||||
buildAppealFilesArray = removeDuplicates(buildAppealFilesArray, "name");
|
||||
|
||||
let pdfObj = props.props.form.appealForm.values;
|
||||
let pdfObj = legacyFormState.appealForm.values;
|
||||
|
||||
pdfObj = updateLinks(pdfObj);
|
||||
|
||||
Object.assign(pdfObj, {
|
||||
"filesList": buildAppealFilesArray,
|
||||
"containerID": props.props.accountDetails.containerID,
|
||||
"containerID": legacyAccountDetails.containerID,
|
||||
"casefolderID": props.appealType.caseReference.ticketnumber
|
||||
});
|
||||
|
||||
@@ -152,19 +158,16 @@ let BuildCheckSection = (props) => {
|
||||
const finaliseAppeal = async () => {
|
||||
//console.log("finalising appeal:", pdfObj);
|
||||
|
||||
const uniqueArray = pdfObj.filesList.filter(
|
||||
(value, index, self) =>
|
||||
index === self.findIndex((t) => t.name === value.name)
|
||||
);
|
||||
const uniqueArray = dedupeFilesListByName(pdfObj.filesList);
|
||||
|
||||
pdfObj.filesList = uniqueArray;
|
||||
pdfObj.locale = locale;
|
||||
|
||||
confirmSections
|
||||
? (setFinaliseAppealProcess(true),
|
||||
await generateAppealPDF(
|
||||
await generateAppealPDFEffect(
|
||||
pdfObj,
|
||||
props.props.accountDetails.containerID,
|
||||
legacyAccountDetails.containerID,
|
||||
props.appealType.caseReference.ticketnumber,
|
||||
router.query.appealtypes,
|
||||
pdfObj.filesList
|
||||
@@ -172,10 +175,10 @@ let BuildCheckSection = (props) => {
|
||||
)
|
||||
.then((data) => {
|
||||
console.log("have generated pdf:", data);
|
||||
sendCaseCompleteMessage(
|
||||
props.props.accountDetails.containerID,
|
||||
sendCaseCompleteMessageEffect(
|
||||
legacyAccountDetails.containerID,
|
||||
props.appealType.caseReference.ticketnumber,
|
||||
props.props.accountDetails.accountDetails
|
||||
legacyAccountDetails.accountDetails
|
||||
.pinswg_typeofinvolvement
|
||||
);
|
||||
})
|
||||
@@ -197,18 +200,15 @@ let BuildCheckSection = (props) => {
|
||||
try {
|
||||
setDownloadInProgress(true);
|
||||
|
||||
const uniqueArray = pdfObj.filesList.filter(
|
||||
(value, index, self) =>
|
||||
index === self.findIndex((t) => t.name === value.name)
|
||||
);
|
||||
const uniqueArray = dedupeFilesListByName(pdfObj.filesList);
|
||||
|
||||
pdfObj.filesList = uniqueArray;
|
||||
pdfObj.locale = locale;
|
||||
|
||||
const containerID =
|
||||
props.accountDetails?.containerID ||
|
||||
props.props?.accountDetails?.containerID ||
|
||||
props.props?.props?.accountDetails?.containerID;
|
||||
legacyProps?.accountDetails?.containerID ||
|
||||
legacyProps?.props?.accountDetails?.containerID;
|
||||
const caseRef =
|
||||
props?.currentView?.caseReference?.ticketnumber ||
|
||||
props?.appealType?.caseReference?.ticketnumber;
|
||||
@@ -221,7 +221,7 @@ let BuildCheckSection = (props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const pdfBlob = await generateAppealPDF(
|
||||
const pdfBlob = await generateAppealPDFEffect(
|
||||
pdfObj,
|
||||
containerID,
|
||||
caseRef,
|
||||
@@ -253,10 +253,8 @@ let BuildCheckSection = (props) => {
|
||||
<div className="govuk-grid-row" key={1222}>
|
||||
<div className="govuk-grid-column-full">
|
||||
<div>
|
||||
<h1 className="govuk-heading-m">
|
||||
Are these answers correct?
|
||||
</h1>
|
||||
<dl className="govuk-summary-list ">
|
||||
<CheckSectionHeading title="Are these answers correct?" />
|
||||
<SectionReviewList>
|
||||
{Object.keys(titles).map((key) => (
|
||||
<div key={key}>
|
||||
<h2 className="govuk-heading-m govuk-!-margin-top-9">
|
||||
@@ -274,7 +272,7 @@ let BuildCheckSection = (props) => {
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</SectionReviewList>
|
||||
{/* <dl className="govuk-summary-list govuk-!-margin-bottom-9 at-summary-list">
|
||||
{buildAppealFilesArray.length > 0
|
||||
? buildAppealFilesArray.map((blob, i) => {
|
||||
@@ -329,120 +327,61 @@ let BuildCheckSection = (props) => {
|
||||
</dl> */}
|
||||
</div>
|
||||
{finaliseAppealProcess ? (
|
||||
<div className="">
|
||||
<h3 className="govuk-heading-m govuk-!-margin-top-9">
|
||||
{t("newappeal:new-appeal-finalising-heading")}
|
||||
</h3>
|
||||
<p className="govuk-body">
|
||||
{t("newappeal:new-appeal-finalising-label")}
|
||||
</p>
|
||||
<p className="govuk-body textloading">
|
||||
{t("newappeal:new-appeal-wait-label")}
|
||||
</p>
|
||||
</div>
|
||||
<FinalisingStatePanel
|
||||
heading={t("newappeal:new-appeal-finalising-heading")}
|
||||
label={t("newappeal:new-appeal-finalising-label")}
|
||||
waitLabel={t("newappeal:new-appeal-wait-label")}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="govuk-form-group ">
|
||||
<fieldset className="govuk-fieldset">
|
||||
<div className="govuk-checkboxes govuk-checkboxes--small">
|
||||
<div className="govuk-checkboxes__item">
|
||||
<input
|
||||
name="pinswg_downloadAppealForm"
|
||||
id="pinswg_downloadAppealForm"
|
||||
type="checkbox"
|
||||
className="govuk-checkboxes__input"
|
||||
checked={downloadAppealForm}
|
||||
onChange={(e) => {
|
||||
setDownloadAppealForm(
|
||||
e.target.checked
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
className="govuk-label govuk-checkboxes__label"
|
||||
htmlFor="pinswg_downloadAppealForm"
|
||||
>
|
||||
{t(
|
||||
"newappeal:new-appeal-check-download-label"
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="govuk-checkboxes govuk-checkboxes--small">
|
||||
<div className="govuk-checkboxes__item">
|
||||
<input
|
||||
name="pinswg_checkconfirmation"
|
||||
id="pinswg_checkconfirmation"
|
||||
type="checkbox"
|
||||
className="govuk-checkboxes__input"
|
||||
value=""
|
||||
onChange={(e) => {
|
||||
setConfirmSections(
|
||||
!confirmSections
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
className="govuk-label govuk-checkboxes__label"
|
||||
htmlFor="pinswg_checkconfirmation"
|
||||
>
|
||||
{t(
|
||||
"newappeal:new-appeal-check-confirm-label"
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div>
|
||||
<p className="govuk-body">
|
||||
{t(
|
||||
"newappeal:new-appeal-check-confirm-paragraph-one"
|
||||
<ConfirmationCheckboxGroup
|
||||
downloadAppealForm={downloadAppealForm}
|
||||
onDownloadChange={(e) => {
|
||||
setDownloadAppealForm(e.target.checked);
|
||||
}}
|
||||
onConfirmChange={(e) => {
|
||||
setConfirmSections(!confirmSections);
|
||||
}}
|
||||
downloadLabel={t(
|
||||
"newappeal:new-appeal-check-download-label"
|
||||
)}
|
||||
confirmLabel={t(
|
||||
"newappeal:new-appeal-check-confirm-label"
|
||||
)}
|
||||
paragraphOne={t(
|
||||
"newappeal:new-appeal-check-confirm-paragraph-one"
|
||||
)}
|
||||
paragraphTwo={t(
|
||||
"newappeal:new-appeal-check-confirm-paragraph-two"
|
||||
)}
|
||||
paragraphTwoLink={
|
||||
<Link
|
||||
target="_blank"
|
||||
href={t(
|
||||
"newappeal:new-appeal-check-confirm-paragraph-two-link"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="govuk-body">
|
||||
>
|
||||
{t(
|
||||
"newappeal:new-appeal-check-confirm-paragraph-two"
|
||||
)}{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
href={t(
|
||||
"newappeal:new-appeal-check-confirm-paragraph-two-link"
|
||||
)}
|
||||
>
|
||||
{t(
|
||||
"newappeal:new-appeal-check-confirm-paragraph-two-link"
|
||||
)}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
"newappeal:new-appeal-check-confirm-paragraph-two-link"
|
||||
)}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="govuk-button-group">
|
||||
<SubmitActionsPanel>
|
||||
{ShowDocLinks ? (
|
||||
<>
|
||||
<div className="govuk-warning-text ">
|
||||
<span
|
||||
className="govuk-warning-text__icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
!
|
||||
</span>
|
||||
<strong className="govuk-warning-text__text govuk-!-margin-top-2">
|
||||
<span className="govuk-visually-hidden">
|
||||
Notice
|
||||
</span>
|
||||
<DocsOfflineNotice
|
||||
noticeText={t(
|
||||
"myportal:docsoffline-newappeal-label"
|
||||
)}
|
||||
returnLink={
|
||||
<Link className="govuk-link" href="/">
|
||||
{t(
|
||||
"myportal:docsoffline-newappeal-label"
|
||||
"newappeal:return-to-myportal-link"
|
||||
)}
|
||||
{" "}
|
||||
</strong>
|
||||
</div>{" "}
|
||||
<Link className="govuk-link" href="/">
|
||||
{t("newappeal:return-to-myportal-link")}
|
||||
</Link>
|
||||
</>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
@@ -468,7 +407,7 @@ let BuildCheckSection = (props) => {
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</SubmitActionsPanel>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -209,7 +209,7 @@ export default function BuildField(props) {
|
||||
uploadCount={uploadCount}
|
||||
setFileCount={setFileCount}
|
||||
setUploadCount={setUploadCount}
|
||||
props={props?.props?.props?.props}
|
||||
props={pageProps}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
import _ from "lodash";
|
||||
import useTranslation from "next-translate/useTranslation";
|
||||
import { useRouter } from "next/router";
|
||||
import xpath from "xpath";
|
||||
import BuildField from "./buildfield";
|
||||
import {
|
||||
parseRowOuterHTML,
|
||||
selectRowDataFieldContent,
|
||||
selectRowDataFieldName,
|
||||
selectRowDateEnd,
|
||||
selectRowDateStart,
|
||||
selectRowDocumentTypeCode,
|
||||
selectRowFieldType,
|
||||
selectRowHint,
|
||||
selectRowLabelDescriptions,
|
||||
selectRowMaxFieldLength,
|
||||
selectRowMaxSubField,
|
||||
selectRowParentField,
|
||||
selectRowParentFieldShowOnValue,
|
||||
selectRowRequiredDocumentLabel,
|
||||
selectRowRequiredDocumentValue,
|
||||
selectRowValidation
|
||||
} from "../../lib/newappeal/formDerivation";
|
||||
|
||||
export default function BuildRow(props) {
|
||||
let { t } = useTranslation();
|
||||
const legacyProps = props.props;
|
||||
const legacyPageProps = legacyProps.props;
|
||||
|
||||
const router = useRouter();
|
||||
const { locale } = router;
|
||||
@@ -20,56 +39,39 @@ export default function BuildRow(props) {
|
||||
uploadCount,
|
||||
setFileCount,
|
||||
completedUploadFilesCount,
|
||||
setCompletedUploadFilesCount,
|
||||
setCompletedUploadFilesCount
|
||||
} = props;
|
||||
|
||||
const parser = new DOMParser();
|
||||
|
||||
return (
|
||||
<div className="govuk-grid-column-full">
|
||||
{Object.keys(rowXML).map((key, index) => {
|
||||
var doc = parser.parseFromString(
|
||||
rowXML[key].outerHTML,
|
||||
"text/xml"
|
||||
);
|
||||
var doc = parseRowOuterHTML(rowXML[key].outerHTML);
|
||||
if (_.isEmpty(rowXML[key].innerHTML)) {
|
||||
("");
|
||||
} else {
|
||||
var rows = xpath.select("//label/@description", doc);
|
||||
var fieldtype = xpath.select("//@classid", doc);
|
||||
var validation = xpath.select("//@validation", doc);
|
||||
var maxFieldLength = xpath.select("//@maxFieldLength", doc);
|
||||
var maxSubField = xpath.select("//@maxsubfield", doc);
|
||||
var datafieldname = xpath.select("//@datafieldname", doc);
|
||||
var datafieldcontent = xpath.select(
|
||||
"//@datafieldcontent",
|
||||
doc
|
||||
);
|
||||
var parentField = xpath.select("//@parentField", doc);
|
||||
var rows = selectRowLabelDescriptions(doc);
|
||||
var fieldtype = selectRowFieldType(doc);
|
||||
var validation = selectRowValidation(doc);
|
||||
var maxFieldLength = selectRowMaxFieldLength(doc);
|
||||
var maxSubField = selectRowMaxSubField(doc);
|
||||
var datafieldname = selectRowDataFieldName(doc);
|
||||
var datafieldcontent = selectRowDataFieldContent(doc);
|
||||
var parentField = selectRowParentField(doc);
|
||||
|
||||
var parentFieldShowOnValue = xpath.select(
|
||||
"//@parentFieldShowOnValue",
|
||||
doc
|
||||
);
|
||||
var parentFieldShowOnValue =
|
||||
selectRowParentFieldShowOnValue(doc);
|
||||
|
||||
var requiredDocumentValue = xpath.select(
|
||||
"//@requiredDocumentValue",
|
||||
doc
|
||||
);
|
||||
var requiredDocumentValue =
|
||||
selectRowRequiredDocumentValue(doc);
|
||||
|
||||
var requiredDocumentLabel = xpath.select(
|
||||
"//@requiredDocumentLabel",
|
||||
doc
|
||||
);
|
||||
var requiredDocumentLabel =
|
||||
selectRowRequiredDocumentLabel(doc);
|
||||
|
||||
var documentTypeCode = xpath.select(
|
||||
"//@ishareDocumentCode",
|
||||
doc
|
||||
);
|
||||
var documentTypeCode = selectRowDocumentTypeCode(doc);
|
||||
|
||||
var hint = xpath.select("//label/@hint", doc);
|
||||
var dateStart = xpath.select("//@dateStart", doc);
|
||||
var dateEnd = xpath.select("//@dateEnd", doc);
|
||||
var hint = selectRowHint(doc);
|
||||
var dateStart = selectRowDateStart(doc);
|
||||
var dateEnd = selectRowDateEnd(doc);
|
||||
|
||||
hint = !_.isEmpty(hint) && hint[0].value;
|
||||
|
||||
@@ -128,7 +130,7 @@ export default function BuildRow(props) {
|
||||
key={key}
|
||||
props={props}
|
||||
picklistData={
|
||||
props.props.props.formData.pickListData
|
||||
legacyPageProps.formData.pickListData
|
||||
}
|
||||
mandatoryFieldsData={mandatoryFieldsData}
|
||||
documentTypeCode={documentTypeCode}
|
||||
@@ -158,7 +160,7 @@ export default function BuildRow(props) {
|
||||
key={key}
|
||||
props={props}
|
||||
picklistData={
|
||||
props.props.props.formData.pickListData
|
||||
legacyPageProps.formData.pickListData
|
||||
}
|
||||
mandatoryFieldsData={mandatoryFieldsData}
|
||||
hint={hint}
|
||||
@@ -168,13 +170,13 @@ export default function BuildRow(props) {
|
||||
maxFieldLength={maxFieldLength}
|
||||
dateStart={dateStart}
|
||||
dateEnd={dateEnd}
|
||||
setDocumentsList={props.props.setDocumentsList}
|
||||
setDocumentsList={legacyProps.setDocumentsList}
|
||||
documentList={
|
||||
props.props.appealType.documentList
|
||||
legacyProps.appealType.documentList
|
||||
}
|
||||
documentTypeCode={documentTypeCode}
|
||||
setFilesForAppeal={
|
||||
props.props.setFilesForAppeal
|
||||
legacyProps.setFilesForAppeal
|
||||
}
|
||||
maxSubField={maxSubField}
|
||||
setFileCount={setFileCount}
|
||||
|
||||
@@ -4,9 +4,16 @@ import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { formValueSelector, reduxForm } from "redux-form";
|
||||
import xpath from "xpath";
|
||||
import { uploadFiles } from "../../actions/services/documentService";
|
||||
import { sendEmail } from "../../actions/services/notifyService";
|
||||
import { mergeWithExistingFilesList } from "../../lib/newappeal/fileListHelpers";
|
||||
import {
|
||||
uploadAppealFilesEffect,
|
||||
sendPartialSaveEmailEffect
|
||||
} from "../../lib/newappeal/journeyEffects";
|
||||
import {
|
||||
normalizeYesNoBooleans,
|
||||
removeNullKeys,
|
||||
stripInternalKeys
|
||||
} from "../../lib/newappeal/payloadCleanup";
|
||||
import {
|
||||
setCurrentSection,
|
||||
setDocumentsList,
|
||||
@@ -16,9 +23,22 @@ import {
|
||||
} from "../../store/appealType/action";
|
||||
import { getFormCollectionByID, getProgressObj, updateLinks } from "../utils";
|
||||
import BuildRow from "./buildrow";
|
||||
import {
|
||||
parseXml,
|
||||
selectAllTabTitleDescriptions,
|
||||
selectCurrentSectionRows,
|
||||
selectCurrentSectionTitleDescriptions,
|
||||
selectErrorFieldLabelDescription,
|
||||
selectFormTabs
|
||||
} from "../../lib/newappeal/formDerivation";
|
||||
|
||||
import { FieldsTranslations } from "../elements";
|
||||
import BuildProgress from "./buildprogress";
|
||||
import ErrorSummary from "./buildSection/ErrorSummary";
|
||||
import PrimaryActionButtons from "./buildSection/PrimaryActionButtons";
|
||||
import SaveStatusIndicator from "./buildSection/SaveStatusIndicator";
|
||||
import SectionHeader from "./buildSection/SectionHeader";
|
||||
import SidebarProgressPanel from "./buildSection/SidebarProgressPanel";
|
||||
|
||||
let BuildSection = (props) => {
|
||||
const [currentSectionSelected, setCurrentSectionSelected] = useState(1);
|
||||
@@ -52,29 +72,22 @@ let BuildSection = (props) => {
|
||||
mandatoryFieldsData
|
||||
} = props;
|
||||
|
||||
const legacyProps = props.props;
|
||||
const legacyAccountDetails = legacyProps.accountDetails;
|
||||
const legacyFormState = legacyProps.form;
|
||||
const legacyAppealType = legacyProps.appealType;
|
||||
const legacyUrl = legacyProps.url;
|
||||
|
||||
const documentListObj = props.appealType.documentList;
|
||||
const currentSection = props.appealType.currentSection;
|
||||
|
||||
const parser = new DOMParser();
|
||||
var doc = parser.parseFromString(props.formXML, "text/xml");
|
||||
var titles = xpath.select(
|
||||
"//form/tabs/tab[" + currentSection + " ]/labels/label/@description",
|
||||
doc
|
||||
);
|
||||
var rowXML = xpath.select(
|
||||
"/form/tabs/tab[" +
|
||||
currentSection +
|
||||
"]/columns//sections/section/rows/row",
|
||||
//"/form/tabs/tab/columns//sections/section/rows/row",
|
||||
doc
|
||||
);
|
||||
var doc = parseXml(props.formXML);
|
||||
var titles = selectCurrentSectionTitleDescriptions(doc, currentSection);
|
||||
var rowXML = selectCurrentSectionRows(doc, currentSection);
|
||||
|
||||
var formObjXML = xpath.select("/form/tabs", doc);
|
||||
var formObjXML = selectFormTabs(doc);
|
||||
|
||||
var titleList = xpath.select(
|
||||
"//form/tabs/tab[*]/labels/label/@description",
|
||||
doc
|
||||
);
|
||||
var titleList = selectAllTabTitleDescriptions(doc);
|
||||
|
||||
const progObj = getProgressObj(
|
||||
formXML,
|
||||
@@ -120,11 +133,11 @@ let BuildSection = (props) => {
|
||||
// _.includes(key, "pinswg_fileUpload") == true && delete dataObj[key];
|
||||
// }
|
||||
|
||||
await uploadFiles(
|
||||
await uploadAppealFilesEffect(
|
||||
dataObj,
|
||||
fileListObj,
|
||||
props.props.accountDetails.containerID,
|
||||
props.props.appealType.caseReference.ticketnumber
|
||||
legacyAccountDetails.containerID,
|
||||
legacyAppealType.caseReference.ticketnumber
|
||||
).then((data) => {
|
||||
//console.log("$1", data);
|
||||
setCompletedUploadFiles(true);
|
||||
@@ -167,12 +180,10 @@ let BuildSection = (props) => {
|
||||
|
||||
//console.log(buildAppealFilesArray);
|
||||
|
||||
buildAppealFilesArray = values.hasOwnProperty("filesList")
|
||||
? (buildAppealFilesArray.concat(values.filesList),
|
||||
buildAppealFilesArray.filter(function (item, idx) {
|
||||
return item.name;
|
||||
}))
|
||||
: buildAppealFilesArray;
|
||||
buildAppealFilesArray = mergeWithExistingFilesList(
|
||||
buildAppealFilesArray,
|
||||
values
|
||||
);
|
||||
|
||||
Object.assign(values, { "filesList": buildAppealFilesArray });
|
||||
|
||||
@@ -218,25 +229,15 @@ let BuildSection = (props) => {
|
||||
Object.assign(updateBody, {
|
||||
"pinswg_appealcasetype": props.appealType.appealTypeID,
|
||||
"pinswg_Appellant@odata.bind":
|
||||
"/contacts(" + props.props.accountDetails.loggedinUserId + ")",
|
||||
"/contacts(" + legacyAccountDetails.loggedinUserId + ")",
|
||||
"pinswg_name": props.appealType.caseReference.ticketnumber,
|
||||
[updateBindAppealTypeToIncident + "@odata.bind"]:
|
||||
"/incidents(" + incidentId + ")"
|
||||
});
|
||||
|
||||
updateBody = JSON.stringify(updateBody);
|
||||
updateBody = updateBody.replace(/:"Yes"/gm, `:true`);
|
||||
updateBody = updateBody.replace(/:"No"/gm, `:false`);
|
||||
updateBody = JSON.parse(updateBody);
|
||||
|
||||
Object.keys(updateBody).forEach((key) => {
|
||||
if (updateBody[key] === null) {
|
||||
delete updateBody[key];
|
||||
}
|
||||
if (key.indexOf("_") == 0) {
|
||||
delete updateBody[key];
|
||||
}
|
||||
});
|
||||
updateBody = normalizeYesNoBooleans(updateBody);
|
||||
updateBody = removeNullKeys(updateBody);
|
||||
updateBody = stripInternalKeys(updateBody);
|
||||
|
||||
//console.log(updateBody);
|
||||
|
||||
@@ -246,12 +247,12 @@ let BuildSection = (props) => {
|
||||
const reference = "PEDW-PARTIAL";
|
||||
const templateId = "021b0a7b-df00-41f1-b94e-c269bee98c75";
|
||||
const templateIdCY = "f3c4a56b-bfb8-4d39-97dd-888df3062b0a";
|
||||
const emailAddress = props.props.accountDetails.loggedinUserEmail;
|
||||
const emailAddress = legacyAccountDetails.loggedinUserEmail;
|
||||
const personalisation = {
|
||||
"caseReference": props.appealType.caseReference.ticketnumber,
|
||||
"emailAddress": props.props.accountDetails.loggedinUserEmail,
|
||||
"emailAddress": legacyAccountDetails.loggedinUserEmail,
|
||||
"returnLink":
|
||||
props.props.url +
|
||||
legacyUrl +
|
||||
(router.locale != "en" ? "/cy/fymhorth/" : "/myportal/") +
|
||||
appTypeCollection.UrlName +
|
||||
"?lpa=" +
|
||||
@@ -283,7 +284,7 @@ let BuildSection = (props) => {
|
||||
// );
|
||||
|
||||
sendSavedEmail == true &&
|
||||
sendEmail(
|
||||
sendPartialSaveEmailEffect(
|
||||
router.locale != "en" ? templateIdCY : templateId,
|
||||
emailAddress,
|
||||
personalisation,
|
||||
@@ -359,71 +360,38 @@ let BuildSection = (props) => {
|
||||
const [hasErrors, setHasErrors] = useState("");
|
||||
|
||||
const getErrors = () => {
|
||||
let errorsobj = props.props.form["appealForm"];
|
||||
let errorsobj = legacyFormState["appealForm"];
|
||||
//setHasErrors(errorsobj.hasOwnProperty("syncErrors"));
|
||||
window.scrollTo(0, 0);
|
||||
};
|
||||
|
||||
let errorsobj = {};
|
||||
const ShowErrorHeader = (props) => {
|
||||
errorsobj = props.props.appealForm.syncErrors;
|
||||
errorsobj = _.keys(errorsobj);
|
||||
|
||||
const getErrorItems = () => {
|
||||
let errorsobj = _.keys(
|
||||
_.get(props, "props.form.appealForm.syncErrors", {})
|
||||
);
|
||||
const getErrorLabel = (whichField) => {
|
||||
var doc = parser.parseFromString(props.formXML, "text/xml");
|
||||
var errorFieldLabel = xpath.select(
|
||||
"//*[control/@id='" + whichField + "']//@description",
|
||||
doc
|
||||
var doc = parseXml(props.formXML);
|
||||
var errorFieldLabel = selectErrorFieldLabelDescription(
|
||||
doc,
|
||||
whichField
|
||||
);
|
||||
|
||||
return errorFieldLabel[0].value;
|
||||
};
|
||||
|
||||
const ShowErrors = () => {
|
||||
let errorList = [];
|
||||
for (let i = 0; i < errorsobj.length; ++i) {
|
||||
//get label from error key - search rowxml for id = errorobj
|
||||
let errorItems = [];
|
||||
for (let i = 0; i < errorsobj.length; ++i) {
|
||||
errorItems.push({
|
||||
"field": errorsobj[i],
|
||||
"label": getErrorLabel(errorsobj[i])
|
||||
});
|
||||
}
|
||||
|
||||
errorList.push(
|
||||
<li key={i}>
|
||||
<a href={"#" + errorsobj[i] + "_label"}>
|
||||
{" "}
|
||||
{getErrorLabel(errorsobj[i])}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return errorList;
|
||||
};
|
||||
|
||||
return !_.isEmpty(errorsobj) ? (
|
||||
<div
|
||||
className="govuk-error-summary"
|
||||
aria-labelledby="error-summary-title"
|
||||
role="alert"
|
||||
tabIndex="-1"
|
||||
data-module="govuk-error-summary"
|
||||
>
|
||||
<h2
|
||||
className="govuk-error-summary__title"
|
||||
id="error-summary-title"
|
||||
>
|
||||
The following fields have a problem :
|
||||
</h2>
|
||||
<div className="govuk-error-summary__body">
|
||||
<ul className="govuk-list govuk-error-summary__list">
|
||||
<ShowErrors />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
""
|
||||
);
|
||||
return errorItems;
|
||||
};
|
||||
|
||||
const hasUploadFiles = () => {
|
||||
const formArr = props.props.form["appealForm"].values;
|
||||
const formArr = legacyFormState["appealForm"].values;
|
||||
const prefix = "pinswg_fileUpload";
|
||||
const hasFiles = Object.keys(formArr).some((key) =>
|
||||
key.startsWith(prefix)
|
||||
@@ -435,19 +403,12 @@ let BuildSection = (props) => {
|
||||
return (
|
||||
<div className="govuk-grid-row">
|
||||
<div className="govuk-grid-column-two-thirds wgForm ">
|
||||
<h1 className="govuk-heading-m ">
|
||||
{FieldsTranslations(titles[0].value)}
|
||||
</h1>
|
||||
<SectionHeader title={FieldsTranslations(titles[0].value)} />
|
||||
<form onSubmit={handleSubmit(onHandleSubmit)}>
|
||||
{hasErrors == true ? (
|
||||
<ShowErrorHeader
|
||||
hasErrors={hasErrors}
|
||||
props={props.props.form}
|
||||
formXML={formXML}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<ErrorSummary
|
||||
hasErrors={hasErrors == true}
|
||||
errorItems={getErrorItems()}
|
||||
/>
|
||||
<BuildRow
|
||||
rowXML={rowXML}
|
||||
whichSection={props.whichSection}
|
||||
@@ -466,7 +427,7 @@ let BuildSection = (props) => {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="govuk-button-group">
|
||||
<PrimaryActionButtons>
|
||||
{props.sectionCount != currentSection ? (
|
||||
<>
|
||||
<button
|
||||
@@ -476,11 +437,10 @@ let BuildSection = (props) => {
|
||||
onClick={() => {
|
||||
getErrors();
|
||||
let valuesObj = _.has(
|
||||
props.props.form[props.form],
|
||||
legacyFormState[props.form],
|
||||
"values"
|
||||
)
|
||||
? props.props.form[props.form]
|
||||
.values
|
||||
? legacyFormState[props.form].values
|
||||
: {};
|
||||
|
||||
//console.log("valuesobj:", valuesObj);
|
||||
@@ -503,26 +463,19 @@ let BuildSection = (props) => {
|
||||
</button>
|
||||
{currentSection > 1 &&
|
||||
(savingStatus ? (
|
||||
<span className=" progress-save-active">
|
||||
{router.locale == "cy"
|
||||
? "Nôl data"
|
||||
: "Saving data"}
|
||||
<img
|
||||
className="data-loading-icon"
|
||||
src="/assets/images/loading.gif"
|
||||
alt={
|
||||
router.locale == "cy"
|
||||
? "Nôl data"
|
||||
: "Saving data"
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
<SaveStatusIndicator
|
||||
text={
|
||||
router.locale == "cy"
|
||||
? "Nôl data"
|
||||
: "Saving data"
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className="govuk-button progress-save "
|
||||
onClick={() => {
|
||||
let valuesObj =
|
||||
props.props.form[props.form]
|
||||
legacyFormState[props.form]
|
||||
.values || {};
|
||||
|
||||
// console.log(
|
||||
@@ -572,26 +525,19 @@ let BuildSection = (props) => {
|
||||
{t("common:submit-button")}{" "}
|
||||
</button>
|
||||
{savingStatus ? (
|
||||
<span className=" progress-save-active">
|
||||
{router.locale == "cy"
|
||||
? "Nôl data"
|
||||
: "Saving data"}
|
||||
<img
|
||||
className="data-loading-icon"
|
||||
src="/assets/images/loading.gif"
|
||||
alt={
|
||||
router.locale == "cy"
|
||||
? "Nôl data"
|
||||
: "Saving data"
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
<SaveStatusIndicator
|
||||
text={
|
||||
router.locale == "cy"
|
||||
? "Nôl data"
|
||||
: "Saving data"
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className="govuk-button progress-save "
|
||||
onClick={() => {
|
||||
let valuesObj =
|
||||
props.props.form[props.form]
|
||||
legacyFormState[props.form]
|
||||
.values || {};
|
||||
|
||||
// console.log(
|
||||
@@ -631,29 +577,25 @@ let BuildSection = (props) => {
|
||||
) : (
|
||||
""
|
||||
)} */}
|
||||
</div>
|
||||
</PrimaryActionButtons>
|
||||
</form>
|
||||
</div>
|
||||
<div className="govuk-grid-column-one-third">
|
||||
<div className="at-nav-container">
|
||||
{/* <span>
|
||||
Last section is {lastIndex} - {currentSection}
|
||||
</span> */}
|
||||
<BuildProgress
|
||||
progObj={progObj}
|
||||
props={props}
|
||||
titleList={titleList}
|
||||
documentListObj={documentListObj}
|
||||
formObjXML={formXML}
|
||||
mandatoryFieldsData={mandatoryFieldsData}
|
||||
currentSection={currentSection}
|
||||
initialValues={props.initialValues}
|
||||
setCurrentSection={setCurrentSection}
|
||||
/>
|
||||
|
||||
<hr className="govuk-section-break govuk-section-break--m govuk-section-break--visible" />
|
||||
</div>
|
||||
</div>
|
||||
<SidebarProgressPanel>
|
||||
{/* <span>
|
||||
Last section is {lastIndex} - {currentSection}
|
||||
</span> */}
|
||||
<BuildProgress
|
||||
progObj={progObj}
|
||||
props={props}
|
||||
titleList={titleList}
|
||||
documentListObj={documentListObj}
|
||||
formObjXML={formXML}
|
||||
mandatoryFieldsData={mandatoryFieldsData}
|
||||
currentSection={currentSection}
|
||||
initialValues={props.initialValues}
|
||||
setCurrentSection={setCurrentSection}
|
||||
/>
|
||||
</SidebarProgressPanel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,14 +4,19 @@ import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { formValueSelector } from "redux-form";
|
||||
import { sendEmail } from "../../actions/services/notifyService";
|
||||
import { sendCompletionEmailEffect } from "../../lib/newappeal/journeyEffects";
|
||||
import { normalizeYesNoBooleans } from "../../lib/newappeal/payloadCleanup";
|
||||
import { setCurrentSection } from "../../store/appealType/action";
|
||||
import { getFormCollectionByID } from "../utils";
|
||||
|
||||
let CompleteAppeal = (props) => {
|
||||
const legacyProps = props.props;
|
||||
const legacyFormState = legacyProps.form;
|
||||
const legacyAccountDetails = legacyProps.accountDetails;
|
||||
|
||||
useEffect(() => {
|
||||
let incidentId = props.appealType.caseReference.incidentid;
|
||||
let updateBody = props.props.form.appealForm.values;
|
||||
let updateBody = legacyFormState.appealForm.values;
|
||||
|
||||
let appTypeCollection = getFormCollectionByID(
|
||||
props.appealType.appealTypeID
|
||||
@@ -23,7 +28,7 @@ let CompleteAppeal = (props) => {
|
||||
|
||||
Object.assign(updateBody, {
|
||||
"pinswg_Appellant@odata.bind":
|
||||
"/contacts(" + props.props.accountDetails.loggedinUserId + ")",
|
||||
"/contacts(" + legacyAccountDetails.loggedinUserId + ")",
|
||||
"pinswg_LocalPlanningAuthority@odata.bind":
|
||||
"/accounts(" + props.appealType.appealLPA + ")",
|
||||
"pinswg_name": props.appealType.caseReference.ticketnumber,
|
||||
@@ -31,10 +36,7 @@ let CompleteAppeal = (props) => {
|
||||
"/incidents(" + incidentId + ")"
|
||||
});
|
||||
|
||||
updateBody = JSON.stringify(updateBody);
|
||||
updateBody = updateBody.replace(/:"Yes"/gm, `:true`);
|
||||
updateBody = updateBody.replace(/:"No"/gm, `:false`);
|
||||
updateBody = JSON.parse(updateBody);
|
||||
updateBody = normalizeYesNoBooleans(updateBody);
|
||||
|
||||
let updateFormCollection = appTypeCollection.LogicalCollectionName;
|
||||
let primaryAttribute = appTypeCollection.PrimaryIdAttribute;
|
||||
@@ -46,13 +48,18 @@ let CompleteAppeal = (props) => {
|
||||
: "618e0463-b092-4733-a024-bf8a3bbde808";
|
||||
|
||||
const reference = "PEDW-COMPLETE" + isWelsh && "-CY";
|
||||
const emailAddress = props.props.accountDetails.loggedinUserEmail;
|
||||
const emailAddress = legacyAccountDetails.loggedinUserEmail;
|
||||
const personalisation = {
|
||||
"caseReference": props.appealType.caseReference.ticketnumber,
|
||||
"emailAddress": props.props.accountDetails.loggedinUserEmail,
|
||||
"emailAddress": legacyAccountDetails.loggedinUserEmail,
|
||||
"linkExpiry": 10 * 60
|
||||
};
|
||||
sendEmail(templateId, emailAddress, personalisation, reference);
|
||||
sendCompletionEmailEffect(
|
||||
templateId,
|
||||
emailAddress,
|
||||
personalisation,
|
||||
reference
|
||||
);
|
||||
|
||||
// sendCaseCompleteMessage(
|
||||
// props.props.accountDetails.containerID,
|
||||
@@ -65,12 +72,12 @@ let CompleteAppeal = (props) => {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}, [
|
||||
props.appealType.caseReference,
|
||||
props.props.form.appealForm.values,
|
||||
legacyFormState.appealForm.values,
|
||||
props.appealType.appealLPA,
|
||||
props.props.accountDetails.loggedinUserId,
|
||||
legacyAccountDetails.loggedinUserId,
|
||||
props.appealType.appealTypeID,
|
||||
props.props.accountDetails.loggedinUserEmail,
|
||||
props.props.accountDetails.containerID
|
||||
legacyAccountDetails.loggedinUserEmail,
|
||||
legacyAccountDetails.containerID
|
||||
]);
|
||||
|
||||
let { t } = useTranslation();
|
||||
|
||||
@@ -378,6 +378,7 @@ let CreateCase = (props) => {
|
||||
|
||||
const router = useRouter();
|
||||
const { locale } = router;
|
||||
const isWelsh = router.locale == "cy";
|
||||
|
||||
const { LPAData, onSubmit, handleSubmit, formData } = props;
|
||||
|
||||
@@ -399,16 +400,17 @@ let CreateCase = (props) => {
|
||||
event.preventDefault();
|
||||
|
||||
var appTypeCollection = values.appealTypes;
|
||||
const selectedAppealType = values.appealTypes;
|
||||
const confirmHASCAS = values.confirmHASCAS;
|
||||
|
||||
formStep == 0 && setFormStep(1);
|
||||
|
||||
whichAppealType =
|
||||
values.appealTypes == "846040004" && values.confirmHASCAS == "true"
|
||||
selectedAppealType == "846040004" && confirmHASCAS == "true"
|
||||
? "846040004"
|
||||
: values.appealTypes == "846040004" &&
|
||||
values.confirmHASCAS == "false"
|
||||
: selectedAppealType == "846040004" && confirmHASCAS == "false"
|
||||
? "846040000"
|
||||
: values.appealTypes;
|
||||
: selectedAppealType;
|
||||
|
||||
var whichAppealType = whichAppealType;
|
||||
|
||||
@@ -435,15 +437,17 @@ let CreateCase = (props) => {
|
||||
// values,
|
||||
// props.containerID
|
||||
// )
|
||||
const selectedLpaName = props.LPAData.LPAData.value.filter((obj) => {
|
||||
return obj.accountid === values.lpaTypes;
|
||||
})[0].name;
|
||||
|
||||
createNewCaseBlob(
|
||||
appTypeCollection.appealTypeID,
|
||||
values.lpaTypes,
|
||||
props.loggedInUser,
|
||||
values,
|
||||
props.containerID,
|
||||
props.LPAData.LPAData.value.filter((obj) => {
|
||||
return obj.accountid === values.lpaTypes;
|
||||
})[0].name
|
||||
selectedLpaName
|
||||
)
|
||||
.then((data) => {
|
||||
router.replace(
|
||||
@@ -477,7 +481,7 @@ let CreateCase = (props) => {
|
||||
// }
|
||||
// );
|
||||
};
|
||||
const yesNo = router.locale == "cy" ? ["Ydw", "Na"] : ["Yes", "No"];
|
||||
const yesNo = isWelsh ? ["Ydw", "Na"] : ["Yes", "No"];
|
||||
|
||||
var parentField = "appealTypes",
|
||||
formProps = props.whichForm,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# Current State Scorecard (2026-03-25)
|
||||
|
||||
Purpose: provide a single operational view of architecture/debt progress with evidence references.
|
||||
|
||||
## RAG Legend
|
||||
|
||||
- Green: materially addressed for current stream
|
||||
- Amber: partial progress, follow-on needed
|
||||
- Red: unresolved/high risk remains
|
||||
|
||||
## Scorecard
|
||||
|
||||
| Area | Status | Current position | Evidence |
|
||||
| --------------------------------- | ------ | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
||||
| API contract consistency | Green | Large endpoint clusters normalized on structured contracts with phase21 coverage. | `memory-bank/change-log.md` (endpoint hardening stream), `tests/phase21/endpoint-handler-contract.test.cjs` |
|
||||
| Actions/service decomposition | Amber | Shared clients and route builders introduced; monolith risk reduced but full domain split remains. | `actions/clients/*`, `actions/services/*`, `memory-bank/debt-list.md` |
|
||||
| Endpoint sprawl/proxy duplication | Amber | Shared relay forwarding and helper reuse reduced duplication; long-tail handlers still exist. | `pages/api/middleware/relayForwarding.js`, `context/architecture.md` |
|
||||
| High-risk automation | Green | Focused checks now cover auth redirect safety, signed-delete/upload negatives, and EN/CY rewrite parity. | `tests/phase22/auth-redirect-safety.test.cjs`, `tests/phase22/i18n-route-parity.test.cjs`, `tests/phase22/index.test.cjs` |
|
||||
| i18n parity assurance | Amber | Targeted parity checks in place; CI-level parity enforcement still pending. | `tests/phase22/i18n-route-parity.test.cjs`, `context/architecture.md` |
|
||||
| Logging redaction consistency | Amber | Relay-side structured redaction improved; broader auth/email/file logging hardening remains open. | `memory-bank/open-questions.md` (Q-001), `context/architecture.md` |
|
||||
| Runtime canonicalization | Red | `server.js` and `server/server.js` ambiguity not yet formally closed. | `memory-bank/architect-review.md`, `context/architecture.md` |
|
||||
|
||||
## Next execution focus
|
||||
|
||||
1. Sequence B: signed-request consolidation + logging hardening.
|
||||
2. Sequence C: CI-level i18n parity gates + endpoint long-tail reduction.
|
||||
3. Runtime canonicalization decision with explicit operational owner.
|
||||
|
||||
## Update cadence
|
||||
|
||||
- Update after every non-trivial architecture/debt slice.
|
||||
- Keep this file aligned with:
|
||||
- `context/architecture.md`
|
||||
- `memory-bank/debt-list.md`
|
||||
- `memory-bank/change-log.md`
|
||||
@@ -0,0 +1,117 @@
|
||||
# New Appeal Refactor Guardrails
|
||||
|
||||
## Purpose
|
||||
|
||||
These guardrails apply to all AI-assisted and human-assisted work on the refactor branch for the new appeal flow.
|
||||
|
||||
## Critical Rule
|
||||
|
||||
Preserve the current live S78 behaviour unless the task explicitly requires a behaviour change.
|
||||
|
||||
Do not assume a cleaner implementation is allowed to alter behaviour.
|
||||
|
||||
## Protected Areas
|
||||
|
||||
Treat the following as protected flow files:
|
||||
|
||||
- `pages/newappeal/index.js`
|
||||
- `pages/newappeal/[appealtypes].js`
|
||||
- `components/newappeal/buildsection.js`
|
||||
- `components/newappeal/buildchecksection.js`
|
||||
- `components/newappeal/buildrow.js`
|
||||
- `components/newappeal/buildfield.js`
|
||||
- `components/newappeal/buildcheckrow.js`
|
||||
- `components/newappeal/complete.js`
|
||||
- `components/newappeal/aboutyou.js`
|
||||
- `components/newappeal/createCase.js`
|
||||
- any helpers or service modules used directly by save/progress/upload/submit/finalise logic
|
||||
|
||||
## Protected Behaviour
|
||||
|
||||
The following must not regress:
|
||||
|
||||
1. Start a new S78 appeal
|
||||
2. Create case / initial journey entry
|
||||
3. Section rendering from form definition
|
||||
4. Section progression
|
||||
5. Save and exit
|
||||
6. Resume saved appeal
|
||||
7. File/document upload behaviour
|
||||
8. Check answers page
|
||||
9. Appeal PDF generation/download path
|
||||
10. Final submit/finalisation
|
||||
11. Confirmation page behaviour
|
||||
12. Email/notification side effects
|
||||
13. English/Welsh parity for touched areas
|
||||
|
||||
## Required Refactor Approach
|
||||
|
||||
When refactoring:
|
||||
|
||||
1. Understand current behaviour first
|
||||
2. Identify the smallest safe boundary
|
||||
3. Prefer extraction of pure/helper logic
|
||||
4. Keep public interfaces stable where possible
|
||||
5. Avoid changing UI, business rules, and structure in one step
|
||||
6. Keep each slice easy to review and revert
|
||||
|
||||
## Required Testing Mindset
|
||||
|
||||
Before changing critical flow behaviour, add or update protection such as:
|
||||
|
||||
- characterization tests for current behaviour
|
||||
- targeted integration/service tests
|
||||
- journey-level regression checks
|
||||
- payload-shape assertions for save/submit/finalise paths
|
||||
|
||||
If automation is not practical yet, include an explicit manual verification matrix.
|
||||
|
||||
## Payload/Integration Safety
|
||||
|
||||
Do not change without explicit need:
|
||||
|
||||
- CRM payload field names or structure
|
||||
- boolean/value normalization behaviour
|
||||
- document/file metadata shape
|
||||
- save/resume payload expectations
|
||||
- PDF generation inputs
|
||||
- notification template selection or personalisation structure
|
||||
|
||||
## i18n / Accessibility Safety
|
||||
|
||||
For touched user-facing behaviour:
|
||||
|
||||
- keep EN/CY behaviour aligned
|
||||
- keep labels, messages, and route behaviour consistent
|
||||
- preserve semantic structure, focus behaviour, and validation messaging
|
||||
|
||||
## Delivery Safety
|
||||
|
||||
Preferred pattern for each change:
|
||||
|
||||
1. protect current behaviour
|
||||
2. extract one concern
|
||||
3. run focused validation
|
||||
4. keep rollback straightforward
|
||||
5. merge only when safe
|
||||
|
||||
## What To Avoid
|
||||
|
||||
Do not:
|
||||
|
||||
- perform broad rewrites
|
||||
- mix feature delivery with refactor work
|
||||
- replace dynamic/config-driven logic with one-off hardcoding
|
||||
- move many responsibilities at once
|
||||
- introduce new dependencies unless clearly justified
|
||||
- silently change business rules while “cleaning up”
|
||||
|
||||
## Refactor Success Criteria
|
||||
|
||||
A refactor slice is successful when it:
|
||||
|
||||
- preserves behaviour
|
||||
- reduces complexity or coupling
|
||||
- improves readability or testability
|
||||
- keeps regression risk controlled
|
||||
- remains small enough to merge safely into `SIPS-Development`
|
||||
@@ -1,101 +0,0 @@
|
||||
# Sequence B Work Plan (2026-03-25)
|
||||
|
||||
Scope: implement the next architecture lane after Sequence A completion.
|
||||
|
||||
Sequence B objectives:
|
||||
|
||||
1. Consolidate signed-request patterns.
|
||||
2. Harden logging policy in sensitive paths.
|
||||
|
||||
## Workstream B1: Signed-request consolidation
|
||||
|
||||
### Goal
|
||||
|
||||
Reduce duplicate hash/header/method composition logic across service modules without changing behavior contracts.
|
||||
|
||||
### Target scope
|
||||
|
||||
- `actions/services/portalDirectService.js`
|
||||
- `actions/services/documentDirectService.js`
|
||||
- `actions/services/caseDirectService.js` (if signed routes exist)
|
||||
- shared client layer in `actions/clients/`
|
||||
|
||||
### Proposed implementation
|
||||
|
||||
1. Introduce a focused signed-request helper (or helper set) in `actions/clients/`:
|
||||
- signed GET
|
||||
- signed POST
|
||||
- signed DELETE
|
||||
2. Normalize hash/signing + header behavior through helper API.
|
||||
3. Migrate in bounded slices (module by module), preserving current catch semantics.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- No route URL/signature behavior regressions.
|
||||
- Existing signed flows preserve:
|
||||
- hash generation behavior
|
||||
- request method
|
||||
- required headers
|
||||
- error-return/catch contracts.
|
||||
- Phase22 behavioural tests expanded where relevant.
|
||||
|
||||
### Validation checklist
|
||||
|
||||
- `node tests/phase22/index.test.cjs`
|
||||
- `node tests/phase7/service-behaviour.test.cjs`
|
||||
- `npm run lint`
|
||||
|
||||
### Rollback plan
|
||||
|
||||
- Revert helper adoption commit(s) for affected service only.
|
||||
- Keep migrations bounded so each module rollback is isolated.
|
||||
|
||||
## Workstream B2: Logging hardening in sensitive paths
|
||||
|
||||
### Goal
|
||||
|
||||
Replace ad-hoc verbose logging in auth/email/file/account-sensitive paths with redacted, structured logs.
|
||||
|
||||
### Target scope
|
||||
|
||||
- `pages/api/auth/[...nextauth].js`
|
||||
- selected `pages/api/email/**`
|
||||
- selected `pages/api/file/**`
|
||||
- any adjacent shared helper used by these routes
|
||||
|
||||
### Proposed implementation
|
||||
|
||||
1. Define/confirm minimal redaction policy (link to `memory-bank/open-questions.md` Q-001).
|
||||
2. Introduce/standardize structured logger usage pattern for sensitive flows.
|
||||
3. Replace high-risk direct logs in bounded route clusters.
|
||||
|
||||
### Acceptance criteria
|
||||
|
||||
- No secrets/tokens/personal data in new logs.
|
||||
- Error correlation remains operationally useful.
|
||||
- Existing route behavior/contracts unchanged.
|
||||
|
||||
### Validation checklist
|
||||
|
||||
- `npm run lint`
|
||||
- targeted route-level negative-path checks for changed handlers
|
||||
- manual review of log payload fields against redaction policy
|
||||
|
||||
### Rollback plan
|
||||
|
||||
- Revert logging-hardening commit(s) by cluster.
|
||||
- Restore previous logger call sites if operational diagnostics regress.
|
||||
|
||||
## Delivery sequencing
|
||||
|
||||
1. B1 signed-request helper design + one pilot migration.
|
||||
2. B1 full module rollout (portal/document, then any remaining signed paths).
|
||||
3. B2 redaction policy confirmation.
|
||||
4. B2 auth cluster hardening.
|
||||
5. B2 file/email cluster hardening.
|
||||
|
||||
## Ownership and governance
|
||||
|
||||
- Track each slice in `memory-bank/change-log.md`.
|
||||
- Record policy decisions in `memory-bank/decisions.md`.
|
||||
- Escalate unresolved policy questions in `memory-bank/open-questions.md`.
|
||||
@@ -1,112 +0,0 @@
|
||||
# Engineer Onboarding Guide — PEDW FrontEnd
|
||||
|
||||
## 1) Project Overview
|
||||
|
||||
PEDW FrontEnd is the Planning and Environment Decisions Wales (PEDW) portal for discovering planning appeals and accessing personalised casework journeys. It supports public search/browse and authenticated dashboard workflows, with strong accessibility and bilingual (English/Welsh) requirements.
|
||||
|
||||
## 2) Technology Stack
|
||||
|
||||
- **Frontend:** Next.js 14 (Pages Router), React 18
|
||||
- **Language:** Primarily JavaScript (some TypeScript tooling present)
|
||||
- **State:** Redux, `next-redux-wrapper`, `redux-persist`, `redux-thunk`, `redux-form`
|
||||
- **Auth:** `next-auth` + Prisma adapter (email magic-link flow)
|
||||
- **Auth data layer:** Prisma + SQL Server (`prisma/schema.prisma`) for next-auth identity/session tables
|
||||
- **Portal business data layer:** Dynamics 365 CRM queried via REST + OData through frontend API routes
|
||||
- **i18n:** `next-translate` + `i18n.js` + Welsh rewrites in `next.config.js`
|
||||
- **Integrations:** Azure Service Bus Relay (CRM transport), Azure Blob/Queue, GOV.UK Notify, Application Insights, mapping (Leaflet/google-map-react), PDF generation
|
||||
|
||||
## 3) Architecture Overview
|
||||
|
||||
- Active runtime is **Next.js runtime** (legacy custom server files exist but are inactive).
|
||||
- UI routes and APIs live in `pages/` and `pages/api/**`.
|
||||
- Security controls are applied through `middleware.js` + security headers in `next.config.js`.
|
||||
- Auth/session logic is centralized in `pages/api/auth/[...nextauth].js`.
|
||||
- Shared integration/service logic sits mostly in `actions/` (notably large `actions/index.js`).
|
||||
- Portal data calls are sent from `pages/api/endpoint/**` to Dynamics 365 CRM through Azure Service Bus Relay.
|
||||
- For relay-bound calls, a hash is generated from request path (excluding domain) and appended; relay validates using the same shared hash key.
|
||||
|
||||
## 4) Repository Structure
|
||||
|
||||
- `pages/` — routes + API handlers
|
||||
- `components/` — UI/features (case, DNS, account, admin, mapping, PDF templates)
|
||||
- `actions/` — API client and side-effect helpers
|
||||
- `lib/` — reusable form/domain helpers
|
||||
- `store/` — Redux reducers/store setup/hydration/persistence
|
||||
- `prisma/` — schema + migrations
|
||||
- `locales/` — EN/CY translations
|
||||
- `data/` — lookup and form metadata files
|
||||
- `tests/` — test area (appears limited)
|
||||
- `server/`, `server.js` — legacy/inactive runtime artifacts
|
||||
|
||||
## 5) Core System Components
|
||||
|
||||
- **Public search/case flows:** basic search (`/search`), advanced search (`/advancedsearch`), and address search (`/addresssearch`) with result pages and case detail/document UIs
|
||||
- **Account/auth:** next-auth email verification and Prisma-backed sessions
|
||||
- Portal routes (`/myportal/**`): user-specific case/representation/watchlist workflows
|
||||
- **File/doc pipeline:** upload/download/blob flows and PDF generation under `pages/api/file/**`
|
||||
- **Notifications:** GOV.UK Notify integrations under `pages/api/email/**` + auth email provider
|
||||
|
||||
## 5a) User Involvement / Role Model
|
||||
|
||||
- Newly registered/authenticated users default to **Interested Party** involvement.
|
||||
- Users who raise a new appeal become **Appellants**.
|
||||
- CRM contact constraints allow only one role type; once Appellant is assigned, it remains their role.
|
||||
- **Agents** can submit appeals on behalf of multiple Appellants.
|
||||
- **LPA (Local Planning Authority)** users are a separate persona with a distinct dashboard view.
|
||||
- LPA dashboards focus on appeals within that authority.
|
||||
- LPA users cannot raise appeals (option is hidden), but they can submit representations.
|
||||
|
||||
## 6) Data Model
|
||||
|
||||
Prisma schema is focused on next-auth persistence (SQL Server):
|
||||
|
||||
- `User`
|
||||
- `Account`
|
||||
- `Session`
|
||||
- `VerificationToken`
|
||||
|
||||
Datasource is SQL Server (`DATABASE_URL`). This model underpins authentication/session behavior and should be treated as sensitive core infrastructure. Portal business/case data is sourced separately from Dynamics 365 CRM via relay-backed API calls.
|
||||
|
||||
## 7) Key Workflows
|
||||
|
||||
- **Sign-in:** user requests magic link -> email via Notify -> callback/session via next-auth.
|
||||
- **Search journey:** frontend query -> `pages/api/endpoint/**` proxy endpoint(s) -> path hash appended -> Azure Service Bus Relay (validates hash with shared key) -> Dynamics 365 CRM (REST/OData) -> normalized UI rendering.
|
||||
- **Case summary to representation:** user selects case reference from results -> lands on case summary -> “make representation” shown only when criteria/date rules allow -> user can start flow but must be authenticated to submit.
|
||||
- **Dashboard journey (`/myportal`):** signed-in users can raise new appeals, search appeals, view watched cases, view partially completed appeals, view submitted appeals, and manage submitted/partially submitted representations.
|
||||
- **Role behavior:** default involvement starts as Interested Party; new appeal creation sets Appellant role (persistent due to CRM single-role contact model); Agent users may act for multiple Appellants.
|
||||
- **LPA dashboard behavior:** LPA users see an authority-scoped dashboard, do not see raise-appeal options, and can raise representations on existing appeals.
|
||||
- **Document workflow:** user upload/submit -> blob storage + metadata updates -> PDF/document retrieval.
|
||||
- **Bilingual routing:** Welsh aliases rewired in `next.config.js`, locale resources in `locales/en|cy`, page namespace mapping in `i18n.js`.
|
||||
|
||||
## 8) Development Workflow
|
||||
|
||||
From scripts and runbook:
|
||||
|
||||
- `npm run dev` — local dev
|
||||
- `npm run build` + `npm start` — production build/start
|
||||
- `npm run lint` — baseline validation
|
||||
|
||||
Expected change protocol emphasizes:
|
||||
|
||||
- scoped changes,
|
||||
- EN/CY parity checks,
|
||||
- accessibility smoke checks,
|
||||
- negative-path checks on sensitive flows,
|
||||
- memory-bank/log updates for non-trivial changes.
|
||||
|
||||
## 9) Observed Conventions
|
||||
|
||||
- 4-space indentation, no trailing commas (per local conventions)
|
||||
- Keep page-level logic thin where possible; place reusable logic in `lib/`/`components/`/`actions/`
|
||||
- API naming pattern commonly uses `*_api.js`
|
||||
- Heavy use of proxy-style API handlers
|
||||
- Existing AI governance artifacts define guardrails (`.clinerules`, `GUARDRAILS.md`, `context/`, `memory-bank/`)
|
||||
|
||||
## 10) Potential Risks / Weak Areas
|
||||
|
||||
1. **Large `actions/index.js` coupling** (many responsibilities in one module)
|
||||
2. **API contract inconsistency** (error/response handling varies across endpoints)
|
||||
3. **Extensive `console.log` footprint** (signal/noise and potential sensitive logging concerns)
|
||||
4. **Duplication across API proxy handlers** (token/header/hash/relay logic repeated)
|
||||
5. **i18n complexity drift risk** (rewrite + locale namespace parity maintenance)
|
||||
6. **Limited automated tests for high-risk flows** (manual validation burden)
|
||||
@@ -1,88 +0,0 @@
|
||||
# PEDW FrontEnd Project Overview
|
||||
|
||||
## Purpose
|
||||
|
||||
PEDW FrontEnd is a public-service web platform for planning casework and DNS (Developments of National Significance) journeys, with public search and citizen self-service plus authenticated portal/admin capabilities.
|
||||
|
||||
## Current Stack
|
||||
|
||||
- **Frontend:** Next.js 14 (`pages` router), React 18
|
||||
- **Server runtime:** Next.js runtime (legacy server files remain in-repo but are not used)
|
||||
- **State:** Redux + `next-redux-wrapper` + `redux-persist`
|
||||
- **Auth:** `next-auth` with Prisma adapter and email sign-in flow
|
||||
- **Auth persistence:** Prisma + SQL Server schema (`prisma/schema.prisma`) for next-auth tables
|
||||
- **Portal case data source:** Microsoft Dynamics 365 CRM (queried via REST + OData)
|
||||
- **Portal API transport path:** Frontend API routes -> Azure Service Bus Relay -> Dynamics 365 CRM
|
||||
- **Relay request integrity:** Forwarded API calls include a hash derived from request path (excluding domain). Relay recomputes hash with shared key and rejects mismatches.
|
||||
- **i18n:** `next-translate`, locales `en` and `cy`, Welsh rewrites in `next.config.js`
|
||||
- **Integrations:** Azure storage/queues, GOV.UK Notify, Application Insights, mapping, PDF generation
|
||||
|
||||
## Repository Shape (Intent)
|
||||
|
||||
- `pages/`: routes and API handlers (`pages/api/**`)
|
||||
- `components/`: UI and feature components (case, account, admin, mapping, PDF templates)
|
||||
- `actions/`: API client + shared side-effect logic
|
||||
- `lib/`: reusable domain helpers and form-related logic
|
||||
- `store/`: Redux reducers/store hydration/persistence
|
||||
- `prisma/`: schema and migrations
|
||||
- `locales/`: translation resources (EN/CY)
|
||||
- `server/` + root `server.js`: legacy runtime files retained in repository
|
||||
|
||||
## Core User/Business Areas
|
||||
|
||||
1. Public appeal discovery via:
|
||||
- basic search (`/search`)
|
||||
- advanced search (`/advancedsearch`)
|
||||
- address search (`/addresssearch`)
|
||||
2. Search results and case summary journey (case reference selection -> summary page)
|
||||
3. Representation flow from case summary (criteria/date dependent “make representation” action)
|
||||
4. Passwordless authentication via next-auth magic links for authenticated submissions
|
||||
5. Authenticated dashboard (`/myportal/**`) for personalised casework
|
||||
6. New appeal and representation lifecycle management (partial + submitted states)
|
||||
7. Admin/document workflows
|
||||
|
||||
## Planning Casework Portal Dashboard (My Portal)
|
||||
|
||||
The Planning Casework dashboard is the authenticated Appellant/Interested party user’s operational home page for:
|
||||
|
||||
- starting new appeal submissions
|
||||
- Viewing the users submitted appeals
|
||||
- finding existing cases quickly
|
||||
- resuming in-progress submissions
|
||||
- monitoring submitted cases/representations
|
||||
- tracking watched cases
|
||||
- Information to view infrastructure project activity
|
||||
|
||||
The Planning Casework dashboard is the authenticated LPA user’s operational home page for:
|
||||
|
||||
- finding existing cases quickly
|
||||
- resuming in-progress submissions
|
||||
- monitoring submitted cases/representations
|
||||
- tracking watched cases
|
||||
- Information to view infrastructure project activity
|
||||
|
||||
The dashboard is organized into task-focused panels that separate **action entry points** (for example, “Make a new appeal”, “Search for a case”) from **state-based worklists** (for example, “Appeals awaiting submission”, “My cases”, and representation lists).
|
||||
|
||||
## Public Basic Search Results (Search Journey)
|
||||
|
||||
In the public case-discovery journey, the basic search results page provides a structured list of matched cases and key metadata. The page supports sorting and pagination, links each case reference through to case detail, and presents a clear no-results or loading/error state when appropriate.
|
||||
|
||||
Results content is locale-aware (EN/CY labels and formatted values) and includes highlighted query matches in key visible fields where supported by returned data.
|
||||
|
||||
## User Involvement Model (Portal)
|
||||
|
||||
- Newly registered/authenticated portal users default to **Interested Party** involvement.
|
||||
- Users who raise a new appeal become **Appellants**.
|
||||
- CRM contact constraints allow only one role type, so once set to Appellant this remains their role.
|
||||
- **Agents** can submit appeals on behalf of multiple Appellants.
|
||||
- **LPA (Local Planning Authority)** users have a distinct persona/dashboard view scoped to appeals raised within their authority.
|
||||
- LPA users do **not** see the “raise appeal” option.
|
||||
- LPA users can still make/submit representations on appeals.
|
||||
|
||||
## Operating Constraints
|
||||
|
||||
- Public-sector reliability expectations: avoid regressions on live service paths.
|
||||
- Bilingual parity is mandatory for user-facing route/content changes.
|
||||
- Accessibility expectations are high (keyboard and semantic behavior).
|
||||
- Security-sensitive areas include auth/session, uploads/documents, notifications, and account data.
|
||||
- Distinguish data paths: next-auth identity/session data is persisted in SQL Server; portal business data is sourced from Dynamics 365 CRM via relay-backed API calls with request-path hash validation.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Refactor Branch Charter — New Appeal Flow
|
||||
|
||||
## Purpose
|
||||
|
||||
This branch exists to safely refactor the live new appeal flow so it is easier to maintain, safer to change, and better prepared to support additional appeal types beyond the current S78 planning appeal flow.
|
||||
|
||||
This is a **behaviour-preserving refactor branch**, not a feature branch.
|
||||
|
||||
## Primary Goal
|
||||
|
||||
Create a safer internal structure for the new appeal flow while preserving current live behaviour for the S78 appeal journey.
|
||||
|
||||
## Why This Branch Exists
|
||||
|
||||
The current new appeal implementation has grown over time to meet business need and now contains a mix of:
|
||||
|
||||
- page composition
|
||||
- flow orchestration
|
||||
- XML-driven form rendering
|
||||
- validation
|
||||
- file/document handling
|
||||
- progress/save logic
|
||||
- submission/finalisation logic
|
||||
- integration shaping for CRM, PDF generation, and notifications
|
||||
|
||||
This branch exists to improve those boundaries incrementally without disrupting the live service.
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
|
||||
- behaviour-preserving refactor of `pages/newappeal/**` and `components/newappeal/**`
|
||||
- extraction of reusable workflow logic from UI-heavy components
|
||||
- improved boundaries between rendering, workflow, and integration logic
|
||||
- regression test coverage for critical S78 journeys
|
||||
- preparing the codebase for future appeal-type extensibility
|
||||
|
||||
Out of scope unless explicitly requested:
|
||||
|
||||
- business rule changes
|
||||
- visual redesign
|
||||
- broad framework/library migration
|
||||
- replacing working dynamic form behaviour with hardcoded appeal-specific logic
|
||||
- changes to live BAU behaviour beyond strictly necessary bug fixes
|
||||
|
||||
## Branch Relationship to BAU
|
||||
|
||||
- BAU continues on `SIPS-Development`
|
||||
- this branch is the protected refactor lane
|
||||
- safe, proven slices may be merged back into `SIPS-Development` when ready
|
||||
- urgent live fixes should go to `SIPS-Development` first, then be synced into this branch
|
||||
|
||||
## Non-Negotiable Principles
|
||||
|
||||
1. Preserve live S78 behaviour unless explicitly told otherwise.
|
||||
2. Prefer extraction over rewrite.
|
||||
3. Prefer small, mergeable slices over long-lived hidden change.
|
||||
4. Add or update regression protection before changing critical flow logic.
|
||||
5. Keep English/Welsh behaviour aligned.
|
||||
6. Treat save/resume/upload/check/submit/complete as protected journey stages.
|
||||
|
||||
## Target Direction
|
||||
|
||||
The long-term direction is:
|
||||
|
||||
- shared new appeal workflow engine
|
||||
- appeal-type definitions/configuration separated from UI rendering
|
||||
- smaller, clearer components
|
||||
- isolated validation and payload-shaping logic
|
||||
- safer addition of future appeal types through definition + bounded type-specific rules
|
||||
|
||||
## Definition of Success
|
||||
|
||||
This branch is succeeding when:
|
||||
|
||||
- core S78 journey behaviour remains stable
|
||||
- regression confidence increases
|
||||
- high-risk logic moves out of large render-heavy components
|
||||
- new appeal code becomes easier to understand and test
|
||||
- future appeal types can be added with less change to core flow code
|
||||
|
||||
## Working Branch Model
|
||||
|
||||
This refactor stream uses the `refactor` branch as its working base.
|
||||
|
||||
- BAU continues on `SIPS-Development`
|
||||
- refactor work is performed from `refactor`
|
||||
- safe refactor slices may later be merged into `SIPS-Development`
|
||||
|
||||
This branch should not be treated as BAU, and BAU should not be treated as the refactor workspace.
|
||||
@@ -0,0 +1,110 @@
|
||||
# New Appeal Refactor Tracker
|
||||
|
||||
Base branch for this tracker: `refactor`
|
||||
|
||||
## Status
|
||||
|
||||
Current slice: Slice 8 — Start Flow Cleanup (CreateCase / AboutYou)
|
||||
Status: READY TO START
|
||||
|
||||
---
|
||||
|
||||
## Slice List
|
||||
|
||||
### Slice 1 — XML/Form Derivation Extraction
|
||||
|
||||
Status: COMPLETE
|
||||
|
||||
- Extract XML parsing helpers into lib
|
||||
- Keep selectors identical
|
||||
- No behaviour change
|
||||
|
||||
---
|
||||
|
||||
### Slice 2a — Payload Cleanup Helpers
|
||||
|
||||
Status: COMPLETE
|
||||
|
||||
- Extract boolean normalization
|
||||
- Extract null/internal key stripping
|
||||
|
||||
---
|
||||
|
||||
### Slice 2b — File Merge/Dedupe Helpers
|
||||
|
||||
Status: COMPLETE
|
||||
|
||||
- Extract file list merge logic
|
||||
- Extract dedupe logic
|
||||
|
||||
---
|
||||
|
||||
### Slice 3 — Side Effect Facade
|
||||
|
||||
Status: COMPLETE
|
||||
|
||||
- Wrap existing service calls
|
||||
- No logic changes
|
||||
|
||||
---
|
||||
|
||||
### Slice 4 — BuildSection UI Extraction
|
||||
|
||||
Status: COMPLETE
|
||||
|
||||
---
|
||||
|
||||
### Slice 5 — BuildCheckSection UI Extraction
|
||||
|
||||
Status: COMPLETE
|
||||
|
||||
---
|
||||
|
||||
### Slice 6 — BuildCheckRow Formatter Map
|
||||
|
||||
Status: COMPLETE
|
||||
|
||||
---
|
||||
|
||||
### Slice 7 — Props Boundary Cleanup
|
||||
|
||||
Status: COMPLETE
|
||||
|
||||
---
|
||||
|
||||
### Slice 8 — Start Flow Cleanup (CreateCase / AboutYou)
|
||||
|
||||
Status: COMPLETE
|
||||
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
- Only work on ONE slice at a time
|
||||
- Do not move to next slice until current is COMPLETE
|
||||
- Do not combine slices
|
||||
- Preserve behaviour at all times
|
||||
|
||||
## Notes
|
||||
|
||||
- Save/resume payload shape is sensitive
|
||||
- File upload logic duplicated in multiple places
|
||||
- BuildSection is highest risk area
|
||||
|
||||
## Regression Checklist (Run After Each Slice)
|
||||
|
||||
- Start new appeal
|
||||
- Save and exit
|
||||
- Resume saved appeal
|
||||
- Navigate sections
|
||||
- Upload file
|
||||
- View check answers
|
||||
- Submit appeal (if safe to test)
|
||||
- View confirmation page
|
||||
- Verify EN/CY parity
|
||||
|
||||
Validation:
|
||||
|
||||
- npm run lint passed
|
||||
- Playwright end-to-end new appeal journey passed
|
||||
- submission completed successfully through CRM insertion
|
||||
@@ -0,0 +1,28 @@
|
||||
export const mergeWithExistingFilesList = (buildAppealFilesArray, values) => {
|
||||
return values.hasOwnProperty("filesList")
|
||||
? (buildAppealFilesArray.concat(values.filesList),
|
||||
buildAppealFilesArray.filter(function (item, idx) {
|
||||
return item.name;
|
||||
}))
|
||||
: buildAppealFilesArray;
|
||||
};
|
||||
|
||||
export const removeDuplicatesByKey = (arr, key) => {
|
||||
const seen = new Set();
|
||||
return arr.filter((item) => {
|
||||
const value = item[key];
|
||||
if (seen.has(value)) {
|
||||
return false; // Duplicate found
|
||||
} else {
|
||||
seen.add(value); // Add value to the set
|
||||
return true; // Keep the item
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const dedupeFilesListByName = (filesList) => {
|
||||
return filesList.filter(
|
||||
(value, index, self) =>
|
||||
index === self.findIndex((t) => t.name === value.name)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import xpath from "xpath";
|
||||
|
||||
export const parseXml = (xmlStr) => {
|
||||
const parser = new DOMParser();
|
||||
return parser.parseFromString(xmlStr, "text/xml");
|
||||
};
|
||||
|
||||
export const selectCurrentSectionTitleDescriptions = (doc, currentSection) => {
|
||||
return xpath.select(
|
||||
"//form/tabs/tab[" + currentSection + " ]/labels/label/@description",
|
||||
doc
|
||||
);
|
||||
};
|
||||
|
||||
export const selectCurrentSectionRows = (doc, currentSection) => {
|
||||
return xpath.select(
|
||||
"/form/tabs/tab[" +
|
||||
currentSection +
|
||||
"]/columns//sections/section/rows/row",
|
||||
doc
|
||||
);
|
||||
};
|
||||
|
||||
export const selectFormTabs = (doc) => {
|
||||
return xpath.select("/form/tabs", doc);
|
||||
};
|
||||
|
||||
export const selectAllTabTitleDescriptions = (doc) => {
|
||||
return xpath.select("//form/tabs/tab[*]/labels/label/@description", doc);
|
||||
};
|
||||
|
||||
export const parseRowOuterHTML = (outerHTML) => {
|
||||
const parser = new DOMParser();
|
||||
return parser.parseFromString(outerHTML, "text/xml");
|
||||
};
|
||||
|
||||
export const selectRowLabelDescriptions = (doc) => {
|
||||
return xpath.select("//label/@description", doc);
|
||||
};
|
||||
|
||||
export const selectRowFieldType = (doc) => {
|
||||
return xpath.select("//@classid", doc);
|
||||
};
|
||||
|
||||
export const selectRowValidation = (doc) => {
|
||||
return xpath.select("//@validation", doc);
|
||||
};
|
||||
|
||||
export const selectRowMaxFieldLength = (doc) => {
|
||||
return xpath.select("//@maxFieldLength", doc);
|
||||
};
|
||||
|
||||
export const selectRowMaxSubField = (doc) => {
|
||||
return xpath.select("//@maxsubfield", doc);
|
||||
};
|
||||
|
||||
export const selectRowDataFieldName = (doc) => {
|
||||
return xpath.select("//@datafieldname", doc);
|
||||
};
|
||||
|
||||
export const selectRowDataFieldContent = (doc) => {
|
||||
return xpath.select("//@datafieldcontent", doc);
|
||||
};
|
||||
|
||||
export const selectRowParentField = (doc) => {
|
||||
return xpath.select("//@parentField", doc);
|
||||
};
|
||||
|
||||
export const selectRowParentFieldShowOnValue = (doc) => {
|
||||
return xpath.select("//@parentFieldShowOnValue", doc);
|
||||
};
|
||||
|
||||
export const selectRowRequiredDocumentValue = (doc) => {
|
||||
return xpath.select("//@requiredDocumentValue", doc);
|
||||
};
|
||||
|
||||
export const selectRowRequiredDocumentLabel = (doc) => {
|
||||
return xpath.select("//@requiredDocumentLabel", doc);
|
||||
};
|
||||
|
||||
export const selectRowDocumentTypeCode = (doc) => {
|
||||
return xpath.select("//@ishareDocumentCode", doc);
|
||||
};
|
||||
|
||||
export const selectRowHint = (doc) => {
|
||||
return xpath.select("//label/@hint", doc);
|
||||
};
|
||||
|
||||
export const selectRowDateStart = (doc) => {
|
||||
return xpath.select("//@dateStart", doc);
|
||||
};
|
||||
|
||||
export const selectRowDateEnd = (doc) => {
|
||||
return xpath.select("//@dateEnd", doc);
|
||||
};
|
||||
|
||||
export const selectErrorFieldLabelDescription = (doc, whichField) => {
|
||||
return xpath.select(
|
||||
"//*[control/@id='" + whichField + "']//@description",
|
||||
doc
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { uploadFiles } from "../../actions/services/documentService";
|
||||
import { sendEmail } from "../../actions/services/notifyService";
|
||||
import { sendCaseCompleteMessage } from "../../actions/services/portalService";
|
||||
import { generateAppealPDF } from "../../actions/services/documentService";
|
||||
|
||||
export const uploadAppealFilesEffect = (
|
||||
dataObj,
|
||||
fileListObj,
|
||||
containerID,
|
||||
caseReference
|
||||
) => {
|
||||
return uploadFiles(dataObj, fileListObj, containerID, caseReference);
|
||||
};
|
||||
|
||||
export const sendPartialSaveEmailEffect = (
|
||||
templateId,
|
||||
emailAddress,
|
||||
personalisation,
|
||||
reference
|
||||
) => {
|
||||
return sendEmail(templateId, emailAddress, personalisation, reference);
|
||||
};
|
||||
|
||||
export const sendCompletionEmailEffect = (
|
||||
templateId,
|
||||
emailAddress,
|
||||
personalisation,
|
||||
reference
|
||||
) => {
|
||||
return sendEmail(templateId, emailAddress, personalisation, reference);
|
||||
};
|
||||
|
||||
export const generateAppealPDFEffect = (
|
||||
pdfObj,
|
||||
containerID,
|
||||
caseReference,
|
||||
appealType,
|
||||
filesList,
|
||||
options
|
||||
) => {
|
||||
return generateAppealPDF(
|
||||
pdfObj,
|
||||
containerID,
|
||||
caseReference,
|
||||
appealType,
|
||||
filesList,
|
||||
options
|
||||
);
|
||||
};
|
||||
|
||||
export const sendCaseCompleteMessageEffect = (
|
||||
containerID,
|
||||
caseReference,
|
||||
typeOfInvolvement
|
||||
) => {
|
||||
return sendCaseCompleteMessage(
|
||||
containerID,
|
||||
caseReference,
|
||||
typeOfInvolvement
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
export const normalizeYesNoBooleans = (updateBody) => {
|
||||
updateBody = JSON.stringify(updateBody);
|
||||
updateBody = updateBody.replace(/:\"Yes\"/gm, `:true`);
|
||||
updateBody = updateBody.replace(/:\"No\"/gm, `:false`);
|
||||
updateBody = JSON.parse(updateBody);
|
||||
|
||||
return updateBody;
|
||||
};
|
||||
|
||||
export const removeNullKeys = (updateBody) => {
|
||||
Object.keys(updateBody).forEach((key) => {
|
||||
if (updateBody[key] === null) {
|
||||
delete updateBody[key];
|
||||
}
|
||||
});
|
||||
|
||||
return updateBody;
|
||||
};
|
||||
|
||||
export const stripInternalKeys = (updateBody) => {
|
||||
Object.keys(updateBody).forEach((key) => {
|
||||
if (key.indexOf("_") == 0) {
|
||||
delete updateBody[key];
|
||||
}
|
||||
});
|
||||
|
||||
return updateBody;
|
||||
};
|
||||
@@ -912,6 +912,63 @@ Follow-ups:
|
||||
|
||||
- Breadcrumb route-state extraction closure slices (1/2/3) complete.
|
||||
|
||||
### CL-22541-Z4: breadcrumbs post-closure hotfix — callable label resolver parity (static + view-all)
|
||||
|
||||
date: 2026-04-09
|
||||
author: Cline
|
||||
scope: `components/breadcrumbs.js`, `tests/phase22/breadcrumbs-route-map-structure.test.cjs`
|
||||
type: change
|
||||
rationale: Fix post-refactor regression where some breadcrumbs rendered blank labels because mapped label entries were returned as function references instead of resolved values.
|
||||
impact: Restores user-visible breadcrumb labels for static and view-all keyed routes; no auth/session/API/security behavior change.
|
||||
status: completed
|
||||
|
||||
Summary:
|
||||
|
||||
- Fixed static text breadcrumb label resolver to invoke mapped label functions:
|
||||
- `getStaticTextCrumbLabel(path)` now resolves callable entries (`resolver ? resolver() : null`).
|
||||
- Fixed view-all label resolver parity to support both function-backed and string-backed map entries:
|
||||
- `getViewAllLabel(viewKey)` now invokes function entries and returns string entries directly.
|
||||
- Added structure guard assertions to prevent regression of callable label resolution behavior.
|
||||
|
||||
Validation:
|
||||
|
||||
- `node tests/phase22/breadcrumbs-route-map-structure.test.cjs` -> pass (4/4).
|
||||
- `node tests/phase22/index.test.cjs` -> pass (combined suite).
|
||||
|
||||
Follow-ups:
|
||||
|
||||
- Breadcrumb label resolver parity now aligned across mapped/static/view-all label paths.
|
||||
|
||||
### CL-22541-Z5: architecture comparison overview refresh (senior architecture review)
|
||||
|
||||
date: 2026-04-09
|
||||
author: Cline
|
||||
scope: `context/architecture-overview-2026-04-09.md`
|
||||
type: change
|
||||
rationale: User requested a fresh senior architecture review and comparison baseline covering scalability, maintainability, coupling, boundary quality, operational/deployment risk, and prioritized technical debt actions.
|
||||
impact: Documentation-only architecture guidance update; no runtime/auth/session/security/API behavior change.
|
||||
status: completed
|
||||
|
||||
Summary:
|
||||
|
||||
- Added new architecture comparison baseline document:
|
||||
- `context/architecture-overview-2026-04-09.md`
|
||||
- Included requested structure:
|
||||
1. current architecture summary
|
||||
2. strengths
|
||||
3. risks (scalability, maintainability, coupling, weak boundaries, operational/deployment)
|
||||
4. prioritized recommendations
|
||||
5. low-risk next improvements
|
||||
- Added explicit comparison notes vs existing `context/architecture.md` and identified strategic focus areas for the next modernization wave.
|
||||
|
||||
Validation:
|
||||
|
||||
- Documentation coherence review against current repo architecture docs (`context/architecture.md`, `context/integration-map.md`, `context/project-overview.md`).
|
||||
|
||||
Follow-ups:
|
||||
|
||||
- Optionally elevate this architecture overview into a periodic architecture scorecard cadence and add measurable KPIs in memory-bank.
|
||||
|
||||
### CL-00X: 22500 `components/elements/index.js` Phase 1 helper extraction
|
||||
|
||||
date: 2026-04-07
|
||||
|
||||
Reference in New Issue
Block a user