Files
pedwfrontend/reports/case-status-tab-stage-display-characterization.md
T
2026-06-18 16:02:40 +00:00

20 KiB

Case Status Tab Stage Display Characterization

1. Purpose

This report characterizes how the portal determines and renders the visual case progress journey shown on the Case Status tab.

Scope is limited to the case status tab and directly related stage-display behaviour.

This is discovery/documentation only. It does not propose implementation changes, refactoring, field normalization, or new domain policy.


2. Business context

  • statuscode is the standard CRM/Dataverse Incident status field.
  • pinswg_casestage is a PEDW custom field used to represent more detailed PEDW case-stage information than the standard CRM status model alone provides.
  • The portal presents visual case progression to users through the Case Status tab.
  • The same broader area also exposes status/stage information elsewhere, including summary/details and search-result consumers.
  • Current repository evidence shows these consumers do not all use the same source field or the same interpretation model.

3. Status tab rendering flow

3.1 CRM inputs fetched on case page load

The public case page loader in pages/case/[ticketnumber].js fetches:

  1. searchResultsObj via getBasicSearch(...)
  2. searchDetailsObj via getSearchDetails(searchResultsObj)

Relevant CRM-derived inputs then get stored into currentView.caseReference using setCurrentReference(...):

  • statuscode
  • ticketnumber
  • incidentid
  • pinswg_appealcasetype as appealType
  • normalized specialist process from searchDetailsObj[0].value[0]

Code reference:

  • pages/case/[ticketnumber].js:213-229

This means the status-tab journey is seeded primarily from:

  • CRM statuscode
  • CRM appeal type
  • CRM specialist-process fields after portal-side normalization

3.2 Specialist-process normalization before rendering

pages/case/[ticketnumber].js calls:

normalizeSpecialistProcess(searchDetailsObj[0].value[0]);

normalizeSpecialistProcess(...) resolves specialist process by preferring:

  1. pinswg_specialistcaseprocess
  2. pinswg_speacialistcaseprocess
  3. empty string fallback

Code references:

  • pages/case/[ticketnumber].js:225-227
  • lib/domain/case-lifecycle/normalizeSpecialistProcess.js:1-7

This is a CRM compatibility step, not a semantic reinterpretation step.

3.3 Case summary hosts the Case Status tab

components/case/summary.js owns the tab structure and mounts the status-tab component.

  • The tab key is case-status
  • The tab panel renders <StatusDetails />

Code evidence:

  • components/case/summary.js contains the selected tab state whichTab == "case-status"
  • components/case/summary.js renders <StatusDetails />

3.4 StatusDetails builds the stage journey

components/case/status.js reads from Redux/currentView and invokes:

getLifecycleStagesForCase(
    props.currentView?.caseReference?.appealType,
    props.currentView?.caseReference?.statuscode,
    specialistProcess
);

Important points:

  • The second argument is named currentStageId in the lifecycle seam, but the actual caller passes CRM statuscode.
  • The component does not use pinswg_casestage.
  • specialistProcess is read again from props.searchResultsObj?.searchDetailsObj[0]?.value[0]?.pinswg_specialistcaseprocess, i.e. only the canonical field in this component.

Code reference:

  • components/case/status.js:26-36

3.5 Lifecycle seam delegates to legacy stage-catalogue helper

getLifecycleStagesForCase(...) is a thin wrapper:

return getStagesForAppealType(caseType, currentStageId, specialistProcess);

Code reference:

  • lib/domain/case-lifecycle/getLifecycleStagesForCase.js:1-9

This confirms the lifecycle seam is currently a read-only indirection layer, not an independently owned lifecycle engine.

3.6 Stage catalogue selection

getStagesForAppealType(...) in components/case/summary/utils/caseStagesByAppealType.js:

  1. resolves a stage key using getStageCaseTypeKey(...)
  2. loads the stage array with getStagesForCaseTypeKey(...)
  3. finds the current stage index with getLifecycleStageIndex(...)
  4. marks each stage complete, in-progress, or not-started with getLifecycleStageStatus(...)

Code references:

  • components/case/summary/utils/caseStagesByAppealType.js:779-796

3.7 Stage journey rendering

components/case/status.js renders the returned stage array as a GOV.UK task-list style visual journey:

  • each stage renders translated titleKey
  • each stage can show descriptionKey in expandable details
  • each stage gets a status tag:
    • complete
    • in-progress
    • not-started
    • blocked is defined in the map, though current lifecycle helper path does not produce it

Code references:

  • components/case/status.js:9-21
  • components/case/status.js:80-137

Rendering-flow summary

CRM data flow to rendered journey is:

CRM incident/search query
    -> searchResultsObj.statuscode
    -> searchResultsObj.pinswg_appealcasetype
    -> searchDetailsObj specialist-process field(s)
    -> setCurrentReference(...) on case page
    -> StatusDetails reads currentView.caseReference
    -> getLifecycleStagesForCase(...)
    -> getStagesForAppealType(...)
    -> getStageCaseTypeKey(...)
    -> getStagesForCaseTypeKey(...)
    -> getLifecycleStageIndex(...)
    -> getLifecycleStageStatus(...)
    -> rendered task-list stage journey

4. Field role assessment

statuscode

Where used

  • stored into currentView.caseReference on case page load (pages/case/[ticketnumber].js)
  • used as the current stage input for getLifecycleStagesForCase(...) (components/case/status.js)
  • displayed directly as formatted status labels in search results, summary-type components, my portal, and admin/reporting consumers
  • mapped in components/utils/caseStagesObj.js

Purpose

  • operational CRM status field
  • primary input for status-tab current-stage resolution
  • direct formatted label source for many non-status-tab consumers

Display impact

  • determines which step in the visual stage journey is marked as current/completed/future
  • also appears directly as human-readable status labels outside the journey UI

pinswg_casestage

Where used

  • displayed in components/case/summary.js as a summary row labelled summary-case-stage-label

Purpose

  • PEDW custom case-stage field
  • summary/details informational display field

Display impact

  • affects the summary/details panel only in the reviewed path
  • does not drive the Case Status tab journey in the current implementation

Appeal type fields

Where used

  • pinswg_appealcasetype stored as appealType in currentView.caseReference
  • mapped through mapAppealType(...)
  • used to choose the stage catalogue and, in some cases, specialist-process override logic

Purpose

  • primary selector of the stage-journey family/catalogue

Display impact

  • determines which stage list the user sees
  • appeal types can share or diverge in stage journeys depending on mappings/aliases

Specialist process fields

Where used

  • normalized in pages/case/[ticketnumber].js from canonical/misspelled CRM fields
  • re-read in components/case/status.js from canonical field only
  • passed to getStageCaseTypeKey(...)
  • used by mapSpecialistProcessStageType(...) for stage-journey override

Purpose

  • refine stage-journey selection for selected appeal types, especially Rights of Way variants

Display impact

  • can switch the status-tab journey from one stage catalogue to another
  • affects selection, not just labels

Closed/terminal status fields

Where used

  • isClosedCaseStatus(...) treats these values as closed/terminal:
    • 1000
    • 5
    • 6
    • 846040013
    • 846040060
    • 846040059

Purpose

  • fallback mechanism when the current numeric input does not exactly match a stage in the selected catalogue

Display impact

  • if the supplied statuscode is considered closed, current stage resolves to the stage whose titleKey === "case-closed"
  • otherwise unmatched values produce currentIndex === -1, making all stages render as not-started

5. Stage journey selection

5.1 Appeal-type selection

The portal selects a stage journey first through mapAppealType(...) and caseTypeKeyByAppealTypeId.

Examples:

  • 846040000 -> PLANNING_S78
  • 846040010 -> CALL_INS
  • 846040018 -> ADVERTS
  • 846040019 -> COMPULSORY_PURCHASE_ORDERS
  • 846040002 -> SIP
  • 846040011 -> DNS

Code reference:

  • lib/domain/case-lifecycle/mapAppealType.js:35-80

5.2 Appeal-family / alias behaviour

Some stage journeys are shared by alias or indirection rather than unique arrays.

Examples:

  • SIP resolves by string indirection to DNS
  • CONDITIONS_73_79, LBCAC, LDCS, PLANNING_OBLIGATIONS_S106, PRIOR_NOTIFICATION all share PLANNING_S78
  • ENFORCEMENT_LISTED_BUILDING and MAINTENANCE_OF_LAND share ENFORCEMENT

This is handled by:

  • alias mapping in mapAppealType.js
  • string indirection in getStagesForCaseTypeKey.js

5.3 Specialist-process selection

getStageCaseTypeKey(...) only applies specialist-process override when the numeric appeal-type mapping is the one in play:

caseTypeKeyByAppealTypeId[appealTypeId] === mappedCaseType
    ? mapSpecialistProcessStageType(mappedCaseType, specialistProcess)
    : undefined;

Current override table only exists for RIGHTS_OF_WAY_SCHEDULE_14, mapping specialist-process values into:

  • RIGHTS_OF_WAY_ORDERS
  • RIGHTS_OF_WAY_SCHEDULE_14
  • REQUESTS_FOR_DIRECTION

Code references:

  • lib/domain/case-lifecycle/getStageCaseTypeKey.js:9-25
  • lib/domain/case-lifecycle/mapSpecialistProcessStageType.js:1-19

5.4 Fallback behaviour

Fallback order in getStageCaseTypeKey(...) is:

  1. specialist-process-derived stage key
  2. mapped appeal type key
  3. alias lookup from normalized input
  4. normalized raw input

Then getStagesForCaseTypeKey(...):

  • returns [] when no entry exists
  • resolves string indirection to another catalogue
  • otherwise returns the array directly

Selection classification

Selection appears mixed:

  • CRM-driven: because appeal type and specialist process are CRM inputs
  • presentation-driven: because the output is selection of a visual journey catalogue with title/description text
  • business-driven: partially, because different appeal types and specialist processes are clearly treated as meaningfully different journeys
  • not purely CRM status display: because the portal constructs a catalogue-driven journey rather than simply displaying a single CRM status value

6. Current stage resolution

6.1 Exact status matching

getLifecycleStageIndex(...) converts the incoming current value to a number and tries to find a stage whose stageId exactly matches it.

const exactIndex = stages.findIndex(
    (stage) => Number(stage.stageId) === currentStageNumber
);

This means the status-tab journey currently treats the passed statuscode as the numeric identifier to align with stage-catalogue stageId values.

6.2 Closed-stage fallback

If exact matching fails, isClosedCaseStatus(...) is consulted.

If true, current index becomes the index of the stage whose titleKey is case-closed.

This is a special fallback path for terminal/closed values rather than exact stage matching.

6.3 Unmatched status handling

If there is no exact match and the value is not recognized as closed, getLifecycleStageIndex(...) returns -1.

Then getLifecycleStageStatus(...) marks all stages as not-started.

That is important characterized behaviour.

6.4 Completed/current/future stage calculation

getLifecycleStageStatus(index, currentIndex) returns:

  • complete when index < currentIndex
  • in-progress when index === currentIndex
  • not-started when index > currentIndex
  • not-started for all stages when currentIndex === -1

6.5 Blocked behaviour

components/case/status.js includes blocked style/text mappings, but the lifecycle calculation path reviewed here never returns blocked.

This suggests either legacy/future intent or another non-reviewed path, but not active current-stage calculation from the status-tab helper chain.


7. Consumer comparison

Case Status Tab

Displays:

  • stage journey: yes
  • CRM status: indirectly, as the input driving current stage selection
  • custom case stage (pinswg_casestage): no
  • formatted labels: yes, but as translated stage titles/descriptions and progress tags, not CRM formatted status labels
  • derived values: yes, complete/in-progress/not-started status is portal-derived

Primary inputs:

  • statuscode
  • appeal type
  • specialist process

Case Details / Summary

Displays:

  • CRM status: yes, via summary-type components using statuscode@OData.Community.Display.V1.FormattedValue
  • custom case stage: yes, in components/case/summary.js via pinswg_casestage@OData.Community.Display.V1.FormattedValue
  • stage journey: yes, but only through the separate Case Status tab component
  • formatted labels: yes
  • derived values: limited; mostly direct display of CRM formatted values

Important distinction:

  • summary/details panel uses pinswg_casestage directly for one row
  • status tab uses statuscode-driven stage-journey derivation

Search Results

Displays:

  • CRM status: yes, direct formatted value via statuscode@OData.Community.Display.V1.FormattedValue
  • custom case stage: no evidence in reviewed search results path
  • stage journey: no
  • formatted labels: yes
  • derived values: no stage derivation; mostly direct CRM-formatted display

Additional role:

  • clicking a search result seeds currentView.caseReference.statuscode, appeal type, and specialist process for downstream case-page/status-tab behaviour

8. Divergence catalogue

Divergence 1: Status tab uses statuscode, summary row uses pinswg_casestage

  • Behaviour: summary/details can display PEDW custom case stage while the Case Status tab derives its journey from statuscode.
  • Likely reason: summary row is informational display of a custom PEDW field; status tab is a portal-constructed journey over a separate stage catalogue.
  • Appears intentional? No / unclear. It is clearly implemented, but the repository does not prove this distinction is consciously documented as an intentional model boundary.

Divergence 2: Status tab renders derived journey; search results render direct formatted status label

  • Behaviour: search results show statuscode@OData.Community.Display.V1.FormattedValue, while the case status tab shows a multi-step journey.
  • Likely reason: search results need compact list display; case page gives richer progress visualization.
  • Appears intentional? Yes, at presentation level.

Divergence 3: Specialist-process normalization differs between case page and status tab component

  • Behaviour:
    • case page uses normalizeSpecialistProcess(...) with canonical + misspelled fallback
    • components/case/status.js reads only pinswg_specialistcaseprocess
  • Likely reason: partial refactor / mixed old-new access patterns.
  • Appears intentional? No based on current evidence.

Divergence 4: Summary/status consumers display different concepts under similar “status/stage” area

  • Behaviour: summary-type components use formatted statuscode; summary row separately uses pinswg_casestage; status tab ignores pinswg_casestage and derives stage journey from statuscode.
  • Likely reason: mixture of CRM operational status display and PEDW custom stage display without one unified presentation model.
  • Appears intentional? Unclear.

Divergence 5: Closed-state handling is explicit in journey logic but not equivalent in direct status consumers

  • Behaviour: status tab has a special closed-case fallback through isClosedCaseStatus(...); direct status-label consumers simply display CRM formatted values.
  • Likely reason: journey UI requires a terminal visual state even when exact stage IDs do not match.
  • Appears intentional? Probably yes for the journey component, though the exact closed-value set may be compatibility-driven.

Divergence 6: Unmatched status values collapse whole journey to not-started

  • Behaviour: if a statuscode does not match any stage ID and is not considered closed, every stage renders as not-started.
  • Likely reason: simple index-based implementation with no fallback “unknown current stage” state.
  • Appears intentional? No / unclear; more likely inherited behaviour.

9. Characterization test candidates

High Priority

  1. Case page seeds status tab inputs correctly

    • protect that statuscode, appeal type, and normalized specialist process are placed into currentView.caseReference
  2. Stage catalogue selection by appeal type

    • representative coverage for shared and distinct catalogues (e.g. S78, Call-in, DNS, Advert, CPO)
  3. Specialist-process override selection

    • protect current Rights of Way specialist-process remapping into alternate stage journeys
  4. Exact status-to-stage matching

    • prove that matching statuscode selects the expected current stage index
  5. Closed-case fallback behaviour

    • prove unmatched closed values map to case-closed
  6. Unmatched non-closed status behaviour

    • prove all stages become not-started
  7. Status tab ignores pinswg_casestage

    • protect current separation between status-tab journey logic and custom case-stage summary display

Medium Priority

  1. String vs numeric appeal type handling in stage selection
  2. Alias/indirection resolution
    • e.g. SIP -> DNS, ENFORCEMENT_LISTED_BUILDING -> ENFORCEMENT
  3. Specialist-process normalization divergence coverage
    • protect current canonical vs misspelled field handling at case-page input stage
  4. Representative summary/status/search comparison tests
    • prove summary row uses pinswg_casestage, search uses formatted statuscode, status tab uses derived journey

Low Priority

  1. Expand-all/default-open UI behaviour on the status tab
  2. Presence/absence of description keys in journey rendering
  3. Translation-key coverage for representative stage catalogues

10. Initial conclusion

Answer: C. Portal-constructed visual journey based on CRM inputs

Supporting evidence

  • The Case Status tab does not directly display pinswg_casestage.
  • It does not simply render CRM formatted statuscode labels.
  • Instead, it uses CRM statuscode, appeal type, and specialist process to select a portal-owned stage catalogue and then derive progress state across that catalogue.
  • The selected catalogue contains portal presentation metadata (titleKey, descriptionKey) and is rendered as a visual journey/task list.
  • pinswg_casestage is displayed elsewhere in summary/details as a separate PEDW custom field, which further shows that the Case Status tab is a constructed layer rather than a simple raw-field display.

Therefore the Case Status tab is best understood as:

a portal-constructed visual journey that uses CRM operational inputs and portal stage catalogues to present case progression to the user.

It is not a pure CRM status display, and it is not currently a pure PEDW custom case-stage display either.


Keep the next slice narrow and characterization-focused.

The strongest follow-on is:

  1. characterize summary/details/search divergence around:
    • statuscode
    • pinswg_casestage
    • formatted labels
    • specialist-process field usage

Reason:

  • this slice established how the Case Status tab works internally
  • the remaining ambiguity now sits at the cross-consumer boundary, especially where summary/details and search display different lifecycle/status concepts from the status-tab journey