Author SHA1 Message Date
robbond e5ada29cdb made getstatus serverside not client 2026-05-28 11:34:50 +01:00
robbond 64dc4e78d2 csp fix remove unneccessary params 2026-05-28 11:00:23 +01:00
Robert Bond 24cd7000f7 Merged PR 2350: added in count on case status dropdown
added in count on case status dropdown

Related work items: #23447
2026-05-28 09:02:30 +00:00
Robert Bond 905c82f598 Merged PR 2349: updated dropdown to show all new case stages
updated dropdown to show all new case stages

Related work items: #23447
2026-05-28 07:05:06 +00:00
Robert Bond 26e8464d46 Merged PR 2348: corrected welsh version of representations period
corrected welsh version of representations period

Related work items: #23319
2026-05-27 14:29:20 +00:00
Robert Bond 1df5573fce Merged PR 2346: updated close status on non val
updated close status on non val

Related work items: #23365
2026-05-26 16:11:48 +00:00
Robert Bond 25485d73ed Merged PR 2345: typo
typo

Related work items: #23365
2026-05-26 14:57:34 +00:00
Robert Bond 6d8d56d745 Merged PR 2343: update to non val
Related work items: #23365
2026-05-26 13:42:09 +00:00
Robert Bond f6bbaa5c1d Merged PR 2342: ammend case status and descriptions per bugs
ammend case status and descriptions per bugs

Related work items: #23319, #23357, #23359, #23365, #23394
2026-05-26 07:50:33 +00:00
Robert Bond 4f3e3f5183 Merged PR 2338: updated welsh translation
updated welsh translation

Related work items: #23319
2026-05-21 12:28:36 +00:00
Robert Bond a6cc030612 Merged PR 2333: updated welsh link on events tab
updated welsh link on events tab

Related work items: #22423
2026-05-19 15:26:33 +00:00
Robert Bond 2e98be3b65 Merged PR 2331: updated welsh text
updated welsh text

Related work items: #23094
2026-05-19 05:23:14 +00:00
Robert Bond 149238dd4e Merged PR 2330: welsh translation update and reps and site visit fix
welsh translation update and reps and site visit fix

Related work items: #23087, #23088
2026-05-18 12:18:03 +00:00
Robert Bond b6bd543527 Merged PR 2329: welsh link for events
welsh link for events

Related work items: #23054
2026-05-18 08:50:58 +00:00
Robert Bond 5967665138 Merged PR 2325: missing welsh translations on search results
missing welsh translations on search results

Related work items: #23044
2026-05-15 15:08:53 +00:00
Robert Bond 9860f47cbe Merged PR 2324: updated status message for case started/case in progrsss
updated status message for case started/case in progrsss

Related work items: #23064
2026-05-15 13:14:23 +00:00
Robert Bond 5bf1393020 Merged PR 2322: removed the ? on event
removed the ? on event

Related work items: #23063
2026-05-15 10:33:01 +00:00
Robert Bond 5a8fd73bfd Merged PR 2321: updated welsh status on search results and status tab descriptions
updated welsh status on search results and status tab descriptions

Related work items: #23044, #23045
2026-05-14 15:03:36 +00:00
Robert Bond c2ae6964f9 Merged PR 2318: auth stabilisation: extract shared myportal auth guard helper
auth stabilisation: extract shared myportal auth guard helper

ntroduces a small shared SSR helper (resolveMyPortalAuthContext) to standardize common myportal auth/session guards (session presence, session user identity, and pinsUser cookie) with preserved reason-coded diagnostics and signin redirect behavior. Migrates exactly two loaders (pages/myportal/searchresults.js, pages/myportal/addresssearchresults.js) to use the helper while keeping loader-specific UPSTREAM_FAILURE and CONTACT_LOOKUP_FAILED logic unchanged.

Related work items: #23020
2026-05-14 10:38:45 +00:00
Robert Bond 4fb62a6773 Merged PR 2317: Auth stabilisation: add reason-coded guards to MyPortal loaders
Adds incremental auth/session hardening across myportal loader paths (loadMyPortalAppealPage, searchresults, addresssearchresults) with explicit guard ordering and reason-coded diagnostics for missing session, missing session identity, missing cookie identity, contact/account lookup failures, and upstream dependency failures. Includes targeted Phase22 loader guard tests and keeps redirect behaviour policy unchanged.

Related work items: #23020
2026-05-14 10:27:12 +00:00
Robert Bond d0b2fd077a Merged PR 2315: Auth stabilistatiion and hardening
Related work items: #23020
2026-05-14 08:55:49 +00:00
Robert Bond d295a507ac Merged PR 2303: updated company name to pinswg_companyname
updated company name to pinswg_companyname

Related work items: #22774
2026-05-11 10:47:39 +00:00
Robert Bond 4f90c9a058 Merged PR 2302: updated text as per vickis request
updated text as per vickis request

Related work items: #22987
2026-05-11 09:52:19 +00:00
Robert Bond 0b02acf035 Merged PR 2300: updated final comments matching string
updated  final comments matching string

Related work items: #22842
2026-05-08 10:31:27 +00:00
Robert Bond 12a28a4be0 Merged PR 2296: added in statuscode and summary description
added in statuscode and summary description

Related work items: #22612, #22616
2026-05-07 10:20:28 +00:00
Robert Bond 1a2c3ede3b Merged PR 2295: resolved lint warnings
resolved lint warnings

Related work items: #22873
2026-05-05 17:13:53 +00:00
Robert Bond 46a0e0b185 Merged PR 2291: reps array refactor and update and tests
reps array refactor and update and tests

Related work items: #22873
2026-05-05 10:16:27 +00:00
Robert Bond 0ff2d69ba6 Merged PR 2290: add guard rail to check for locale and ensure verify request is language spec...
add guard rail to check for locale and ensure verify request is language specific

Related work items: #22303
2026-05-05 08:25:17 +00:00
Robert Bond d2e88b57d8 Merged PR 2288: add in company name for appellant and autofill agent details in new appeal
add in company name for appellant and autofill agent details in new appeal

Related work items: #22842
2026-04-30 16:25:03 +00:00
Robert Bond b596374c3c Merged PR 2286: allowed LPAs to make statements
allowed LPAs to make statements

Related work items: #22848
2026-04-30 09:15:53 +00:00
Robert Bond 9140476a2a Merged PR 2285: tweak to live link
tweak to live link

Related work items: #22423
2026-04-29 17:03:50 +00:00
Robert Bond baf5b1838d Merged PR 2283: Data driven status pages from CRM
Related work items: #22805
2026-04-29 15:29:54 +00:00
Robert Bond 1fd192b870 Merged PR 2282: replaced deadlineextensiuondate
replaced deadlineextensiuondate

Related work items: #22783
2026-04-29 08:02:29 +00:00
Robert Bond 8b187742af Merged PR 2281: update logic for reps arr take isAppellant from capacity selection not type o...
update logic for reps arr take isAppellant from capacity selection not type of involvement

Related work items: #22835
2026-04-28 16:07:35 +00:00
Robert Bond 421adafca3 Merged PR 2280: fix in place for app error
fix in place for app error

Related work items: #22834
2026-04-28 15:48:32 +00:00
Robert Bond a25ab433a6 Merged PR 2279: passed in case customer email in search and update rep logic to stop non owne...
passed in case customer email in search and update rep logic to stop non owners rasing reps on other owners cases

Related work items: #22095
2026-04-28 12:00:12 +00:00
Robert Bond 843e6adee6 Merged PR 2278: mocked design for status tab
mocked design for status tab

Related work items: #22805
2026-04-28 09:36:48 +00:00
Robert Bond b2945790b1 Merged PR 2274: case deadline and watch stream on results
case deadline and wat

Related work items: #22423, #22783
2026-04-27 13:07:46 +00:00
Robert Bond 138f03912b Merged PR 2273: check for existing record and not insert if exists
check for existing record and not insert if exists

Related work items: #22441
2026-04-24 15:56:54 +00:00
Robert Bond d406d97ddf Merged PR 2272: final comments added and update _IP_ precendence to be lower
final comments added and update _IP_ precendence to be lower

Related work items: #22573, #22661
2026-04-24 14:44:24 +00:00
Robert Bond f367f3342c Merged PR 2271: typo on videos and update portal sumamry fro removed fields
Related work items: #22698, #22813
2026-04-24 13:10:20 +00:00
Robert Bond d604d4f264 Merged PR 2269: added link for streaming events
added link for streaming events

Related work items: #22423
2026-04-23 14:34:08 +00:00
Robert Bond 43e37dc844 Merged PR 2268: adjusted styling tto center vertically dynamically
adjusted styling tto center vertically dynamically

Related work items: #22659
2026-04-23 04:59:06 +00:00
Robert Bond 7a14b7e8c9 Merged PR 2267: added ai guidance overlay
added ai guidance overlay

Related work items: #21744
2026-04-22 13:28:05 +00:00
Robert Bond b0004381ed Merged PR 2266: update media tabs display and query
update media tabs display and query

Related work items: #22698
2026-04-22 11:38:04 +00:00
Robert Bond 20782db916 Merged PR 2264: updatd createinvolvent call
updatd createinvolvent call

Related work items: #22441
2026-04-22 08:15:59 +00:00
Robert Bond f34ac3ba11 Merged PR 2263: createinvolvement updates contactinvolvement table
createinvolvement updates contactinvolvement table

Related work items: #22441
2026-04-21 16:16:31 +00:00
Robert Bond 5bf885933d Merged PR 2262: shows primary lpa and then lists additional lps below on the portal summary tab
shows primary lpa and then lists additional lps below on the portal summary tab

Related work items: #22699
2026-04-21 08:39:37 +00:00
Robert Bond 05d83822f2 Merged PR 2261: added in text and ui redesign for rep type selection
added in text and ui redesign for rep type selection

Related work items: #22574
2026-04-21 04:59:01 +00:00
Robert Bond 37a81522d5 Merged PR 2260: refactor(representations): complete slice-based refactor of representations flow
## Representations Refactor — Behaviour-Preserving Structural Improvements

This PR delivers a full refactor of the representations flow, improving structure, readability, and maintainability while preserving all existing behaviour.

The work was completed using a controlled, slice-based approach with strict guardrails and regression validation at each step. No changes have been made to user journeys, payloads, routing, or EN/CY behaviour.

The result is a cleaner, more maintainable codebase with reduced coupling and clearer separation of concerns, ready for future enhancements without increased risk.

---

## What Was Done

The refactor was delivered incrementally across the following slices:

- **R1** — Representation entry logic extraction
- **R2** — Page loader separation (SSR/data orchestration)
- **R3** — Journey step resolution extraction
- **R4** — Flow shell decomposition
- **R5** — Representation elements normalisation
- **R6** — Data/service layer cleanup
- **R7** — Summary rendering proof slice
- **R8** — Submission/finalisation boundary isolation
- **R9** — Summary rollout (Batch 1)

Each slice:
- was isolated to a single concern
- followed strict guardrails
- was validated before merge

Full detail is available in:
`context/representations-refactor-tracker.md`

---

## Key Improvements

- Reduced coupling across the representations journey
- Separated data loading, orchestration, and rendering concerns
- Simplified complex conditional logic into testable helpers
- Standardised summary rendering using shared primitives (`SummaryCard`, `SummaryRow`)
- Isolated submission/finalisation sequencing into explicit boundaries
- Improved overall readability and maintainability

---

## Behaviour Preservation

This refactor does **not** change:

- User journeys (APP / IP / Agent / LPA)
- Route and query behaviour
- Payload contracts and API interactions
- Redux state shape and usage
- Validation rules and messaging
- EN/CY behaviour
- File upload / PDF / email sequencing
- Linked-case logic

All changes are structural only.

---

## Validation

### Automated

- `npm run lint` — passed (warnings only, no new errors)
- `npm run test:reps` — passed (7/7)

### Manual

Validated end-to-end across:

- APP
- IP
- Agent
- LPA

Including:

- representation creation
- editing/resuming representations
- submission flow
- confirmation/completion behaviour
- summary rendering across case types
- EN/CY parity

---

## Risk Management

The refactor targeted several high-risk areas:

- Case summary entry logic
- Representation submission/finalisation sequencing
- Dual-mode entry (new vs existing representation)

Risk was controlled through:

- small, incremental slices
- one branch per slice
- regression validation per slice
- strict behaviour-preservation guardrails
- controlled rollout for summary rendering changes

---

## Reviewer Guidance

Suggested areas to focus on:

- End-to-end representation journey (create → submit → complete)
- S...
2026-04-20 13:09:07 +00:00
Robert Bond c8b08a04a6 Merged PR 2250: fixed typo
Related work items: #22632
2026-04-16 10:47:54 +00:00
Robert Bond bb4c3ee054 Merged PR 2247: added health endpoint to check pre prod container startup issues
added health endpoint to check pre prod container startup issues

Related work items: #22624
2026-04-15 09:52:15 +00:00
Robert Bond 15c35d201e Merged PR 2244: added condition to fix log in journey in welsh if no account in crm
added condition to fix log in journey in welsh if no account in crm

Related work items: #22081, #22303
2026-04-13 15:54:51 +00:00
Robert Bond 0de9268255 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
2026-04-13 13:09:12 +00:00
Robert Bond bd3d311cfb Merged PR 2231: refactor breadcrumb
Related work items: #22541
2026-04-09 15:36:06 +00:00
robbond ee2751a642 catch fix in view all 2026-04-09 16:34:41 +01:00
robbond ac74782030 fix(breadcrumbs): restore static and viewall label crumb resolution 2026-04-09 16:26:44 +01:00
robbond 7d195ad906 refactor(breadcrumbs): remove stale /newappeal inline no-op branch 2026-04-09 15:22:36 +01:00
robbond 61254e3942 test(breadcrumbs): cover missing representation route map fallback 2026-04-09 15:19:40 +01:00
robbond 97cde880c0 docs(breadcrumbs): record map precedence and callback invariants 2026-04-09 15:17:44 +01:00
robbond d3f4dda8af refactor(breadcrumbs): extract representation route-state renderer group 2026-04-09 15:16:01 +01:00
robbond 7a60e64a84 test(breadcrumbs): assert strict null on non-exact route map lookups 2026-04-09 15:06:41 +01:00
robbond cebd47bf1c refactor(breadcrumbs): map step-back route branches into renderer group 2026-04-09 15:03:19 +01:00
robbond 0b905f9374 refactor(breadcrumbs): extract callback route renderers into mapped groups 2026-04-09 14:57:47 +01:00
robbond 0cd78e6f46 22541 harden breadcrumb mapped route resolver against invalid entries 2026-04-09 14:45:12 +01:00
robbond 2a84056a76 22541 extract new appeal breadcrumb branch into mapped renderer group 2026-04-09 14:39:30 +01:00
robbond 17795f20c7 22541 extract deterministic case breadcrumb branches into factory maps 2026-04-09 14:33:54 +01:00
robbond 8edd23d621 22541 compose mapped breadcrumb groups in factory and add null fallback guard 2026-04-09 14:30:03 +01:00
robbond 96946ed205 Extract additional routes in to shared renderer factory 2026-04-09 13:56:17 +01:00
robbond c264830934 Extract case detail breadcrumb routes into shared renderer factory 2026-04-09 13:16:56 +01:00
robbond 062cd23463 Refactor breadcrumbs to use factory-based route renderers 2026-04-09 12:08:36 +01:00
robbond 5f6dde782a 22541 extract breadcrumb route-map helpers and add precedence guard tests 2026-04-09 10:25:47 +01:00
robbond 78d62c541f 22541 consolidate breadcrumb mapped route maps into grouped constants 2026-04-09 10:21:09 +01:00
robbond ee3f3eae74 22541 map myportal results pair routes through unified breadcrumb resolver 2026-04-09 10:16:45 +01:00
robbond 786f906ab8 22541 unify breadcrumb mapped-route resolver and widen route-map coverage 2026-04-09 10:02:02 +01:00
robbond 04dc8fd2b9 22541 extract simple link-text breadcrumb route map and add structure tests 2026-04-09 09:58:22 +01:00
robbond c464fbf13b 22541 introduce simple route renderer map for static breadcrumb branches 2026-04-09 09:54:49 +01:00
robbond a8ef55c65a 22541 extract grouped breadcrumb pair helpers for myportal and dns routes 2026-04-09 09:52:42 +01:00
robbond e24ab89e83 22541 consolidate static text breadcrumb route branches 2026-04-09 09:50:41 +01:00
robbond dd2ef694f8 22541 add semantic back crumb helper and consolidate edge breadcrumb branches 2026-04-09 09:47:53 +01:00
robbond e3e893ed58 22541 extract shared anchor/back breadcrumb helper patterns 2026-04-09 09:45:00 +01:00
robbond 30bc729c70 22541 consolidate remaining inline Link breadcrumb patterns 2026-04-09 09:41:33 +01:00
robbond d576987fcf 22541 extract shared text and link breadcrumb render helpers 2026-04-09 09:38:14 +01:00
robbond d75675f059 22541 extract shared case-reference breadcrumb list item helper 2026-04-09 09:34:24 +01:00
robbond 035f0e9adf 22541 extract shared my-portal breadcrumb list item helper 2026-04-09 09:32:21 +01:00
robbond f447aa19ea 22541 add bundled case breadcrumb state resolver 2026-04-09 09:28:54 +01:00
robbond 59d3ed287c 22541 expand breadcrumb route-state helpers for label and dns path reuse 2026-04-09 09:26:19 +01:00
robbond 1c06ef1639 22541 extract breadcrumb route-state helper and align phase22 auth locale test 2026-04-09 09:23:22 +01:00
Robert Bond 3cea83b7e1 Merged PR 2230: pass in locale to questionnaire for welsh version
pass in locale to questionnaire for welsh version

Related work items: #22534
2026-04-08 15:51:37 +00:00
robbond 1f5be7c9a2 pass in locale to questionnaire for welsh version 2026-04-08 16:50:47 +01:00
Robert Bond 87af9a2c4c Merged PR 2228: refactoring fix and fix for language dependant login and download questionnaire
Related work items: #22303, #22344, #22500
2026-04-08 11:20:59 +00:00
robbond 1b2f8e29c6 defect fix for language and login - crm precedence then current session 2026-04-08 12:17:28 +01:00
robbond 56e854ca2b defect fix for download local questionnaire 2026-04-08 11:18:41 +01:00
robbond 5d40fab5c4 Extract Radio, Numeric and Decimal wrappers 2026-04-08 10:00:01 +01:00
robbond dbebd03cac Extract CheckBox, DateFieldPicker and YesNo wrappers 2026-04-08 08:33:06 +01:00
robbond 8350e42d02 Remove dead and debug-only code from elements index 2026-04-08 08:23:51 +01:00
robbond 6c387fa6fb Extract PickList wrapper from elements index 2026-04-08 08:12:40 +01:00
robbond 30bd8c49da Extract visibility and validation helpers in elements index 2026-04-07 15:44:02 +01:00
robbond eccb87e4a1 Export FieldArrayForm from elements index barrel 2026-04-07 15:36:34 +01:00
robbond a3b848cfc1 Re-export RenderSubFields from elements index 2026-04-07 15:35:19 +01:00
robbond 497bb4c799 Extract RenderFileUpload and FieldArrayForm 2026-04-07 15:32:45 +01:00
robbond 5e992507de Clean dead code in FieldArrayForm slice 2026-04-07 14:27:31 +01:00
robbond dcdec08f6d Fix render-phase dispatch in FieldArrayForm 2026-04-07 12:37:29 +01:00
robbond 503a0b5b9a Fix render-phase update in RenderSubFields 2026-04-07 12:34:36 +01:00
robbond 52071058d6 Fix MultiLinefield hook order warning 2026-04-07 12:30:35 +01:00
robbond f886fb8f25 Remove unused renderField from elements index 2026-04-07 12:27:36 +01:00
robbond f8665ee6b7 Refactor field array subfields renderer into dedicated component 2026-04-07 12:17:15 +01:00
robbond 7ae5f80f26 Refactor checkbox, picklist, and decimal renderers into field components 2026-04-07 11:42:11 +01:00
robbond b022c0a0d9 Refactor elements renderers into dedicated field components 2026-04-07 11:25:48 +01:00
robbond 5b5a91b33b Make uploadsinglefile batch size env-configurable with fallback of 5 2026-04-07 11:24:17 +01:00
robbond aa4d1fad1e 22500 Phase 2: extract text and multiline leaf renderers 2026-04-07 09:45:19 +01:00
robbond 5b9498b3ef 22500 Phase 2: extract rich multiline leaf renderer 2026-04-07 09:40:25 +01:00
robbond 06b7855826 22500 Phase 1: extract pure helpers from elements index 2026-04-07 09:34:50 +01:00
Robert Bond 02512ec403 Merged PR 2227: batch uploads on frontend
batch uploads on frontend

Related work items: #22422
2026-04-02 15:11:05 +00:00
robbond c754e04034 batch uploads on frontend 2026-04-02 16:09:56 +01:00
Robert Bond da7e694120 Merged PR 2225: added fallback of 5 if no env var declared
added fallback of 5 if no env var declared

Related work items: #22422
2026-04-02 14:29:51 +00:00
robbond 786ed43fb2 added fallback of 5 if no env var declared 2026-04-02 15:29:15 +01:00
Robert Bond 638b51e55d Merged PR 2224: batch count from env var
batch count from env var

Related work items: #22422
2026-04-02 14:26:01 +00:00
robbond bddcd1e964 batch count from env var 2026-04-02 15:25:07 +01:00
Robert Bond bd13886222 Merged PR 2222: batch uploaded files in blocks of 10 at a time
batch uploaded files in blocks of 10 at a time

Related work items: #22422
2026-04-02 14:07:20 +00:00
robbond 69cf5d32b4 batch uploaded files in blocks of 10 at a time 2026-04-02 15:06:34 +01:00
Robert Bond fd76dfe9bc Merged PR 2215: update ldp query
update ldp query

Related work items: #21992
2026-04-02 09:03:21 +00:00
robbond 2add85b519 update ldp query 2026-04-02 10:02:12 +01:00
Robert Bond 82b99ae44f Merged PR 2210: Rep raising and pdf for welsh
Related work items: #22095, #22305
2026-03-30 11:46:22 +00:00
robbond 3bc3759a48 welsh downloadable new appeal form 2026-03-30 12:35:41 +01:00
robbond 13cd2514c7 ip app lpa ip can raise final comments 2026-03-30 12:34:48 +01:00
Robert Bond 461ca7dc02 Merged PR 2209: sprint 16 bug fixes
Related work items: #22079, #22151, #22304
2026-03-27 13:10:49 +00:00
robbond 536ca11f6b 22304 language switch on dnsapplications 2026-03-27 13:04:26 +00:00
robbond 2c4d645d56 22151 multiple files issue 2026-03-27 11:25:41 +00:00
robbond 1b145dd90f 22079 update text 2026-03-27 10:57:54 +00:00
Robert Bond 6f8bbd0131 Merged PR 2208: update and clean azurestorage.js
Related work items: #22326
2026-03-26 11:42:42 +00:00
robbond 830e36d0c4 TASK22269: reuse contentLength locals in touched azurestorage blob loops 2026-03-26 11:36:47 +00:00
robbond 48878e9ebd TASK22269: extract touched azurestorage case object path helper 2026-03-26 11:20:37 +00:00
robbond 5c62f3088e TASK22269: reuse azurestorage path builders in touched blob object fields 2026-03-26 11:18:50 +00:00
robbond 361328c14a TASK22269: prune touched azurestorage unused locals 2026-03-26 11:17:09 +00:00
robbond 427e1c4673 TASK22269: normalize touched azurestorage logging calls 2026-03-26 11:15:33 +00:00
robbond 9791270670 TASK22269: tidy azurestorage blob path split locals 2026-03-26 11:13:48 +00:00
robbond 58e1eef1f5 TASK22269: add phase22 azurestorage helper contract tests 2026-03-26 11:11:24 +00:00
robbond 8744d3dafc TASK22269: consolidate azurestorage hash metadata mapping 2026-03-25 14:27:52 +00:00
robbond fcce6a3978 TASK22269: normalize azurestorage hash query path builders 2026-03-25 14:21:09 +00:00
robbond 1b7e6009aa TASK22269: normalize document hash-suffix route composition 2026-03-25 14:13:53 +00:00
robbond 4e964ad1ad TASK22269: group migrate remaining account/portal signed GET flows 2026-03-25 14:06:18 +00:00
robbond 85cc7d165e TASK22269: delegate fileClient signed get/post to signedRequestClient 2026-03-25 13:47:46 +00:00
robbond 7470975d0e TASK22269: migrate portal deleteMyRepresentations to signed delete helper 2026-03-25 13:43:08 +00:00
robbond 811655c8d2 TASK22269: add signed request client and migrate portal deleteWatchedCases pilot 2026-03-25 13:37:49 +00:00
Robert Bond 35ff16b2b2 Merged PR 2207: updated refactoring docs
docs: add current-state context pack and actionable sequence-b prompts

Related work items: #22242
2026-03-25 13:21:02 +00:00
robbond 30fad8e954 docs: add current-state context pack and actionable sequence-b prompts 2026-03-25 13:08:05 +00:00
Robert Bond e238ec079f Merged PR 2206: split out actions to services and clients
Related work items: #22260
2026-03-25 12:16:40 +00:00
robbond 70d2ee175d test(phase22): add auth redirect safety and i18n route parity checks 2026-03-25 12:15:06 +00:00
robbond 10f221af38 test(portal): add phase22 behavioural closure and route harmonization 2026-03-25 12:04:11 +00:00
robbond 6b66ad6d8f test(phase22): add case service behavioural coverage 2026-03-25 11:58:58 +00:00
robbond 05f26ff12c refactor(case): normalize route composition with fileRouteBuilder 2026-03-25 11:49:19 +00:00
robbond ae64ee6c2b refactor(portal): normalize route composition with fileRouteBuilder 2026-03-25 11:36:59 +00:00
robbond 65003bb08c refactor(document): extract file route builder and normalize query composition 2026-03-25 11:31:39 +00:00
robbond ad855fec88 refactor(document): complete signed post flows via fileClient 2026-03-25 11:14:57 +00:00
robbond 391040b5ac test(phase7): lock downloadBlob file-client delegation 2026-03-25 11:08:52 +00:00
robbond e4bd8ab9b6 test(harness): add fileClient default helper injections 2026-03-25 11:04:57 +00:00
robbond a66d55af80 refactor(actions): extract fileClient with phase22 coverage 2026-03-25 10:59:20 +00:00
robbond 93eb19d028 test(phase22): add client utility behaviour coverage 2026-03-25 10:49:37 +00:00
robbond 64201a6591 test(phase22): add aggregate phase runner entrypoint 2026-03-25 10:43:38 +00:00
robbond 7553f992b7 test(phase22): align runner export/guard parity for core token suite 2026-03-25 10:34:34 +00:00
robbond d7874686a1 test(core): add bounded regression coverage for token request client 2026-03-25 10:30:39 +00:00
robbond 342c7ed457 refactor(core): continue bounded risk-reduction for token client extraction 2026-03-25 10:24:42 +00:00
robbond a715e5fac7 test(harness): continue bounded risk-reduction with shared service harness 2026-03-25 10:13:01 +00:00
robbond 7d2f8a2c27 test(phase6): align behavioural harness with client wrappers 2026-03-25 10:07:25 +00:00
robbond 480897bdc0 test(phase7): align service behaviour harness with client wrappers 2026-03-25 10:04:16 +00:00
robbond 5309ffb8cf refactor(actions): fix document download helper contract 2026-03-25 09:57:26 +00:00
robbond 4a49bbca09 refactor(actions): reduce remaining axios usage in document service 2026-03-25 09:52:27 +00:00
robbond ba7d94d83d refactor(actions): include residual axios-risk slices in branch 2026-03-25 09:43:40 +00:00
robbond 94e2ed5831 refactor(actions): adopt endpoint client in portal config helpers 2026-03-25 08:51:39 +00:00
robbond 2f95636c4a refactor(actions): remove remaining axios usage in case service 2026-03-25 08:03:02 +00:00
robbond 9d67079fb0 refactor(actions): adopt endpoint client in case service posts 2026-03-25 08:00:14 +00:00
robbond ad3b18ac27 refactor(actions): complete case read migration to endpoint client 2026-03-25 07:52:51 +00:00
robbond 353378ee45 refactor(actions): expand endpoint client adoption in case reads 2026-03-25 07:45:51 +00:00
robbond c1f6cc9832 refactor(actions): adopt endpoint client in case service reads 2026-03-25 07:36:53 +00:00
robbond f4471c5702 refactor(actions): adopt endpoint client in remaining document reads 2026-03-25 06:55:30 +00:00
robbond 13dbb8b2d0 refactor(actions): adopt endpoint client in document service posts 2026-03-25 06:48:54 +00:00
robbond 17be69323e refactor(actions): adopt endpoint client in document service reads 2026-03-25 06:43:51 +00:00
robbond 6e6a104dd9 refactor(actions): adopt endpoint client in portal involvement helpers 2026-03-25 06:30:35 +00:00
robbond f596ad4e98 refactor(actions): adopt endpoint client in portal service reads 2026-03-25 06:26:45 +00:00
robbond a74f59394e refactor(actions): adopt endpoint client in account direct service 2026-03-25 06:23:12 +00:00
robbond eb3b6013b3 refactor(actions): adopt endpoint client in reference data service 2026-03-25 06:19:37 +00:00
robbond 6fa7cf9375 refactor(actions): extract shared relay client for direct services 2026-03-25 06:14:23 +00:00
Robert Bond 5e51c2eb97 Merged PR 2205: addin timeouts and retries for stability
Related work items: #22242
2026-03-24 18:19:01 +00:00
robbond fcfb7cf75e refactor(relay): extract shared relay policy presets across endpoints 2026-03-24 18:07:04 +00:00
robbond 318f913397 feat(relay): apply policy overrides for my-portal case and representation endpoints 2026-03-24 15:52:29 +00:00
robbond 0a5819803e feat(relay): add portal endpoint relayPolicy overrides and contract checks 2026-03-24 15:44:09 +00:00
robbond e0510f9e8f feat(relay): apply endpoint-level relayPolicy overrides across account and search flows 2026-03-24 15:36:21 +00:00
robbond bb190946ae feat(relay): add policy overrides and idempotency-aware retry gating 2026-03-24 13:03:49 +00:00
robbond 1ba0cbe8a9 feat(relay): enrich request lifecycle telemetry for retries and outcomes 2026-03-24 12:54:07 +00:00
Robert Bond 0faf79945f Merged PR 2204: update hardening docs
Related work items: #22239
2026-03-24 12:45:53 +00:00
robbond 7903944223 docs(runbook): add relay PR evidence template and summary draft 2026-03-24 12:41:30 +00:00
robbond ad0d2bf285 docs(governance): track context docs and add relay rollout controls 2026-03-24 12:26:32 +00:00
Robert Bond 2ac2f51fc3 Merged PR 2203: Api hardening
Related work items: #22236
2026-03-24 12:06:02 +00:00
robbond f12a719a1e docs(memory-bank): record relay hardening policy baseline and operating knobs 2026-03-24 11:55:24 +00:00
robbond 4e390491c0 refactor(api): tighten relay retry policy and config safety bounds 2026-03-24 11:54:42 +00:00
robbond fad99c93bc refactor(api): harden relay forwarding with timeout, retries and redacted logs 2026-03-24 11:43:40 +00:00
Robert Bond 1b7d9da32c Merged PR 2202: relay GET completion + notify logging fix
Related work items: #22229
2026-03-24 11:32:13 +00:00
robbond 51e255f70b fix(email): log notify send attempts without error framing 2026-03-24 10:35:09 +00:00
robbond da29e789c0 refactor(api): migrate batch 12 remaining relay-get candidates 2026-03-24 10:27:27 +00:00
robbond bf1ba9bc1e refactor(api): migrate batch 11 advancedsearchpaged to relayGet 2026-03-24 10:15:25 +00:00
robbond 0f4b8005ba refactor(api): migrate batch 10 endpoints to relayGet 2026-03-24 10:08:51 +00:00
robbond e8d4e8563e refactor(api): migrate batch 9 endpoints to relayGet 2026-03-24 09:59:59 +00:00
robbond 13be069b8b refactor(api): migrate batch 8 endpoints to relayGet 2026-03-24 09:51:13 +00:00
robbond bd15fd18e1 refactor(api): migrate batch 7 endpoints to relayGet 2026-03-24 09:43:17 +00:00
robbond 6da6fdb4d2 refactor(api): migrate batch 6 endpoints to relayGet 2026-03-24 09:28:01 +00:00
robbond 5797443edc refactor(api): migrate batch 5 endpoints to relayGet 2026-03-24 09:27:06 +00:00
robbond ac0253daeb refactor(api): migrate batch 4 endpoints to relayGet 2026-03-24 09:26:37 +00:00
robbond 44b32a618a TASK22229: P2-S2 batch 3 migrate case/event relay GET endpoints 2026-03-24 07:49:28 +00:00
robbond fdf95dd0b1 TASK22229: P2-S2 batch 2 migrate portal/representation relay GET endpoints 2026-03-24 07:45:59 +00:00
robbond 6edbc431f8 TASK22229: P2-S2 batch 1 migrate account/login relay GET endpoints 2026-03-24 06:58:50 +00:00
robbond 312f7415f5 TASK22229: pilot shared relay forwarding utility 2026-03-24 06:40:32 +00:00
Robert Bond dea127912f Merged PR 2201: update file endpoints
Related work items: #22224
2026-03-24 06:30:29 +00:00
robbond 7c0dc30b1a TASK22224: nextauth notify try-catch micro-refactor 2026-03-24 06:18:35 +00:00
robbond fe394f3fa8 TASK22224: complete documents + endpoint hygiene slices 2026-03-24 06:12:04 +00:00
robbond dfdfd3653c TASK22224: prune dead code in pdf handlers 2026-03-24 05:58:31 +00:00
robbond c6205bbb39 TASK22224: tidy pdf render compatibility 2026-03-24 05:52:38 +00:00
robbond 9b0365ffbd TASK22224: harden createappealcompletemessage parity 2026-03-24 05:49:58 +00:00
robbond cc4fb36439 TASK22224: modernize generatepdf handlers 2026-03-23 18:39:44 +00:00
robbond 411190e403 TASK22224: aggressive non-file parity bundle 2026-03-23 18:32:49 +00:00
robbond ba9beda65e TASK22224: modernize involvement and updatecase handlers 2026-03-23 18:23:54 +00:00
robbond 3f2b945172 TASK22224: harden proxy file routes and expand contracts 2026-03-23 18:14:22 +00:00
robbond 91a0ab82ea TASK22224: big parity bundle for completion message routes 2026-03-23 18:09:36 +00:00
robbond 9f5ec6f50a TASK22224: larger parity bundle for reps and upload handlers 2026-03-23 18:03:59 +00:00
robbond 3f5006a744 TASK22224: harden setupcontainer hash compatibility parity 2026-03-23 17:59:31 +00:00
robbond 6e02ffe26d TASK22224: harden deleteawaitingsubmission route parity 2026-03-23 17:56:14 +00:00
robbond 5c29a82842 fix ui issue on awaiting submission card 2026-03-23 17:53:19 +00:00
robbond ab50430493 TASK22224: harden getbloblist and getprogressobjblob parity 2026-03-23 17:45:47 +00:00
robbond c84287be3c TASK22224: harden awaiting-submission blob retrieval contract 2026-03-23 17:42:25 +00:00
robbond f484f87694 TASK22224: harden getrepsblob against stale tag entries 2026-03-23 17:37:03 +00:00
robbond 8e0131d1ce TASK22224: align delete file route hash guards and contracts 2026-03-23 17:31:25 +00:00
robbond bc483c5097 TASK22224: document downloadblob hotfix closure and next file-guard slice 2026-03-23 17:23:55 +00:00
robbond bd3bf6834b TASK22224: accept encoded hash variants for downloadblob 2026-03-23 17:04:30 +00:00
robbond f09f3b75cc TASK22224: fix downloadblob path normalization for prefixed blob names 2026-03-23 17:01:03 +00:00
robbond f8fba6a561 TASK22224: harden file/static endpoint contracts and phase21 coverage 2026-03-23 16:40:39 +00:00
Robert Bond 78cac2cbb9 Merged PR 2198: update apis
Related work items: #22211
2026-03-23 12:38:30 +00:00
robbond 51eb9b5a4f TASK22211: version memory-bank and backfill change-log stream 2026-03-23 12:32:44 +00:00
robbond eec59e8e94 TASK22211: normalize token endpoint contract handling 2026-03-23 12:24:36 +00:00
robbond 8b6ed73508 TASK22211: normalize new-appeal appeal types endpoint contract 2026-03-23 12:19:43 +00:00
robbond 134f99c04c TASK22211: normalize address search endpoint contract 2026-03-23 12:16:31 +00:00
robbond 722ef9846a TASK22211: normalize hash and metadata endpoint contracts 2026-03-23 12:14:17 +00:00
robbond cb69bbe732 TASK22211: normalize case update and crm task endpoint contracts 2026-03-23 12:11:16 +00:00
robbond 88e4586541 TASK22211: normalize advanced search paged endpoint contract 2026-03-23 12:07:40 +00:00
robbond 98e159d523 TASK22211: normalize case creation and media endpoint contracts 2026-03-23 11:55:23 +00:00
robbond e0e91c843b TASK22211: normalize dns and representation endpoint contracts 2026-03-23 11:52:14 +00:00
robbond bcf03a65f9 TASK22211: normalize form and publication endpoint contracts 2026-03-23 11:47:18 +00:00
robbond b59f13a45d TASK22211: normalize metadata and linked-case endpoint contracts 2026-03-23 11:42:03 +00:00
robbond 2959c7d7a4 TASK22211: normalize delete and watched-case endpoint contracts 2026-03-23 11:38:36 +00:00
robbond 4601d7c15f TASK22211: normalize case detail endpoint contracts 2026-03-23 11:34:01 +00:00
robbond b5a3a62c4b TASK22211: normalize portal module and lpa case endpoint contracts 2026-03-23 11:30:14 +00:00
robbond a106dea6c9 TASK22211: normalize dns basic search endpoint contracts 2026-03-23 11:26:56 +00:00
robbond b880364e51 TASK22211: normalize basic search endpoint contracts 2026-03-23 11:23:07 +00:00
robbond 9af541ac6b TASK22211: normalize myportal retrieval endpoint contracts 2026-03-23 11:12:19 +00:00
robbond b57f3de06f TASK22211: standardize search-document endpoint contracts and phase21 coverage 2026-03-23 11:07:52 +00:00
Robert Bond 2caae7b458 Merged PR 2197: update endpoint api slice
Related work items: #22168
2026-03-23 10:45:31 +00:00
robbond a67f84a99f update error and response handling 2026-03-23 10:28:19 +00:00
robbond 26b889d182 TASK22168 bundle C endpoint login/account consistency extension 2026-03-19 12:27:17 +00:00
robbond f9c9687cfe TASK22168 endpoint account contract consistency slice 2026-03-19 12:11:52 +00:00
Robert Bond 54820324f5 Merged PR 2188: mocku pand welsh login flow
Related work items: #22079, #22081
2026-03-18 14:12:43 +00:00
robbond 20b07ec3d5 22079 mockup 2026-03-18 14:11:37 +00:00
robbond e3bb321eae 22081 welsh signin flow 2026-03-18 14:10:54 +00:00
Robert Bond 860291e1e6 Merged PR 2185: address parts now do and and not fail if more than one is added
address parts now do and and not fail if more than one is added

Related work items: #22093
2026-03-18 10:41:36 +00:00
robbond 0d23e47bea address parts now do and and not fail if more than one is added 2026-03-18 10:41:02 +00:00
Robert Bond 22b70f9081 Merged PR 2184: update apis with helpers
Related work items: #22109
2026-03-17 16:39:31 +00:00
robbond e9cf1cf634 TASK22109: email mini-slice tidy and notify negative-path tests 2026-03-17 16:04:50 +00:00
robbond d17f3a11d2 TASK22109: expand email contract coverage and logging hygiene 2026-03-17 14:28:22 +00:00
robbond 6610e5c7f3 TASK22109: email api contract cleanup and phase21 coverage 2026-03-17 14:23:42 +00:00
Robert Bond 864da9fe23 Merged PR 2183: stanadardised response contract test
Related work items: #22104
2026-03-17 14:11:54 +00:00
robbond 16a5493109 TASK22104: slice4 split phase21 helper and handler contract tests 2026-03-17 14:06:57 +00:00
robbond 0e99d9548b TASK22104: slice3 add failure-path api contract tests 2026-03-17 14:00:09 +00:00
robbond 991cbf73ba TASK22104: slice2 extend api contract success and download tests 2026-03-17 13:57:48 +00:00
robbond 25ba88ac34 TASK22104: slice1 add api response contract tests 2026-03-17 13:49:05 +00:00
Robert Bond f6c5e7183c Merged PR 2182: update apis with standardised response helpers
Related work items: #22102
2026-03-17 13:38:06 +00:00
robbond 91890f4d73 TASK22102: slice7 complex file handler contract closeout 2026-03-17 13:25:51 +00:00
robbond 98184b65df TASK22102: slice6 blob crud and retrieval contract cleanup 2026-03-17 13:23:01 +00:00
robbond 3048df3cd9 TASK22102: slice5 larger file handler contract cleanup 2026-03-17 13:16:48 +00:00
robbond e68319f16c TASK22102: slice4 larger file set + production detail suppression 2026-03-17 13:06:28 +00:00
robbond 3cbd876b77 TASK22102: slice3 migrate file proxy handlers 2026-03-17 12:58:20 +00:00
robbond f16161bcee TASK22102: slice2 migrate remaining email handlers 2026-03-17 12:55:05 +00:00
robbond 9ae8ce7136 TASK22102: slice1 api contract helper + email/admin pilot 2026-03-17 12:50:54 +00:00
Robert Bond 7d06bc7bfa Merged PR 2181: updated query and display logic for post determination fields
updated query and display logic for post determination fields

Related work items: #21710
2026-03-17 12:01:30 +00:00
robbond 8ff1af00b7 updated query and display logic for post determination fields 2026-03-17 12:00:30 +00:00
Robert Bond bda0af9e5c Merged PR 2178: removed app if not an app in capacity
removed app if not an app in capacity

Related work items: #22095
2026-03-17 11:16:42 +00:00
robbond 8ecde91bd9 removed app if not an app in capacity 2026-03-17 11:15:54 +00:00
Robert Bond 51f89320de Merged PR 2177: languge check for relevant template language
languge check for relevant template language

Related work items: #21979
2026-03-17 10:29:58 +00:00
robbond c64c47ca1f languge check for relevant template language 2026-03-17 10:29:00 +00:00
Robert Bond fcd51b41bd Merged PR 2176: update formatting
update formatting

Related work items: #22012
2026-03-17 09:24:41 +00:00
robbond 605932a640 removed commented out call for testing 2026-03-17 09:24:20 +00:00
robbond f1775c2a96 update formatting 2026-03-17 09:22:31 +00:00
Robert Bond 1e690608fb Merged PR 2172: applied downloadable checkbox on submit of new appeal
applied downloadable checkbox on submit of new appeal

Related work items: #22062
2026-03-16 13:58:54 +00:00
robbond 7c84344d5d applied downloadable checkbox on submit of new appeal 2026-03-16 13:58:02 +00:00
Robert Bond 72eafd0a8e Merged PR 2170: updated copy on representation complete submit
updated copy on representation complete submit

Related work items: #21095, #21979
2026-03-16 12:08:49 +00:00
robbond 071fcc0dab updated copy on representation complete submit 2026-03-16 12:07:39 +00:00
Robert Bond e9b2753fd9 Merged PR 2169: Api hardening
Related work items: #22057
2026-03-16 11:17:53 +00:00
robbond 18dfc60754 moved hashapipath to reusable single helper 2026-03-16 11:13:41 +00:00
robbond 023a8e9e6d api hardening 2026-03-16 10:46:05 +00:00
robbond 598e4a6077 TASK22057: phase19 endpoint consistency hardening slice 2026-03-14 07:06:57 +00:00
robbond fffe1d19dd TASK22057: phase18 additional consolidation slices 2026-03-14 06:52:17 +00:00
robbond daa38d6459 TASK22057: phase17 closeout hash/input consistency hardening 2026-03-13 18:43:42 +00:00
Robert Bond 36d35f9549 Merged PR 2168: update client side api calls with hardened hash
Related work items: #22028
2026-03-13 17:59:32 +00:00
robbond c2e14cab31 TASK22028: update memory-bank for myportal and i18n follow-up 2026-03-13 17:57:25 +00:00
robbond d32d7d016e TASK22028: fix myportal deleteRepItem caseRef and i18n namespace 2026-03-13 17:44:27 +00:00
robbond 6283786aeb TASK22028: expand signer coverage for delete flows 2026-03-13 16:40:51 +00:00
robbond e404a6d6db TASK22028: document signer allow-list follow-up 2026-03-13 16:31:21 +00:00
robbond 49dd99016a TASK22028: allow signer hash for deleteblobcase browser flow 2026-03-13 16:30:23 +00:00
robbond 6a9a257ae8 TASK22028: phase 16 memory-bank updates 2026-03-13 16:24:55 +00:00
robbond c196edb457 TASK22028: phase 16 file-proxy guard consistency slice 2026-03-13 16:23:34 +00:00
robbond 1a95cf1098 TASK22028: phase 15 hash guard consistency hardening slice 2026-03-13 16:04:31 +00:00
Robert Bond 0ca6cd94cc Merged PR 2167: APi hardening
Related work items: #22019
2026-03-13 14:24:39 +00:00
robbond 43204dfb54 Update memory-bank with phase 14 follow-up hardening closeout 2026-03-13 14:21:12 +00:00
robbond b8fa514e3b Align getrepsblob guard with route contract and fix quill utils import 2026-03-13 14:12:36 +00:00
robbond 6094874851 Route portal login hashing through authenticated signer 2026-03-13 14:01:18 +00:00
robbond 82f891ada8 Fix client hash signing via authenticated endpoint 2026-03-13 13:49:28 +00:00
robbond 846cba1645 TASK22019: phase 13 harden delete and involvement guards 2026-03-13 13:02:49 +00:00
robbond 0ca63d0228 TASK22019: phase 12 harden download and blob proxy guards 2026-03-13 12:49:54 +00:00
robbond 36260bb405 TASK22019: phase 11 harden blob/delete hash guards and negative paths 2026-03-13 12:32:37 +00:00
robbond 368ef86b74 TASK22019: phase 10 harden file retrieval hash guards and negative paths 2026-03-13 12:26:06 +00:00
robbond c73313b0fa TASK22019: phase 9 harden file handlers hash guards and negative paths 2026-03-13 12:17:51 +00:00
Robert Bond 51269db849 Merged PR 2166: hardening for sensitive hash-guarded APIs
TASK22017: phase 8 hardening for sensitive hash-guarded APIs

Related work items: #22017
2026-03-13 11:43:17 +00:00
robbond 52202f4e1c TASK22017: phase 8 hardening for sensitive hash-guarded APIs 2026-03-13 11:38:14 +00:00
Robert Bond f28b25cc24 Merged PR 2165: TASK21988a: Phase 7 search SSR fix and service behaviour expansion
TASK21988a: Phase 7 search SSR fix and service behaviour expansion

Related work items: #21998
2026-03-13 11:01:10 +00:00
robbond e39c9203e3 TASK21988a: Phase 7 search SSR fix and service behaviour expansion 2026-03-13 10:58:26 +00:00
Robert Bond 3abebc8093 Merged PR 2164: test(actions): phase 6 add parity and behavioural service hardening checks
test(actions): phase 6 add parity and behavioural service hardening checks

Related work items: #21998
2026-03-13 10:16:46 +00:00
robbond b5ae6c271f test(actions): phase 6 add parity and behavioural service hardening checks 2026-03-13 10:11:33 +00:00
Robert Bond a35f2990c6 Merged PR 2163: split actions to individual services
Related work items: #21997
2026-03-13 07:42:22 +00:00
robbond 0c4a50a9e1 refactor(actions): remove legacy actions service after phase 5 split 2026-03-13 06:47:00 +00:00
robbond 13e7e0c1a4 refactor(actions): phase 5 extract remaining direct service modules 2026-03-13 06:44:10 +00:00
robbond cd13453121 refactor(actions): phase 5 extract account and case direct services 2026-03-13 06:41:47 +00:00
robbond 9a24ba1774 refactor(actions): phase 5 extract portal direct service 2026-03-13 06:37:24 +00:00
robbond 906be69648 refactor(actions): phase 5 extract document direct service 2026-03-13 06:32:29 +00:00
robbond 344b6efed1 chore(lint): unblock next lint and fix current eslint errors 2026-03-13 06:24:38 +00:00
robbond 903118e47d refactor(actions): phase 5 extract search and reference direct services 2026-03-13 06:15:32 +00:00
Robert Bond 1d1baec43b Merged PR 2162: refactor actions
Related work items: #21996
2026-03-13 05:51:45 +00:00
robbond bef8f2590b chore(refactor): remove stale legacy actions import comments 2026-03-12 17:41:36 +00:00
robbond fa985351ff refactor(actions): migrate endpoint and file proxies to focused imports 2026-03-12 17:39:50 +00:00
robbond f0202bbd02 refactor(actions): migrate viewall/search/unsubscribe to focused imports 2026-03-12 17:34:03 +00:00
robbond c4ad9ab7a2 refactor(actions): complete priority-1 consumer import migration pass 2026-03-12 13:26:48 +00:00
robbond a0a633a9ea refactor phase 3a components chunk 1 targeted actions imports 2026-03-12 12:13:29 +00:00
robbond be59237ec9 refactor phase 3a api-file chunk 3 targeted actions imports 2026-03-12 11:46:06 +00:00
robbond 61913f9f80 refactor phase 3a api-file chunk 2 targeted actions imports 2026-03-12 11:41:40 +00:00
robbond 5822930f0a refactor phase 3a api-file chunk 1 targeted actions imports 2026-03-12 11:38:18 +00:00
robbond 2a3c7eac34 refactor phase 3a api-endpoint chunk 5a targeted actions imports 2026-03-12 11:31:38 +00:00
robbond 86f41f81e9 refactor phase 3a api-endpoint chunk 5 targeted actions imports 2026-03-12 11:21:40 +00:00
robbond 79141ac6a9 refactor phase 3a api-endpoint chunk 4b targeted actions imports 2026-03-12 11:09:52 +00:00
robbond 36e41e064d refactor phase 3a api-endpoint chunk 4a targeted actions imports 2026-03-12 11:07:25 +00:00
robbond d91ceda196 refactor phase 3a api-endpoint chunk 4 targeted actions imports 2026-03-12 10:50:16 +00:00
robbond 8b8742753e refactor phase 3a api-endpoint chunk 3 targeted actions imports 2026-03-12 10:31:28 +00:00
robbond 8141cb2394 refactor phase 3a api-endpoint chunk 2 targeted actions imports 2026-03-12 10:15:29 +00:00
robbond e7882c9f78 refactor phase 3a api-endpoint chunk 1 targeted actions imports 2026-03-12 10:08:13 +00:00
robbond 7bd5c7d833 refactor phase 3a api admin auth documents email targeted actions imports 2026-03-12 09:58:39 +00:00
robbond cf99d8e466 refactor phase 3a pages-dns targeted actions imports 2026-03-12 09:53:17 +00:00
robbond 1a8a28fe3a refactor phase 3a top-level pages dbcheck targeted actions import 2026-03-12 09:51:13 +00:00
robbond c33cdf1b28 refactor phase 3a top-level pages targeted actions imports 2026-03-12 09:49:40 +00:00
robbond eebcfffd5e refactor phase 3a pages-newappeal-and-case targeted actions imports 2026-03-12 09:43:47 +00:00
robbond 0da4a4b3b4 refactor phase 3a pages-admin targeted actions imports 2026-03-12 09:35:25 +00:00
robbond 570bbb2fa1 refactor phase 3a pages-auth targeted actions imports 2026-03-12 09:34:50 +00:00
robbond 3550faf051 refactor phase 3a pages-account targeted actions imports 2026-03-12 09:34:16 +00:00
robbond 72e4655188 refactor phase 3a myportal representation and case imports 2026-03-12 09:26:34 +00:00
robbond dacf2511f5 refactor phase 3a myportal index targeted actions imports 2026-03-12 09:25:56 +00:00
robbond 5c54220bf8 refactor phase 3a myportal case-by-incident imports 2026-03-12 09:24:10 +00:00
robbond 548c85e339 refactor phase 3a myportal dns imports 2026-03-12 09:22:17 +00:00
robbond 562cffdc59 refactor phase 3a myportal advanced and address search imports 2026-03-12 09:20:33 +00:00
robbond cbcb9549be refactor phase 3a myportal search results imports 2026-03-12 09:20:01 +00:00
robbond 694eab8297 refactor phase 3a myportal contactus targeted actions imports 2026-03-12 09:18:28 +00:00
robbond a0ae6fcba3 refactor phase 4 hardening helpers and redacted logging 2026-03-12 09:03:24 +00:00
robbond aeec5e0f59 refactor phase 3 targeted actions imports 2026-03-12 07:39:06 +00:00
robbond 2950ce788a refactor actions into core modules and grouped services 2026-03-12 07:30:40 +00:00
Robert Bond 0bd6e12877 Merged PR 2159: adjusted logic to cater for viewl all path and dnsapplications
adjusted logic to cater for viewl all path and dnsapplications

Related work items: #21942, #21943, #21971
2026-03-11 10:42:44 +00:00
robbond f947821afa adjusted logic to cater for viewl all path and dnsapplications 2026-03-11 10:40:48 +00:00
Robert Bond 9c483ec655 Merged PR 2158: fixed breacrumb and label
fixed breacrumb and label

Related work items: #18688, #21942, #21943
2026-03-10 18:06:46 +00:00
robbond df311e131a fixed breacrumb and label 2026-03-10 18:02:37 +00:00
Robert Bond 93d02697a2 Merged PR 2157: correctly go back to the previous page search results
correctly go back to the previous page search results

Related work items: #18688, #21942, #21943
2026-03-10 10:33:13 +00:00
robbond 3e48f8cd28 correctly go back to the previous page search results 2026-03-10 10:31:16 +00:00
Robert Bond e6c25d7ef5 Merged PR 2154: notify check for new case ref tempate and pref lang
notify check for new case ref tempate and pref lang

Related work items: #21722, #21726
2026-03-09 14:11:29 +00:00
robbond 6e3f7eeec1 notify check for new case ref tempate and pref lang 2026-03-09 14:10:30 +00:00
Robert Bond de5ee86cde Merged PR 2153: fix for pdf formatting and hyperlinks
fix for pdf formatting and hyperlinks

Related work items: #21897, #21898
2026-03-09 13:13:04 +00:00
robbond 96e180040d fix for pdf formatting and hyperlinks 2026-03-09 13:11:28 +00:00
Robert Bond 95c1492fe0 Merged PR 2152: check box to download questionnaire
chekc box to download questionnaire

Related work items: #17556
2026-03-09 12:14:14 +00:00
robbond 2c6d110d9b chekc box to download questionnaire 2026-03-09 12:13:24 +00:00
Robert Bond e2c79d6b0f Merged PR 2134: regex update to include apostrophe
regex update to include apostrophe

Related work items: #21099
2026-02-27 09:42:32 +00:00
robbond 0e9a4da4be regex update to include apostrophe 2026-02-27 09:41:40 +00:00
Robert Bond db184984ca Merged PR 2133: removed the help section on registration
removed the help section on registration

Related work items: #21093
2026-02-27 09:04:54 +00:00
robbond 610056849d removed the help section on registration 2026-02-27 09:04:01 +00:00
Robert Bond 92eb0c5a15 Merged PR 2132: included emdash in doc upload validation
included emdash in doc upload validation

Related work items: #21099
2026-02-26 13:05:10 +00:00
robbond cfe732f299 included emdash in doc upload validation 2026-02-26 13:04:28 +00:00
Robert Bond b39ed0fd8c Merged PR 2123: updated to allow new appeals to work post ph merge
updated to allow new appeals to work post ph merge

Related work items: #17555
2026-02-23 12:47:20 +00:00
robbond 21eceff8df updated to allow new appeals to work post ph merge 2026-02-23 12:46:15 +00:00
Robert Bond 43c692a959 Merged PR 2121: missing case ref failing sip reps pdf
missing case ref failing sip reps pdf

Related work items: #21254
2026-02-20 16:17:22 +00:00
robbond ab847a62b3 missing case ref failing sip reps pdf 2026-02-20 16:16:34 +00:00
Robert Bond 292a280f50 Merged PR 2120: update to cater for tickenumber not title
had been done on ph2 needed updating for sips

Related work items: #21254
2026-02-20 12:00:14 +00:00
robbond 7da20386dd config 2026-02-20 11:59:09 +00:00
robbond 5e46495e79 reference ticket number not title, caters for long titles 2026-02-20 11:58:35 +00:00
Robert Bond c438a62553 Merged PR 2115: reinstate sips rep button logic
reinstate sips rep button logic

Related work items: #21254
2026-02-20 07:44:14 +00:00
robbond 5013b967f3 reinstate sips rep button logic 2026-02-20 07:40:27 +00:00
Robert Bond 85b0a7e4c4 Merged PR 2112: pass mediaobj to case pages
Related work items: #21254
2026-02-19 14:14:42 +00:00
robbond 5cd3744e52 passing mediaobj to dns case and myportal dns case 2026-02-19 14:12:55 +00:00
robbond 9d355f6f77 passing media obj in myportal search 2026-02-19 13:58:58 +00:00
Robert Bond 38a948e9a0 Merged PR 2070: media tab and player
Related work items: #21254, #21255, #21256
2026-02-04 11:23:21 +00:00
robbond 53540911cc 21254 youtube media player 2026-02-04 11:22:11 +00:00
robbond ec067e62a5 21254 media tab 2026-02-04 11:21:05 +00:00
robbond 3bd1e7e7fe 21256 mocked api response 2026-02-04 11:20:29 +00:00
robbond dc8958f7a1 updated service status down functionality 2026-02-04 11:19:35 +00:00
Robert Bond 5c6ee2fedd Merged PR 2064: update language delimeter to >>>
Related work items: #21187
2026-01-27 12:01:12 +00:00
robbond 1b586da1ae 21187 implement helper function on summary 2026-01-27 11:58:40 +00:00
robbond 49374640e5 21187 add bilingual helper function 2026-01-27 11:57:44 +00:00
Robert Bond 3569bce8d1 Merged PR 2060: added in link and label in english and welsh
added in link and label in english and welsh

Related work items: #21091
2026-01-22 12:19:28 +00:00
robbond 6b7403f6c8 added in link and label in english and welsh 2026-01-22 12:18:03 +00:00
Robert Bond a65ed04ad7 Merged PR 2055: 21099 api serverside validation check
21099 api serverside validation check

Related work items: #21099
2026-01-21 13:18:35 +00:00
robbond dd12dc99da 21099 api serverside validation check 2026-01-21 13:17:03 +00:00
Robert Bond e0392b108c Merged PR 2051: Update filename validation
Related work items: #21099, #21154
2026-01-20 15:22:40 +00:00
robbond e6b0b0763a 21099 update filename validation with # 2026-01-20 15:20:54 +00:00
robbond 737b74539d 21154 updated language labels 2026-01-20 15:20:01 +00:00
Robert Bond b9434e07a3 Merged PR 2038: 18791 remove further details
Related work items: #21097, #21098
2026-01-19 13:17:43 +00:00
robbond cfe6070d57 21098 update rep pdfs remove further details 2026-01-19 13:16:20 +00:00
robbond 4710d3e420 21097 remove ui for further details 2026-01-19 13:15:50 +00:00
Robert Bond 088c93e2cf Merged PR 2034: updates to new appeal
restricts completion of an appeal or rep if no docapi....stop it failing if already started.

Related work items: #17559
2026-01-15 13:23:55 +00:00
robbond 8a0c82f4e0 update env.local with d365 2026-01-15 13:21:26 +00:00
robbond 83dc4e12e3 refactor appeal for new and resume 2026-01-15 13:21:11 +00:00
robbond 9241fe0b84 updated restriction to disallow partial reps and appeals from being submitted when not docapi 2026-01-15 13:20:07 +00:00
Robert Bond 94861846aa Merged PR 2026: added searching by LPA application ref and criteria highlighting
added searching by LPA application ref and criteria highlighting

Related work items: #18544
2026-01-09 13:38:36 +00:00
robbond 9b0f9acff9 added searching by LPA application ref and criteria highlighting 2026-01-09 13:37:40 +00:00
Robert Bond 062f64d157 Merged PR 2024: Update validation for decision date
Related work items: #17559
2026-01-08 12:52:08 +00:00
robbond dfaa54a6cf updated start date validation to be dependant on value of dateofapplication 2026-01-06 12:47:56 +00:00
Robert Bond 6978114ce0 Merged PR 2017: merge latest ph2 to sips
Related work items: #20553
2025-12-02 14:17:22 +00:00
robbond 3a5497fe76 merged 2025-12-02 14:12:55 +00:00
robbond 5889c4093b Merge branch 'Development' into SIPS-Development 2025-12-02 14:03:48 +00:00
Robert Bond 140d615310 Merged PR 2016: part 2 ph2 update
part 2 ph2 update

Related work items: #20553
2025-11-28 13:30:31 +00:00
robbond 1f933b6d17 part 2 ph2 update 2025-11-28 13:27:52 +00:00
Robert Bond ab1df81745 Merged PR 2009: partial synch with ph2 updates
partial synch with ph2 updates

Related work items: #20553
2025-11-28 08:50:01 +00:00
robbond 3f4bd8d52e partial synch with ph2 updates 2025-11-28 08:45:13 +00:00
Robert Bond ff394882a5 Merged PR 1999: fixed ui misaligned button and wrong text
fixed ui misaligned button and wrong text

Related work items: #20166
2025-11-18 08:04:38 +00:00
robbond 6a7806f5ea fixed ui misaligned button and wrong text 2025-11-18 07:57:09 +00:00
Robert Bond c622cb2f74 Merged PR 1987: docoffline dependant appeals and reps
docoffline dependant appeals and reps

Related work items: #20166
2025-11-10 08:57:48 +00:00
robbond 02a83700ba docoffline dependant appeals and reps 2025-11-10 06:22:31 +00:00
Robert Bond eebe30a232 Merged PR 1957: Fixes for d365
Related work items: #19548, #19887, #19889
2025-10-28 11:26:42 +00:00
robbond e54a0b2253 updated serching to reduce CRM calls for performance and stability 2025-10-28 11:21:38 +00:00
robbond df199cff53 19889 added in curmurl_version variable 2025-10-28 11:20:02 +00:00
robbond d0d2b5d7a9 19845 other party rep dates now showing - added in query 2025-10-28 11:18:10 +00:00
robbond 84aed438f8 19548 remove setcaseinvolvement 2025-10-28 11:17:08 +00:00
Robert Bond 8be971f0b9 Merged PR 1905: Download throttling
Related work items: #19431
2025-10-01 08:28:16 +00:00
robbond 118d8a56c7 welsh language labels 2025-10-01 09:18:31 +01:00
robbond 0b3cb9778b queued downloads and stream not buffer of downloads 2025-09-23 12:45:20 +01:00
robbond cb948a160b updated admin tabs and filtering 2025-09-23 12:44:21 +01:00
Robert Bond c5d3792048 Merged PR 1870: Fix downloading reps links
updated download and delete links to encode with correct hash values

Related work items: #18901
2025-09-11 09:55:16 +00:00
robbond 17042d9836 update copy to remove allowed zip files 2025-09-11 10:51:47 +01:00
robbond e9fc8816c1 url encode values corect for hash and remove .zip files allowed 2025-09-11 10:51:14 +01:00
robbond f8769d0bf1 url encode/decode correctly paths for hashed values 2025-09-11 10:50:45 +01:00
robbond c0dbaa2c3f reps files to use ticket number not title for case ref 2025-09-11 10:49:47 +01:00
Robert Bond dc14e6c9af Merged PR 1869: ph 2 merge to sips
Related work items: #18559
2025-09-10 09:22:28 +00:00
robbond b5b114f455 Merge branch 'Development' into SIPS-Development 2025-09-10 10:18:07 +01:00
Robert Bond 16010f177a Merged PR 1847: doc link fix for unpublished docs and cases
doc link fix for unpublished docs and cases

Related work items: #18559
2025-09-01 08:48:23 +00:00
robbond 7ec709581f doc link fix for unpublished docs and cases 2025-09-01 08:19:22 +01:00
Robert Bond 2c3c3f1cdf Merged PR 1846: Admin tabs
Related work items: #18559
2025-08-29 07:59:12 +00:00
robbond ac17e2058b removed older storage pages 2025-08-29 08:25:01 +01:00
robbond 75bab2af0b admin tabbed pages 2025-08-29 08:24:00 +01:00
Robert Bond 60a9081ead Merged PR 1836: added in navigation property for transport acts
added in navigation property for transport acts

Related work items: #17346
2025-08-21 10:13:04 +00:00
robbond 2d18a5a2b3 added in navigation property for transport acts 2025-08-21 11:10:20 +01:00
Robert Bond bf2679045e Merged PR 1835: merge in PH2 work
Related work items: #17344, #17346, #17392, #17466, #17552, #17592, #17728, #18356
2025-08-20 11:14:26 +00:00
robbond 92ceacb57b Merge branch 'Development' into SIPS-Development 2025-08-20 12:11:55 +01:00
Robert Bond 6f58e7ad63 Merged PR 1832: right mouse click fixes on view all
Related work items: #17346
2025-08-19 11:39:59 +00:00
robbond 9a3201792c removed link for res awiating submissin making it onclick instead 2025-08-19 12:35:51 +01:00
robbond 0b070de9f7 updated breadcrumb when key is used 2025-08-19 12:35:29 +01:00
robbond 7cc9469b2b added in a key to be used when right clicked on link 2025-08-19 12:35:00 +01:00
Robert Bond a4ce38511d Merged PR 1830: updated right click actions
updated right click actions

Related work items: #17346
2025-08-19 08:01:21 +00:00
robbond 95d5164b2f updated right click actions 2025-08-18 18:24:42 +01:00
Robert Bond 6df20eb960 Merged PR 1829: updated text for home page to say DNS on button
updated text for home page to say DNS on button

Related work items: #17346
2025-08-18 12:01:32 +00:00
robbond a9c3a1368f updated text for home page to say DNS on button 2025-08-18 12:57:16 +01:00
Robert Bond 92aee7e23e Merged PR 1825: updated watched case filter on dashboard to not count reps
Related work items: #18356
2025-08-14 11:23:17 +00:00
robbond 5cf5a64ad5 added count for orphaned reps 2025-08-14 12:20:22 +01:00
robbond 1d1adf017a tidied up view all and icon 2025-08-14 12:18:35 +01:00
robbond fcba17c81f updated watch case filter on dashboard 2025-08-14 10:54:59 +01:00
Robert Bond 0e91dfe8a2 Merged PR 1819: updated watched case filter
Related work items: #18356
2025-08-14 07:40:28 +00:00
robbond 38ebd522a8 added count of accounts 2025-08-14 08:31:18 +01:00
robbond b86712efb8 storage admin update 2025-08-14 08:25:20 +01:00
robbond 8e9d89a2af included filter out reps 2025-08-14 08:25:05 +01:00
Robert Bond fabcb3ee5a Merged PR 1818: home page content update and storage admin updates
Related work items: #17346
2025-08-13 14:59:55 +00:00
robbond ec3a591126 updated storage admin 2025-08-13 15:57:18 +01:00
robbond ad2dddc9a4 updated home page copy 2025-08-13 13:45:33 +01:00
Robert Bond 68a34d03ae Merged PR 1815: Ticketnumber on watched cases and storage browser
Related work items: #18356
2025-08-13 09:11:20 +00:00
robbond fc1f63f388 updated weatched case for tickent number 2025-08-13 09:58:39 +01:00
robbond 664d0dd787 storage admin browser 2025-08-13 09:56:50 +01:00
Robert Bond c55242fd90 Merged PR 1813: ticket number not ref/title/pinswg_name
Related work items: #18356
2025-08-12 11:57:18 +00:00
robbond 21bba4a131 use ticketnumber not ref/title/pinswg_name 2025-08-12 12:52:43 +01:00
robbond 82f96493ce storage account report 2025-08-12 12:49:42 +01:00
Robert Bond 2347a104c8 Merged PR 1812: fix build for multiple repsonresults import
fix build for multiple repsonresults import

Related work items: #18356
2025-08-12 09:09:46 +00:00
robbond fbfd21df41 fix build for multiple repsonresults import 2025-08-12 10:05:43 +01:00
Robert Bond f6bfc4b5d0 Merged PR 1811: use ticket number not title/reference/pinswg_name
Related work items: #18356
2025-08-12 08:42:11 +00:00
robbond 8d8ae89ae6 show reps on results for view all 2025-08-12 07:20:43 +01:00
robbond 6fa2418b19 use ticketnumber not ref/title/pinswg_name 2025-08-12 07:20:17 +01:00
robbond b808a34b14 pass showlogincheck prop 2025-08-12 07:18:51 +01:00
robbond 8a2ee508c0 use ticket not title/ref/pinswg_name 2025-08-12 07:18:36 +01:00
robbond 5fb944e90f pass showlogincheck prop to display correctly 2025-08-12 07:17:30 +01:00
robbond 5a4670dda3 update query to get ticketnumber using navigationproproperty 2025-08-12 07:16:48 +01:00
robbond 75436837a2 pass ticketnumber as prop 2025-08-12 07:15:31 +01:00
robbond 58b5c71a64 update tags with ticketnumber not title/ref 2025-08-12 07:14:43 +01:00
Robert Bond f9524638a7 Merged PR 1793: And reps open/available on results lists
Related work items: #17728
2025-08-11 08:32:35 +00:00
robbond f696b20671 Merge branch '17728-show-reps-open-on-dnsapplications-list-page' of https://welshgovernment.visualstudio.com/PEDW%20-%20Planning%20Casework/_git/FrontEnd into 17728-show-reps-open-on-dnsapplications-list-page 2025-08-11 07:49:14 +01:00
robbond 8895d5a328 included ticketnumber 2025-08-11 07:35:49 +01:00
bevanl3 f9831b9aa4 fix missing case info when signed out 2025-08-04 16:15:55 +01:00
robbond 986d5a382f case ref not case title 2025-08-04 06:27:29 +01:00
robbond 9845af7046 updated lang label for myportal/dnsapplications 2025-08-01 07:41:12 +01:00
robbond 14a24616d0 updated language label for repsonresults to dns list 2025-08-01 07:34:35 +01:00
robbond e990feddbc added repsonresults to dns list 2025-08-01 07:34:08 +01:00
Robert Bond 8f1aede2e3 Merged PR 1787: 17346 updated sips in line
17346 updated sips in line

Related work items: #17346
2025-07-29 11:31:24 +00:00
robbond d0b2fd0996 17346 updated sips in line 2025-07-29 12:30:20 +01:00
Robert Bond 9744ab93a4 Merged PR 1778: re ordered home page cards and updated text
re ordered home page cards and updated text

Related work items: #17346
2025-07-25 07:57:08 +00:00
robbond 9971f8be4d re ordered home page cards and updated text 2025-07-25 08:16:16 +01:00
Robert Bond 4562e5f092 Merged PR 1772: show reps on results for dns search
show reps on results for dns search

Related work items: #13922
2025-07-24 08:24:20 +00:00
robbond 3ccb96abc5 show reps on results for dns search 2025-07-24 09:21:26 +01:00
Robert Bond b2e8eae518 Merged PR 1756: added chaining and a blank
added chaining and a blank

Related work items: #17728
2025-07-17 11:26:43 +00:00
robbond f6cd76ed38 added chaining and a blank 2025-07-17 12:21:26 +01:00
Robert Bond ab97d28a1c Merged PR 1744: fix to service title size
fix to  service title size

Related work items: #17592, #17552
2025-07-10 11:17:44 +00:00
robbond 7f2ed69972 fix to service title size 2025-07-09 15:16:51 +01:00
Robert Bond 0d24e3ee12 Merged PR 1738: adjsuted image size
adjsuted image size

Related work items: #17552
2025-07-09 08:39:49 +00:00
robbond 42866765c2 adjsuted image size 2025-07-09 09:20:41 +01:00
Robert Bond a156cc5dda Merged PR 1735: adjsuted size of service banner title svg
adjsuted size of service banner title svg

Related work items: #17552
2025-07-08 15:10:14 +00:00
robbond 9710301b8d adjsuted size of service banner title svg 2025-07-08 16:07:38 +01:00
Robert Bond 79538f5f5c Merged PR 1731: added scrol ltop on complete
added scrol ltop on complete

Related work items: #17344
2025-07-08 08:23:00 +00:00
robbond 8c07850486 added scrol ltop on complete 2025-07-08 09:11:50 +01:00
Robert Bond ab04761677 Merged PR 1730: scroll top for anorid
Related work items: #12956, #17344
2025-07-08 08:09:51 +00:00
robbond cb2ade03b4 update git ignore 2025-07-08 09:08:35 +01:00
robbond 103c548e32 added scrolltop 2025-07-08 09:08:13 +01:00
robbond 28fb2ecbe7 Merge branch 'SIPS-Development' of https://git.rdbdata.com/robbond/pedwFrontend into SIPS-Development 2025-07-08 08:59:21 +01:00
robbond 8713317404 Merged PR 1717: merge dev to sips
Related work items: #12956
2025-07-08 08:58:41 +01:00
robbond 7c1262e4dc Update Jenkinsfile 2025-07-07 15:06:28 +00:00
robbond 777d37f8b4 Add Jenkinsfile 2025-07-07 15:02:39 +00:00
robbond a24d68b8c4 Add Dockerfile 2025-07-07 12:02:02 +00:00
Robert Bond 5fbd07bb1f Merged PR 1726: corrected query for getappealtypes to exclude sips
corrected query for getappealtypes to exclude sips

Related work items: #17466
2025-07-04 13:06:47 +00:00
robbond ac21337d4d corrected query for getappealtypes to exclude sips 2025-07-04 14:04:55 +01:00
Robert Bond 67d6db3b05 Merged PR 1725: doc label and breadcrumb
doc label and breadcrumb

Related work items: #17392
2025-07-04 12:08:49 +00:00
robbond f705cc41c3 doc label and breadcrumb 2025-07-04 13:07:01 +01:00
Robert Bond 8edc905a8c Merged PR 1721: fix typon in jsonpath expression
fix typon in jsonpath expression

Related work items: #17392
2025-07-03 14:58:03 +00:00
robbond 527decda43 fix typon in jsonpath expression 2025-07-03 15:54:36 +01:00
Robert Bond 7742558f17 Merged PR 1720: remove reference to sandbox to see if helps
remove reference to sandbox to see if helps

Related work items: #17392
2025-07-03 13:11:40 +00:00
robbond a1b821521d remove reference to sandbox to see if helps 2025-07-03 14:09:42 +01:00
Robert Bond b736239bf3 Merged PR 1719: adjusted query typo
adjusted query typo

Related work items: #17392
2025-07-03 12:05:58 +00:00
robbond 12a6e15a40 adjusted query typo 2025-07-03 12:44:09 +01:00
Robert Bond dcb67e0de3 Merged PR 1717: merge dev to sips
Related work items: #12956
2025-07-03 10:21:50 +00:00
robbond 39dc5a349a Merge branch 'Development' into SIPS-Development 2025-07-03 11:20:38 +01:00
Robert Bond d59290364d Merged PR 1716: tweak for welsh link to fix build
tweak for welsh link to fix build

Related work items: #17392
2025-07-02 16:32:52 +00:00
robbond 0a25358860 tweak for welsh link to fix build 2025-07-02 17:30:58 +01:00
Robert Bond 9935f65e73 Merged PR 1713: updated welsh path to cas summary include case ref
updated welsh path to cas summary include case ref

Related work items: #17392
2025-07-02 13:16:55 +00:00
robbond 5ecaa58bdf updated welsh path to cas summary include case ref 2025-07-02 14:07:29 +01:00
Robert Bond dbc0345197 Merged PR 1708: fixed query url
fixed query url

Related work items: #17334
2025-06-30 13:03:03 +00:00
robbond 4bf2a129de fixed query url 2025-06-30 12:43:20 +01:00
Robert Bond 75a15948e4 Merged PR 1706: new appeal pdf file location tag
Related work items: #12956
2025-06-27 07:49:29 +00:00
robbond 91dcf6c5c3 rep create fix 2025-06-27 06:40:51 +01:00
robbond e0988de47b createappealpdf function fix 2025-06-27 06:21:47 +01:00
Robert Bond dfd76757ae Merged PR 1704: docuemnt location update
docuemnt location  update

Related work items: #12956
2025-06-26 21:06:01 +00:00
robbond 84fc916c7f docuemnt location update 2025-06-26 21:51:50 +01:00
Robert Bond a5dbd52d73 Merged PR 1699: past sprint defect fixes
Related work items: #16503, #17110
2025-06-26 05:23:16 +00:00
robbond 53f4b8140a 16503 email for consutlation response update 2025-06-26 06:21:30 +01:00
robbond 9e1d091116 removed extra heading of submission 2025-06-26 06:12:30 +01:00
Robert Bond 7e3c79830c Merged PR 1696: link on portal for pdf download
link on portal for pdf download

Related work items: #12925
2025-06-24 09:53:17 +00:00
robbond 4d310fd6c1 link on portal for pdf download 2025-06-24 10:52:06 +01:00
Robert Bond febfb4e532 Merged PR 1688: 17143 removed pref from myportal page rely on link for lang
17143 removed pref from myportal page rely on link for lang

Related work items: #17143
2025-06-18 12:06:32 +00:00
robbond a92b0af2db 17143 removed pref from myportal page rely on link for lang 2025-06-18 12:49:38 +01:00
Robert Bond 806952b98d Merged PR 1686: 17143 adjsut switcher
17143 adjsut switcher

Related work items: #17143
2025-06-18 09:18:21 +00:00
robbond 412f7d2e90 17143 adjsut switcher 2025-06-18 10:17:26 +01:00
Robert Bond 56ad0c4843 Merged PR 1685: 17143 pref lang abck in
17143 pref lang abck in

Related work items: #17143
2025-06-18 08:36:26 +00:00
Robert Bond f39bdee1ee Merged PR 1641: remove swagger docs
remove swagger docs

Related work items: #12956
2025-05-22 16:21:25 +00:00
robbond cc3c70024f remove swagger docs 2025-05-22 17:11:57 +01:00
Robert Bond 2ac542e6c2 Merged PR 1618: updated gitignore,package lock for node 20 pipeline
updated gitignore,package lock for node 20 pipeline

Related work items: #12956
2025-05-13 12:57:21 +00:00
robbond e3546e29a7 updated gitignore,package lock for node 20 pipeline 2025-05-13 13:54:23 +01:00
519 changed files with 63193 additions and 30637 deletions
+176
View File
@@ -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.
+95
View File
@@ -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.
+234
View File
@@ -0,0 +1,234 @@
# Refactor Branch Rules (Active)
## Purpose
This file defines the active working rules for the **refactor branch**.
This branch is focused on **safe, behaviour-preserving refactor of portal journeys**, including:
- New Appeal Flow (completed baseline)
- Representations Flow (active refactor stream)
---
## Core Rule (Highest Priority)
Do not change live behaviour unless explicitly instructed.
Refactor = improve structure, not behaviour.
---
## Primary Objectives
1. Make core portal journeys easier to understand and maintain
2. Reduce risk when making future changes
3. Improve separation of concerns (UI, workflow, data, integrations)
4. Prepare flows for future extensibility (e.g. multiple appeal types, additional journey types)
---
## Active Refactor Streams
### Completed
- New Appeal Flow (S78)
### Active
- Representations Flow
Each stream must follow **slice-based, behaviour-preserving refactor discipline**.
---
## Non-Negotiable Rules
- Preserve all current user journeys:
### Appeals
- start appeal
- save and exit
- resume appeal
- upload documents
- check answers
- submit appeal
- confirmation
### Representations
- start from case summary CTA
- select capacity
- select representation type
- enter content / upload files
- check answers
- submit representation
- completion
---
Do not:
- change payload structures
- change validation rules
- change business logic
- change route/query behaviour
- change navigation or side-effect sequencing
- introduce EN/CY drift
- 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
- One slice = one branch = one commit
---
## Branch Model (Critical)
### Branch Roles
- `SIPS-Development`
- BAU branch
- ongoing feature and bugfix work
- must remain stable
- `refactor`
- integration branch for refactor work
- all refactor slices merge here first
- feature branches (short-lived)
- created from `refactor`
- one per slice
- merged back into `refactor`
---
### Flow
SIPS-Development
refactor
feature/slice-x
refactor
↓ (when stable & validated)
SIPS-Development
---
## Context Usage Rules (Critical for Efficiency)
### Default context (only load these):
- `context/refactor-branch-charter.md`
- `context/architecture.md`
- `context/domain-flows.md`
### Only load when needed:
- appeal work → `newappeal-refactor-guardrails.md`
- representation work → `representations-refactor-guardrails.md`
- plans → `context/plans/*`
### Never load by default:
- all context files
- onboarding docs
- all templates
---
## Templates Usage
- 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
---
## Regression Safety Rule (Critical)
This is a live system. All refactor work must prove behaviour is unchanged.
Before completing any slice:
### Appeals
- start appeal
- save and exit
- resume
- upload
- check answers
- submit
- confirmation
### Representations
- navigate from case summary → make representation
- complete full journey
- submit successfully
- confirmation shown
Also:
- verify EN/CY parity
- verify no navigation or state regressions
If regression cannot be ruled out:
→ do not proceed
---
## When Unsure
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
- is small and safe to merge
- introduces no regression risk
---
## One-Line Rule
> Keep behaviour the same, reduce risk, and make the smallest safe change.
+15
View File
@@ -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
+17
View File
@@ -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
+17
View File
@@ -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
+13
View File
@@ -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
+16
View File
@@ -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
+16
View File
@@ -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
+171 -44
View File
@@ -13,31 +13,38 @@ HASHKEY = 028ffbae928d7191c0017f298260995f901d78eabde1436c0af0423459cc3715832136
# CRMURL = "devcrm2016.llcdt.gov.wales/DVPINS"
# //New Dev oauth ESNR
# CLIENT_ID = 9b834097-03d7-409b-b038-ecfec31a394a
# CLIENT_SECRET = .Jf8Q~tKMObCJjhh_hy3J5AIctFrBalU4FS~tdxk
# RELAY_ROOT = https://ar-ns-lz-pedw-dev-uks-01.servicebus.windows.net/ar-hc-lz-pedw-dev-uks-01/
# RELAYURI = "ar-ns-lz-pedw-dev-uks-01.servicebus.windows.net"
# RELAYPATH = "ar-hc-lz-pedw-dev-uks-01"
# CRMURL = "devcrm2016.llcdt.gov.wales/DVPINS"
CLIENT_ID = 9b834097-03d7-409b-b038-ecfec31a394a
CLIENT_SECRET = .Jf8Q~tKMObCJjhh_hy3J5AIctFrBalU4FS~tdxk
RELAY_ROOT = https://ar-ns-lz-pedw-dev-uks-01.servicebus.windows.net/ar-hc-lz-pedw-dev-uks-01/
RELAYURI = "ar-ns-lz-pedw-dev-uks-01.servicebus.windows.net"
RELAYPATH = "ar-hc-lz-pedw-dev-uks-01"
CRMURL = "dv-pedw.crm11.dynamics.com"
CRMURL_VERSION = "v9.2"
# //New Test
# CLIENT_ID = d40b9373-96c2-4b04-8bd8-c2a2fe6444c1
# CLIENT_SECRET = 6hT8Q~glL~jloWpGmvXw3UY37QFwjUE0r._jobEM
# RELAY_ROOT = https://ar-ns-lz-pedw-test-uks-01.servicebus.windows.net/ar-hc-lz-pedw-test-uks-01/
# RELAYURI = "ar-ns-lz-pedw-test-uks-01.servicebus.windows.net"
# RELAYPATH = "ar-hc-lz-pedw-test-uks-01"
CLIENT_ID = d40b9373-96c2-4b04-8bd8-c2a2fe6444c1
CLIENT_SECRET = 6hT8Q~glL~jloWpGmvXw3UY37QFwjUE0r._jobEM
RELAY_ROOT = https://ar-ns-lz-pedw-test-uks-01.servicebus.windows.net/ar-hc-lz-pedw-test-uks-01/
RELAYURI = "ar-ns-lz-pedw-test-uks-01.servicebus.windows.net"
RELAYPATH = "ar-hc-lz-pedw-test-uks-01"
# CRMURL = "tscrm2016.llcdt.gov.wales/UATPINS"
# CRMURL_VERSION = "v8.2"
CRMURL = "ts-pedw.crm11.dynamics.com"
CRMURL_VERSION = "v9.2"
# //New PreProd
CLIENT_ID = ab6c4678-b31a-4eb3-b429-e039120e488a
CLIENT_SECRET = V288Q~P1lFMvvX9Xnkx2pLixK~GOREH983Owqc1E
RELAY_ROOT = https://ar-ns-lz-pedw-ppe-uks-01.servicebus.windows.net/ar-hc-lz-pedw-ppe-uks-01/
RELAYURI = "ar-ns-lz-pedw-ppe-uks-01.servicebus.windows.net"
RELAYPATH = "ar-hc-lz-pedw-ppe-uks-01"
# RELAY_ROOT = https://ar-ns-lz-pedw-ppe-ukw-01.servicebus.windows.net/ar-hc-lz-pedw-ppe-ukw-01/
# RELAYURI = "ar-ns-lz-pedw-ppe-ukw-01.servicebus.windows.net"
# RELAYPATH = "ar-hc-lz-pedw-ppe-ukw-01"
CRMURL = "ppcrm2016.llcdt.gov.wales/PPPINSWales"
# CLIENT_ID = ab6c4678-b31a-4eb3-b429-e039120e488a
# CLIENT_SECRET = VWY8Q~9EtRzGXpd8Xs~5OS1fej2IyVJ8JgTMncw3
# RELAY_ROOT = https://ar-ns-lz-pedw-ppe-uks-01.servicebus.windows.net/ar-hc-lz-pedw-ppe-uks-01/
# RELAYURI = "ar-ns-lz-pedw-ppe-uks-01.servicebus.windows.net"
# RELAYPATH = "ar-hc-lz-pedw-ppe-uks-01"
# # RELAY_ROOT = https://ar-ns-lz-pedw-ppe-ukw-01.servicebus.windows.net/ar-hc-lz-pedw-ppe-ukw-01/
# # RELAYURI = "ar-ns-lz-pedw-ppe-ukw-01.servicebus.windows.net"
# # RELAYPATH = "ar-hc-lz-pedw-ppe-ukw-01"
# # CRMURL = "ppcrm2016.llcdt.gov.wales/PPPINSWales"
# # CRMURL_VERSION = "v8.2"
# CRMURL = "wg-pp-pedw.crm11.dynamics.com"
# CRMURL_VERSION = "v9.2"
# //Prod Oauth WGO
@@ -48,18 +55,79 @@ CRMURL = "ppcrm2016.llcdt.gov.wales/PPPINSWales"
# RELAYPATH = "prod-pedw-hc"
# CRMURL = "crm2016.llc.gov.wales/PEDW"
# //New Prod
# CLIENT_ID = 3e4141a3-1fbd-454e-98f0-7f3a4f14a3cc
# CLIENT_SECRET = ULC8Q~fZiXm0.teVLlZ_7LjxBOj2vIsWSApu2bAq
# RELAY_ROOT = https://ar-ns-lz-pedw-prod-uks-01.servicebus.windows.net/ar-hc-lz-pedw-prod-uks-01/
# RELAYURI = "ar-ns-lz-pedw-prod-uks-01.servicebus.windows.net"
# RELAYPATH = "ar-hc-lz-pedw-prod-uks-01"
# # RELAY_ROOT = https://ar-ns-lz-pedw-ppe-ukw-01.servicebus.windows.net/ar-hc-lz-pedw-prod-ukw-01/
# # RELAYURI = "ar-ns-lz-pedw-prod-ukw-01.servicebus.windows.net"
# # RELAYPATH = "ar-hc-lz-pedw-prod-ukw-01"
# CRMURL = "crm2016.llc.gov.wales/PEDW"
# CRMURL_VERSION = "v8.2"
# D365 cnr WGO
# CLIENT_ID = 3125ac03-91ac-46a7-a166-0086f0ed90b3
# CLIENT_SECRET = bFq8Q~9VX5Ra3lKnGGIXsB.G6-ZGMx7LqyqXjdr4
# RELAY_ROOT = https://ar-ns-lz-pedw-d365cnr-uks-01.servicebus.windows.net/ar-hc-lz-pedw-d365cnr-uks-01/
# RELAYURI = "ar-ns-lz-pedw-d365cnr-uks-01.servicebus.windows.net"
# RELAYPATH = "ar-hc-lz-pedw-d365cnr-uks-01"
# CRMURL = "dv-pedw.crm11.dynamics.com"
# CRMURL_VERSION = "v9.2"
# D365 Dev WGO
# CLIENT_ID = 9b834097-03d7-409b-b038-ecfec31a394a
# CLIENT_SECRET = .Jf8Q~tKMObCJjhh_hy3J5AIctFrBalU4FS~tdxk
# RELAY_ROOT = https://ar-ns-lz-pedw-d365dev-uks-01.servicebus.windows.net/ar-hc-lz-pedw-d365dev-uks-01/
# RELAYURI = "ar-ns-lz-pedw-d365dev-uks-01.servicebus.windows.net"
# RELAYPATH = "ar-hc-lz-pedw-d365dev-uks-01"
# CRMURL = "dv-pedw.crm11.dynamics.com"
# CRMURL_VERSION = "v9.2"
# D365 Test WGO
# CLIENT_ID = d40b9373-96c2-4b04-8bd8-c2a2fe6444c1
# CLIENT_SECRET = 6hT8Q~glL~jloWpGmvXw3UY37QFwjUE0r._jobEM
# RELAY_ROOT = https://ar-ns-lz-pedw-d365test-uks-01.servicebus.windows.net/ar-hc-lz-pedw-d365test-uks-01/
# RELAYURI = "ar-ns-lz-pedw-d365test-uks-01.servicebus.windows.net"
# RELAYPATH = "ar-hc-lz-pedw-d365test-uks-01"
# CRMURL = "ts-pedw.crm11.dynamics.com"
# CRMURL_VERSION = "v9.2"
# D365 PP WGO
# CLIENT_ID = ab6c4678-b31a-4eb3-b429-e039120e488a
# CLIENT_SECRET = V288Q~P1lFMvvX9Xnkx2pLixK~GOREH983Owqc1E
# RELAY_ROOT = https://ar-ns-lz-pedw-d365ppe-uks-01.servicebus.windows.net/ar-hc-lz-pedw-d365ppe-uks-01/
# RELAYURI = "ar-ns-lz-pedw-d365ppe-uks-01.servicebus.windows.net"
# RELAYPATH = "ar-hc-lz-pedw-d365ppe-uks-01"
# CRMURL = "wg-pp-pedw.crm11.dynamics.com"
# CRMURL_VERSION = "v9.2"
# D365 Prod WGO
# CLIENT_ID = 3e4141a3-1fbd-454e-98f0-7f3a4f14a3cc
# CLIENT_SECRET = ULC8Q~fZiXm0.teVLlZ_7LjxBOj2vIsWSApu2bAq
# RELAY_ROOT = https://ar-ns-lz-pedw-d365prod-uks-01.servicebus.windows.net/ar-hc-lz-pedw-d365prod-uks-01/
# RELAYURI = "ar-ns-lz-pedw-d365prod-uks-01.servicebus.windows.net"
# RELAYPATH = "ar-hc-lz-pedw-d365prod-uks-01"
# CRMURL = "wg-prod-pedw.crm11.dynamics.com"
# CRMURL_VERSION = "v9.2"
//GOOGLE_TAG_MANAGER = GTM-T78CBC3
GOOGLE_TAG_MANAGER = G-GTWW3JT03Z
SHOWLOGIN = true
SHOWREPRESENTATIONS = true
SHOWFILEUPLOAD = "false"
SHOWMAPS = false
SHOWMAPS = true
API_ROOT = "http://localhost:3000"
CY_API_ROOT = "http://cymru.local:3000"
I18N_DOMAIN = "cymru.local"
DOCAPI_OFFLINE = "12:00:00,14:00:00"
SHOWFILTERDOCS = false
# DOCAPI_OFFLINE = "07:00:00,16:00:00"
DOCAPI_OFFLINE = "01:00:00,04:00:00"
SHOWFILTERDOCS = true
// Next Auth vars
@@ -71,15 +139,36 @@ NEXTAUTH_SECRET = SuperSecret
# DATABASE_URL = sqlserver://sql-srv-plt-pedw-cnr-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword
// dev test sql server
# DATABASE_URL = sqlserver://pedw-dev-test-sql-svr.database.windows.net:1433;database=PEDWDev;user=RobBondSQL;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword
# DATABASE_URL = sqlserver://sql-srv-plt-pedw-dev-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword
DATABASE_URL = sqlserver://sql-srv-plt-pedw-dev-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword
// test sql server
DATABASE_URL = sqlserver://sql-srv-plt-pedw-test-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword
// Pre prod sql server
DATABASE_URL = sqlserver://sql-srv-plt-pedw-ppe-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustservercertificate=true;hostnameincertificate=*.database.windows.net;logintimeout=30;authentication=SqlPassword
# DATABASE_URL = sqlserver://sql-srv-plt-pedw-ppe-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustservercertificate=true;hostnameincertificate=*.database.windows.net;logintimeout=30;authentication=SqlPassword
// Prod sql server
# DATABASE_URL = sqlserver://pp-pedw-sql.database.windows.net:1433;database=PEDWPreProd;user=RobBondSQL;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword
// Prod sql server
# DATABASE_URL = sqlserver://sql-srv-plt-pedw-prod-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword
// D365 CNR sql server
# DATABASE_URL = sqlserver://sql-srv-plt-pedw-d365cnr-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword
// D365 Dev sql server
# DATABASE_URL = sqlserver://sql-srv-plt-pedw-d365dev-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword
// D365 Test sql server
# DATABASE_URL = sqlserver://sql-srv-plt-pedw-d365test-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword
// D365 PPE sql server
# DATABASE_URL = sqlserver://sql-srv-plt-pedw-d365ppe-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword
// D365 Prod sql server
# DATABASE_URL = sqlserver://sql-srv-plt-pedw-d365prod-uks-01.database.windows.net:1433;database=PEDW;user=PedwSqlAdmin;password=GhWKr62F5sQ69dZx;encrypt=true;trustServerCertificate=true;hostNameInCertificate=*.database.windows.net;loginTimeout=30;authentication=ActiveDirectoryPassword
SECRET = SuperSecret
@@ -91,31 +180,62 @@ AZURE_TENANT_ID = $TENANT
AZURE_CLIENT_SECRET = $CLIENT_SECRET
// CNR key
# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwcnruks"
# AZURE_STORAGE_ACCOUNT_KEY = hRffHq4IwctpaTKdS/U33Xq6nyTDF1t++WJgIwsw7gzstktZin/rRwbTIAL9KKqesVsK7EHrTTG6+ASt4+7skw==
# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwcnruks.blob.core.windows.net
AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwcnruks"
AZURE_STORAGE_ACCOUNT_KEY = hRffHq4IwctpaTKdS/U33Xq6nyTDF1t++WJgIwsw7gzstktZin/rRwbTIAL9KKqesVsK7EHrTTG6+ASt4+7skw==
AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwcnruks.blob.core.windows.net
// new dev key
# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwdevuks"
# AZURE_STORAGE_ACCOUNT_KEY = MmrsrCjBfop9pI2oImsP/+lJUWU2DrXsAB8nq5bOVROTmPovSGxjCjKw9+TAFr00k8WDUK9r6mVx+AStcMLyAA==
# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwdevuks.blob.core.windows.net
AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwdevuks"
AZURE_STORAGE_ACCOUNT_KEY = MmrsrCjBfop9pI2oImsP/+lJUWU2DrXsAB8nq5bOVROTmPovSGxjCjKw9+TAFr00k8WDUK9r6mVx+AStcMLyAA==
AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwdevuks.blob.core.windows.net
// New test key
# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwtestuks"
# AZURE_STORAGE_ACCOUNT_KEY = bqJAz5pYQVGee8dnVhlsqW+xHwinNTjgY2AmojNyB+d4uAotMR20WcO4Op13yB+9dFaXAkkjtIA4+AStHT/IYg==
# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwtestuks.blob.core.windows.net
AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwtestuks"
# AZURE_STORAGE_ACCOUNT_KEY = 5NdDO9GGlbTFLHFOXZRw8H2C0Ow5bFTvHwUHEbStyzg2mbd1uwHSeBSvM/dkN+HfdwrFzqAkwL5P+AStdO6cig==
AZURE_STORAGE_ACCOUNT_KEY = bqJAz5pYQVGee8dnVhlsqW+xHwinNTjgY2AmojNyB+d4uAotMR20WcO4Op13yB+9dFaXAkkjtIA4+AStHT/IYg==
AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwtestuks.blob.core.windows.net
// New preprod key
AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwppeuks"
AZURE_STORAGE_ACCOUNT_KEY = B4GRKr+cA9b9mNM/GHFivzhzflJIZltG8bMmooXlYvGYySPAspTTHistAT30sHipnR+hh6dqUc9q+ASttAjPRQ==
AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwppeuks.blob.core.windows.net
# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwppeuks"
# AZURE_STORAGE_ACCOUNT_KEY = B4GRKr+cA9b9mNM/GHFivzhzflJIZltG8bMmooXlYvGYySPAspTTHistAT30sHipnR+hh6dqUc9q+ASttAjPRQ==
# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwppeuks.blob.core.windows.net
// PROD key
# PROD key
# AZURE_STORAGE_ACCOUNT_NAME = "pedwprod"
# AZURE_STORAGE_ACCOUNT_KEY = koZ46STRqapSa2Dod8MLL6aju1eYLhhyJ6DXpCLMTnFLJvbDT6CHUv/jwr5U/Ta8a60MWzwToHQy+AStADO7sg==
# AZURE_PEDW_STORAGE_ENDPOINT = https://pedwprod.blob.core.windows.net
# New PROD key
# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwproduks"
# AZURE_STORAGE_ACCOUNT_KEY = OfV9MFqXcLLPam9GhEdMWadIOwIzhc5hd5TUqzIuQoswFdtV4ybaemjatSv+Kht0Ny1qQKjLplwY+AStUTZJrg==
# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwproduks.blob.core.windows.net
# D365 CNR key
# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwd365cnruks"
# AZURE_STORAGE_ACCOUNT_KEY = nEwCp+4FuVf/9a4DOyKuQO2yRRV4Ypigg20ch1YaiL09YmEhWMEO5pNLpYtI3WnQCkIqeu1CUqwM+AStCLW01g==
# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwd365cnruks.blob.core.windows.net
# D365 DEV key
# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwd365devuks"
# AZURE_STORAGE_ACCOUNT_KEY = XzwQxuZXSwYrVgemDUD2ymTpYTIfWqhypbmEALw1ZEEP0M4D3b3Gh28clSymxwFu7Gu9mhwkW4/V+AStZ6RF8Q==
# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwd365devuks.blob.core.windows.net
# D365 TEST key
# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwd365testuks"
# AZURE_STORAGE_ACCOUNT_KEY = 5NdDO9GGlbTFLHFOXZRw8H2C0Ow5bFTvHwUHEbStyzg2mbd1uwHSeBSvM/dkN+HfdwrFzqAkwL5P+AStdO6cig==
# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwd365testuks.blob.core.windows.net
# D365 PP key
# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwd365ppeuks"
# AZURE_STORAGE_ACCOUNT_KEY = xQz4FWDnQDE09uPOCj1t7axRiquvZG9vbxL88ywzVsJnTsAgewrHnuAGkxGPBCp2xGE53ffYOme6+AStSr8OoQ==
# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwd365ppeuks.blob.core.windows.net
# D365 Prod key
# AZURE_STORAGE_ACCOUNT_NAME = "sapltpedwd365produks"
# AZURE_STORAGE_ACCOUNT_KEY = DvKcWhDpFKMFdVXFF2SqDG0sYQ3AkNCmJUBG0cvgzi5NBHS4Ahd2hPAlfA3hGi2EbohUag3vCucK+ASt1IdBhA==
# AZURE_PEDW_STORAGE_ENDPOINT = https://sapltpedwd365produks.blob.core.windows.net
AZURE_PEDW_CONTAINER = "pedwapplications"
AZURE_PEDW_QUEUE_ENDPOINT = https://$AZURE_STORAGE_ACCOUNT_NAME.queue.core.windows.net
@@ -128,22 +248,29 @@ SHOW_DEBUG = true
// DEV Connection string
//NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=4b7568b3-70bd-4591-9dc2-267c0a6501d1;IngestionEndpoint=https://ukwest-0.in.applicationinsights.azure.com/;LiveEndpoint=https://ukwest.livediagnostics.monitor.azure.com/;ApplicationId=447d38b5-5d40-4238-8fb3-0ef4741b3db1
NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=4b7568b3-70bd-4591-9dc2-267c0a6501d1;IngestionEndpoint=https://ukwest-0.in.applicationinsights.azure.com/;LiveEndpoint=https://ukwest.livediagnostics.monitor.azure.com/;ApplicationId=447d38b5-5d40-4238-8fb3-0ef4741b3db1
APPLICATIONINSIGHTS_CONNECTIONSTRING = InstrumentationKey=7a06e24a-793c-47bc-8987-ce80f180e035;IngestionEndpoint=https://uksouth-1.in.applicationinsights.azure.com/;LiveEndpoint=https://uksouth.livediagnostics.monitor.azure.com/;ApplicationId=9906b6d0-b791-4add-8d53-c827a534e1d4
// Test Connection string
//NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=cc60e8e7-5b30-4c2b-a176-8b96b21b12df;IngestionEndpoint=https://ukwest-0.in.applicationinsights.azure.com/;LiveEndpoint=https://ukwest.livediagnostics.monitor.azure.com/;ApplicationId=2a4094be-6fd1-46ab-a67d-5f241692b129
NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=cc60e8e7-5b30-4c2b-a176-8b96b21b12df;IngestionEndpoint=https://ukwest-0.in.applicationinsights.azure.com/;LiveEndpoint=https://ukwest.livediagnostics.monitor.azure.com/;ApplicationId=2a4094be-6fd1-46ab-a67d-5f241692b129
// Preprod Connection string
//NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=7370f0f9-834d-4c7a-b3c4-9951fcad5ad2;IngestionEndpoint=https://ukwest-0.in.applicationinsights.azure.com/;LiveEndpoint=https://ukwest.livediagnostics.monitor.azure.com/;ApplicationId=04362cb7-6dfc-469e-8274-12243e8be851
# NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=7370f0f9-834d-4c7a-b3c4-9951fcad5ad2;IngestionEndpoint=https://ukwest-0.in.applicationinsights.azure.com/;LiveEndpoint=https://ukwest.livediagnostics.monitor.azure.com/;ApplicationId=04362cb7-6dfc-469e-8274-12243e8be851
NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=a41b91bf-f7dc-4f44-bb66-a67df4eee435;IngestionEndpoint=https://uksouth-1.in.applicationinsights.azure.com/;LiveEndpoint=https://uksouth.livediagnostics.monitor.azure.com/;ApplicationId=473d0632-16f1-4065-8984-d6bac62402c0
# NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=a41b91bf-f7dc-4f44-bb66-a67df4eee435;IngestionEndpoint=https://uksouth-1.in.applicationinsights.azure.com/;LiveEndpoint=https://uksouth.livediagnostics.monitor.azure.com/;ApplicationId=473d0632-16f1-4065-8984-d6bac62402c0
// Prod Connection string
//NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=61f358c2-8075-4164-b897-17bc6dd31fee;IngestionEndpoint=https://ukwest-0.in.applicationinsights.azure.com/;LiveEndpoint=https://ukwest.livediagnostics.monitor.azure.com/;ApplicationId=0c297349-396b-4dc6-8b56-85f5c680adc6
# NEXT_PUBLIC_APP_INSIGHT = InstrumentationKey=61f358c2-8075-4164-b897-17bc6dd31fee;IngestionEndpoint=https://ukwest-0.in.applicationinsights.azure.com/;LiveEndpoint=https://ukwest.livediagnostics.monitor.azure.com/;ApplicationId=0c297349-396b-4dc6-8b56-85f5c680adc6
HIDEERRORLOGS = false
SHOWSIPS = true
ALLOWED_IPS=::1,203.0.113.42,198.51.100.17
UPLOAD_BATCH_COUNT = 5
NRWDOMAIN = rdbmedia.co.uk
+19 -2
View File
@@ -37,5 +37,22 @@ pages/polling.js
pages/api/endpoint/getaccounts_api.js
# certs
certificates/*
# remove swagger api docs
next-swagger-doc.json
pages/api-doc.js
public/swagger.json
certificates/cert-key.pem
certificates/cert.pem
gitclean.sh
pages/admin/logcheck.js
pages/admin/logchecker.js
pages/baracuda.html
pages/holding-min.html
pages/holding.html
# Cline / AI-assistant governance artifacts (keep local-only)
ai-prompts/
pages/baracuda.min.html
+2 -1
View File
@@ -1,5 +1,6 @@
{
"tabWidth": 4,
"useTabs": false,
"quoteProps":"preserve"
"quoteProps": "preserve",
"trailingComma": "none"
}
+2 -2
View File
@@ -8,10 +8,10 @@
"name": "Next.js: debug server-side",
"type": "node-terminal",
"request": "launch",
"command": "yarn run dev"
"command": "npm run dev"
},
{"name": "Launch Chrome against localhost",
"type": "pwa-chrome",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
+46
View File
@@ -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.
+25
View File
@@ -0,0 +1,25 @@
# Use official Node.js 20 LTS image
FROM node:20-alpine
# Set working directory
WORKDIR /app
# Install dependencies first (better caching)
COPY package*.json ./
RUN npm ci --production
# Copy source files (including .next build output if pre-built)
COPY . .
# If you want to build inside container, uncomment:
# RUN npm run build
# Use non-root user (optional, but recommended)
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Expose port your app listens on
EXPOSE 3000
# Start your Next.js app (adjust if using custom server)
CMD ["npm", "start"]
+83
View File
@@ -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`
Vendored
+50
View File
@@ -0,0 +1,50 @@
pipeline {
agent {
docker {
image 'node:20' // ✅ Node.js 20 with npm included
}
}
environment {
IMAGE = "pedw-app"
}
stages {
stage('Install & Build') {
steps {
sh 'npm ci'
sh 'npm run build'
}
}
stage('Build Docker Image') {
steps {
sh "docker build -t $REGISTRY/$IMAGE:latest ."
}
}
stage('Push Docker Image') {
steps {
withCredentials([usernamePassword(credentialsId: 'dockerhub-credentials', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) {
sh "echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin"
sh "docker push $REGISTRY/$IMAGE:latest"
}
}
}
stage('Deploy to Plesk') {
steps {
sshagent(['plesk-ssh-key']) {
sh """
ssh root@your-plesk-domain.com '
docker pull $REGISTRY/$IMAGE:latest &&
docker stop nextjs-app || true &&
docker rm nextjs-app || true &&
docker run -d --restart=always --name nextjs-app -p 3000:3000 $REGISTRY/$IMAGE:latest
'
"""
}
}
}
}
}
+379 -253
View File
@@ -11,7 +11,7 @@ const {
ContainerSASPermissions,
generateBlobSASQueryParameters,
SASProtocol,
SASProtocol
} = require("@azure/storage-blob");
const {
QueueServiceClient,
@@ -23,7 +23,7 @@ const {
generateAccountSASQueryParameters,
generateQueueSASQueryParameters,
StorageSharedKeyCredential,
QueueClient,
QueueClient
} = require("@azure/storage-queue");
const STORAGE_PATH = process.env.AZURE_PEDW_STORAGE_ENDPOINT;
const STORAGE_CONTAINER = process.env.AZURE_PEDW_CONTAINER;
@@ -31,6 +31,88 @@ const QUEUE_PATH = process.env.AZURE_PEDW_QUEUE_ENDPOINT;
const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME;
const buildDownloadBlobQueryPath = ({
containerName,
casefolderID,
blobname,
encodeCasefolderID = true,
encodeBlobname = true
}) => {
const casefolderPart = encodeCasefolderID
? encodeURIComponent(casefolderID)
: casefolderID;
const blobPart = encodeBlobname ? encodeURIComponent(blobname) : blobname;
return (
"/api/file/downloadblob?container=" +
containerName +
"&casefolderID=" +
casefolderPart +
"&blobname=" +
blobPart
);
};
const buildDeleteBlobQueryPath = ({
containerName,
casefolderID,
blobname,
encodeCasefolderID = true,
encodeBlobname = true
}) => {
const casefolderPart = encodeCasefolderID
? encodeURIComponent(casefolderID)
: casefolderID;
const blobPart = encodeBlobname ? encodeURIComponent(blobname) : blobname;
return (
"/api/file/deleteblob?container=" +
containerName +
"&casefolderID=" +
casefolderPart +
"&blobname=" +
blobPart
);
};
const buildGetBlobListQueryPath = ({ containerName, casefolderID }) => {
return (
"/api/file/getbloblist?container=" +
containerName +
"&casefolderID=" +
casefolderID
);
};
const buildCaseObjectPath = (casefolderID) => {
return casefolderID + "/" + casefolderID + "_case.json";
};
const buildHashMetadataPaths = ({ containerName, casefolderID, blobname }) => {
return {
"hashedfilepath": hashAPIPath(
buildDownloadBlobQueryPath({
containerName,
casefolderID,
blobname
})
),
"hasheddeletepath": hashAPIPath(
buildDeleteBlobQueryPath({
containerName,
casefolderID,
blobname
})
),
"hashgetblobs": hashAPIPath(
buildGetBlobListQueryPath({
containerName,
casefolderID
})
)
};
};
export const createContainerSas = async (containerName) => {
// Get environment variables
@@ -67,7 +149,7 @@ export const createContainerSas = async (containerName) => {
permissions: ContainerSASPermissions.parse(containerPermissions),
protocol: SASProtocol.HttpsAndHttp,
startsOn: TEN_MINUTES_BEFORE_NOW,
expiresOn: TEN_MINUTES_AFTER_NOW,
expiresOn: TEN_MINUTES_AFTER_NOW
};
// console.log(
@@ -126,7 +208,7 @@ export const createBlobSas = async (containerName, blobName) => {
permissions: BlobSASPermissions.parse(blobPermissionsForAnonymousUser),
protocol: SASProtocol.HttpsAndHttp,
startsOn: TEN_MINUTES_BEFORE_NOW,
expiresOn: TEN_MINUTES_AFTER_NOW,
expiresOn: TEN_MINUTES_AFTER_NOW
};
const sasToken = generateBlobSASQueryParameters(
@@ -187,61 +269,38 @@ export const getBlobs = async (containerName, casefolderID) => {
const blobObj = [];
for await (const blob of containerClient.listBlobsFlat({
prefix: casefolderID + "/files/",
prefix: casefolderID + "/files/"
})) {
let blobDocumentType = blob.name
.split("/")[2]
.slice(0, blob.name.split("/")[2].indexOf("_"));
const blobPathParts = blob.name.split("/");
const fileName = blobPathParts[2];
const contentLength = blob.properties.contentLength;
blobObj.push({
"name": blob.name.split("/")[2],
"name": fileName,
"path": blob.name,
"documentType": getDocumentTypeFromFilename(
blob.name.split("/")[2]
),
"documentType": getDocumentTypeFromFilename(fileName),
"versionId": blob.versionId,
"caseObj": casefolderID + "/" + casefolderID + "_case.json",
"caseObj": buildCaseObjectPath(casefolderID),
"isCurrentVersion": blob.isCurrentVersion,
"contentLength": blob.properties.contentLength,
"size": blob.properties.contentLength,
"contentLength": contentLength,
"size": contentLength,
"contentType": blob.contentType,
"lastModified": blob.properties.lastModified,
"filepath":
"/api/file/downloadblob?container=" +
containerName +
"&casefolderID=" +
casefolderID +
"&blobname=" +
blob.name,
"hashedfilepath": hashAPIPath(
"/api/file/downloadblob?container=" +
containerName +
"&casefolderID=" +
casefolderID +
"&blobname=" +
blob.name.split("/")[2]
),
"deletepath":
"/api/file/deleteblob?container=" +
containerName +
"&casefolderID=" +
casefolderID +
"&blobname=" +
blob.name.split("/")[2],
"hasheddeletepath": hashAPIPath(
"/api/file/deleteblob?container=" +
containerName +
"&casefolderID=" +
casefolderID +
"&blobname=" +
blob.name.split("/")[2]
),
"hashgetblobs": hashAPIPath(
"/api/file/getbloblist?container=" +
containerName +
"&casefolderID=" +
casefolderID
),
"filepath": buildDownloadBlobQueryPath({
containerName,
casefolderID,
blobname: blob.name
}),
...buildHashMetadataPaths({
containerName,
casefolderID,
blobname: fileName
}),
"deletepath": buildDeleteBlobQueryPath({
containerName,
casefolderID,
blobname: fileName
})
});
}
//console.log("blobObj:", blobObj);
@@ -277,7 +336,7 @@ export const createBlob = async (formContent, containerName, caseref) => {
const tags = {
containerid: containerName,
caseID: caseID,
blobType: "Appeal",
blobType: "Appeal"
};
//console.log("the tags:", tags);
@@ -309,15 +368,15 @@ export const createRepBlob = async (formContent, containerName, caseref) => {
const tags = {
containerid: containerName,
caseID: formContent.caseRef,
blobType: "Representation",
caseID: formContent.ticketnumber || formContent.caseRef,
blobType: "Representation"
};
//console.log("the tags:", tags);
const withTags = await blockBlobClient.setTags(tags);
const withMeta = await blockBlobClient.setMetadata(tags);
return formContent.pinswg_name;
return formContent.ticketnumber;
};
export const createAppealPDFBlob = async (
@@ -356,21 +415,24 @@ export const createAppealPDFBlob = async (
const uploadOptions = {
blockSize: 4 * 1024 * 1024, // 4 MiB max block size
concurrency: 2, // maximum number of parallel transfer workers
maxSingleShotSize: 8 * 1024 * 1024, // 8 MiB initial transfer size
maxSingleShotSize: 8 * 1024 * 1024 // 8 MiB initial transfer size
};
console.log("blobName:", blobName);
const blockBlobClient = containerClient.getBlockBlobClient(blobName);
const uploadBlobResponse = await blockBlobClient.uploadStream(content);
const uploadBlobResponse = Buffer.isBuffer(content)
? await blockBlobClient.uploadData(content)
: await blockBlobClient.uploadStream(content);
console.log(uploadBlobResponse);
let whichLocation = "846040000"; // Default value
const tags = {
containerid: containerName,
caseID: caseID,
blobType: "Appeal PDF",
documentLocationId: "846040000",
ishareLocation: "846040000",
ishareLocation: whichLocation,
documentLocationId: whichLocation
};
//console.log("the tags:", tags);
@@ -404,35 +466,36 @@ export const createRepPDFBlob = async (
const uploadOptions = {
blockSize: 4 * 1024 * 1024, // 4 MiB max block size
concurrency: 2, // maximum number of parallel transfer workers
maxSingleShotSize: 8 * 1024 * 1024, // 8 MiB initial transfer size
maxSingleShotSize: 8 * 1024 * 1024 // 8 MiB initial transfer size
};
console.log("blobName:", blobName);
const blockBlobClient = containerClient.getBlockBlobClient(blobName);
const uploadBlobResponse = await blockBlobClient.uploadStream(content);
const uploadBlobResponse = Buffer.isBuffer(content)
? await blockBlobClient.uploadData(content)
: await blockBlobClient.uploadStream(content);
let whichLocation =
repName.indexOf("_IP_") > 0
? "846040005"
: repName.indexOf("_Statement_") > 0
? "846040002"
: repName.indexOf("_Questionnaire_") > 0
? "846040001"
: repName.indexOf("_Comments") > 0
? "846040003"
: repName.indexOf("_Impact") > 0
? "846040029"
: repName.indexOf("_Consultation_Response_") > 0
? "846040036"
: "846040002";
const locationMap = [
{ key: "_IP_-_Comments_", value: "846040038" }, // most specific first
{ key: "_IP_", value: "846040005" },
{ key: "_Statement_", value: "846040002" },
{ key: "_Questionnaire_", value: "846040001" },
{ key: "_Comments", value: "846040003" },
{ key: "_Impact", value: "846040029" },
{ key: "_Consultation_Response_", value: "846040036" }
];
const whichLocation =
locationMap.find((item) => repName.includes(item.key))?.value ||
"846040002";
const tags = {
containerid: containerName,
caseID: caseID,
blobType: "Representation PDF",
documentLocationId: whichLocation,
ishareLocation: whichLocation,
documentLocationId: whichLocation
};
//console.log("the tags:", tags);
@@ -446,7 +509,7 @@ export const deleteBlob = async (containerName, blobName) => {
const creds = new DefaultAzureCredential();
const options = {
deleteSnapshots: "include", // or 'only'
deleteSnapshots: "include" // or 'only'
};
const containerToken = await createContainerSas(containerName);
@@ -466,7 +529,7 @@ export const deleteBlobCase = async (containerName, blobName) => {
const creds = new DefaultAzureCredential();
const options = {
deleteSnapshots: "include", // or 'only'
deleteSnapshots: "include" // or 'only'
};
const containerToken = await createContainerSas(containerName);
@@ -478,7 +541,7 @@ export const deleteBlobCase = async (containerName, blobName) => {
console.log("blob to delete:", blobName);
for await (const blob of containerClient.listBlobsFlat({
prefix: blobName,
prefix: blobName
})) {
console.log(" ------ :", blob.name);
containerClient.deleteBlob(blob.name);
@@ -496,7 +559,7 @@ export const deleteBlobRep = async (containerName, blobName) => {
const creds = new DefaultAzureCredential();
const options = {
deleteSnapshots: "include", // or 'only'
deleteSnapshots: "include" // or 'only'
};
const containerToken = await createContainerSas(containerName);
@@ -506,7 +569,7 @@ export const deleteBlobRep = async (containerName, blobName) => {
console.log("blob to delete:", blobName);
for await (const blob of containerClient.listBlobsFlat({
prefix: blobName,
prefix: blobName
})) {
console.log("------ :", blob.name);
const blockBlobClient = containerClient.getBlockBlobClient(blob.name);
@@ -554,76 +617,54 @@ export const uploadFile = async (formContent, containerName, foldername) => {
);
console.log(data);
let whichLocation =
files[prop][0].fieldName.indexOf("_Statement_of_Case") >
0 ||
files[prop][0].fieldName.indexOf("_-_Statement_of_Case") >
0 ||
files[prop][0].fieldName.indexOf("_-_Application_Form") >
0 ||
files[prop][0].fieldName.indexOf(
"_-_Site_Ownership_Certificate"
) > 0 ||
files[prop][0].fieldName.indexOf("_-_Decision_Notice") >
0 ||
files[prop][0].fieldName.indexOf("_-_Site_Location_Plan") >
0 ||
files[prop][0].fieldName.indexOf(
"_-_Plans_Drawing_Documents"
) > 0 ||
files[prop][0].fieldName.indexOf(
"_-_Additional_Plans_Drawings_Documents"
) > 0 ||
files[prop][0].fieldName.indexOf(
"_-_Design_and_Access_Statement"
) > 0 ||
files[prop][0].fieldName.indexOf(
"_-_NSB_LPA_Additional_Documents"
) > 0 ||
files[prop][0].fieldName.indexOf("_-_LPA_Correspondence") >
0 ||
files[prop][0].fieldName.indexOf(
"_-_LPA_Original_Permission"
) > 0 ||
files[prop][0].fieldName.indexOf(
"_-_LPA's_Registration_Letter"
) > 0 ||
files[prop][0].fieldName.indexOf(
+"_-_Environmental_Statement"
) > 0 ||
files[prop][0].fieldName.indexOf("_-_Cost_of_Application") >
0 ||
files[prop][0].fieldName.indexOf(
"_-_Other_Relevant_Material"
) > 0 ||
files[prop][0].fieldName.indexOf(
"_-_S106_Agreement_or_Unilateral_Undertaking" > 0
)
? "846040000"
: files[prop][0].fieldName.indexOf("_IP_") > 0
? "846040005"
: files[prop][0].fieldName.indexOf("_Statement_") > 0
? "846040002"
: files[prop][0].fieldName.indexOf("_Questionnaire_") >
0
? "846040001"
: files[prop][0].fieldName.indexOf("_Comments_") > 0
? "846040003"
: files[prop][0].fieldName.indexOf("_Impact_") > 0
? "846040001"
: files[prop][0].fieldName.indexOf(
"_Consultation_Response_"
) > 0
? "846040036"
: "846040000";
const fieldName = files[prop][0].fieldName;
const whichLocation =
[
// 1️⃣ LPA docs (grouped)
{
match: [
"_Statement_of_Case",
"_-_Statement_of_Case",
"_-_Application_Form",
"_-_Site_Ownership_Certificate",
"_-_Decision_Notice",
"_-_Site_Location_Plan",
"_-_Plans_Drawing_Documents",
"_-_Additional_Plans_Drawings_Documents",
"_-_Design_and_Access_Statement",
"_-_NSB_LPA_Additional_Documents",
"_-_LPA_Correspondence",
"_-_LPA_Original_Permission",
"_-_LPA's_Registration_Letter",
"_-_Environmental_Statement",
"_-_Cost_of_Application",
"_-_Other_Relevant_Material",
"_-_S106_Agreement_or_Unilateral_Undertaking"
],
value: "846040000"
},
{ match: ["_IP_-_Comments_"], value: "846040038" },
{
match: ["_Consultation_Response_"],
value: "846040036"
},
{ match: ["_Questionnaire_"], value: "846040001" },
{ match: ["_Statement_"], value: "846040002" },
{ match: ["_Comments_"], value: "846040003" },
{ match: ["_Impact_"], value: "846040001" },
{ match: ["_IP_"], value: "846040005" }
].find((rule) =>
rule.match.some((key) => fieldName.includes(key))
)?.value || "846040000";
const tags = {
containerid: containerName,
caseID: foldername.split("/")[0],
documentType: files[prop][0].fieldName.split(".")[1],
blobType: "RepresentationFile",
documentLocationId: whichLocation,
ishareLocation: whichLocation,
documentLocationId: whichLocation
};
const withTags = blockBlobClient.setTags(tags);
const withMeta = blockBlobClient.setMetadata(tags);
@@ -734,14 +775,16 @@ export const uploadSingleFile = async (
{
pattern:
/_Statement_of_Case|_-_Statement_of_Case|_-_Application_Form|_-_Site_Ownership_Certificate|_-_Decision_Notice|_-_Site_Location_Plan|_-_Plans_Drawing_Documents|_-_Additional_Plans_Drawings_Documents|_-_Design_and_Access_Statement|_-_NSB_LPA_Additional_Documents|_-_LPA_Correspondence|_-_LPA_Original_Permission|_-_LPA's_Registration_Letter|_-_Environmental_Statement|_-_Cost_of_Application|_-_Other_Relevant_Material|_-_S106_Agreement_or_Unilateral_Undertaking/,
value: "846040000",
value: "846040000"
},
{ pattern: /_IP_-_Comments_/, value: "846040038" },
{ pattern: /_IP_/, value: "846040005" },
{ pattern: /_Statement_/, value: "846040002" },
{ pattern: /_Questionnaire_/, value: "846040001" },
{ pattern: /_Comments_/, value: "846040003" },
{ pattern: /_Impact_/, value: "846040001" },
{ pattern: /_Consultation_Response_/, value: "846040036" },
{ pattern: /_Consultation_Response_/, value: "846040036" }
];
// Find a matching pattern or fallback to default
@@ -762,7 +805,7 @@ export const uploadSingleFile = async (
documentType: files[prop][0].fieldName.split(".")[1],
blobType: "RepresentationFile",
ishareLocation: whichLocation,
documentLocationId: whichLocation,
documentLocationId: whichLocation
};
const withTags = await blockBlobClient.setTags(tags);
const withMeta = await blockBlobClient.setMetadata(tags);
@@ -799,7 +842,7 @@ export const uploadRepFiles = async (
const tags = {
containerid: containerName,
caseID: formContent.split("tmp/")[0],
documentType: "Rep docs",
documentType: "Rep docs"
};
console.log(tags);
const withTags = await blockBlobClient.setTags(tags);
@@ -844,7 +887,7 @@ export const uploadPDFRepFiles = async (
containerid: containerName,
caseID: foldername.split("/")[0],
documentType: repType,
blobType: "RepresentationPDF",
blobType: "RepresentationPDF"
};
//console.log("the tags:", tags);
@@ -897,7 +940,7 @@ export const uploadPDFAppealFiles = async (
const tags = {
containerid: containerName,
caseID: foldername.split("/")[0],
blobType: "AppealPDF",
blobType: "AppealPDF"
};
//console.log("the tags:", tags);
@@ -936,7 +979,7 @@ export const uploadPDFCaseFiles = async (
const tags = {
containerid: containerName,
caseID: foldername.split("/")[0],
blobType: "AppealPDF",
blobType: "AppealPDF"
};
//console.log("the tags:", tags);
@@ -1142,7 +1185,7 @@ export const downloadAllRepsFiles = async (containerName, repsBlobObj) => {
return {
"@odata.count": blobCount,
"value": downloaded,
"value": downloaded
};
};
@@ -1181,7 +1224,7 @@ export const getCaseBlob = async (
const tags = {
containerid: containerName,
caseID: caseReference,
blobType: "Case",
blobType: "Case"
};
//console.log("the tags:", tags);
@@ -1212,7 +1255,7 @@ export const getProgressBlobs = async (containerName, caseReference) => {
let blobCount = 0;
for await (const blob of containerClient.listBlobsFlat({
prefix: caseReference,
prefix: caseReference
})) {
blobCount++;
}
@@ -1225,47 +1268,34 @@ export const getProgressBlobs = async (containerName, caseReference) => {
let blobObj = [];
for await (const blob of containerClient.listBlobsFlat({
prefix: caseReference + "/" + caseReference + "_appeal.json",
prefix: caseReference + "/" + caseReference + "_appeal.json"
})) {
console.log("getProgressBlobs in here", blob.name.split("/"));
const blobPathParts = blob.name.split("/");
const appealBlobName = blobPathParts[1];
const contentLength = blob.properties.contentLength;
//consoleLogger("getProgressBlobs in here", blobPathParts);
blobObj.push({
"name": blob.name.split("/")[1],
"name": appealBlobName,
"path": blob.name,
"versionId": blob.versionId,
"caseObj": caseReference + "/" + caseReference + "_case.json",
"caseObj": buildCaseObjectPath(caseReference),
"isCurrentVersion": blob.isCurrentVersion,
"contentLength": blob.properties.contentLength,
"contentLength": contentLength,
"contentType": blob.contentType,
"lastModified": blob.properties.lastModified,
"hashedfilepath": hashAPIPath(
"/api/file/downloadblob?container=" +
containerName +
"&casefolderID=" +
caseReference +
"&blobname=" +
blob.name.split("/")[1]
),
"hasheddeletepath": hashAPIPath(
"/api/file/deleteblob?container=" +
containerName +
"&casefolderID=" +
caseReference +
"&blobname=" +
blob.name.split("/")[1]
),
"hashgetblobs": hashAPIPath(
"/api/file/getbloblist?container=" +
containerName +
"&casefolderID=" +
caseReference
),
...buildHashMetadataPaths({
containerName,
casefolderID: caseReference,
blobname: appealBlobName
})
});
}
blobObj = _.sortBy(blobObj, [
function (o) {
return o.lastModified;
},
}
]).reverse()[0];
return blobObj;
@@ -1284,8 +1314,24 @@ export const getAllProgressBlobs = async (containerName) => {
for await (const blob of containerClient.findBlobsByTags(
"blobType='Appeal'"
)) {
blob.name.split("/")[1].indexOf("_appeal.json") > 0 &&
blob.name.split("/")[1].indexOf("undefined") < 0 &&
const namePart = blob.name.split("/")[1] ?? "";
// keep your existing name filters
if (
namePart.indexOf("_appeal.json") <= 0 ||
namePart.indexOf("undefined") >= 0
)
continue;
// existence check (filters out soft-deleted / stale index hits)
const blobClient = containerClient.getBlobClient(blob.name);
try {
await blobClient.getProperties();
} catch (e) {
// errors usually expose statusCode
if (e?.statusCode === 404) continue;
throw e;
}
blobObj.push({
"name": blob.name.split("/")[1],
"path": blob.name,
@@ -1294,7 +1340,7 @@ export const getAllProgressBlobs = async (containerName) => {
blob.name.split("/")[0] +
"/" +
blob.name.split("/")[0] +
"_case.json",
"_case.json"
// "isCurrentVersion": blob.isCurrentVersion,
// "contentLength": blob.properties.contentLength,
// "contentType": blob.contentType,
@@ -1351,7 +1397,7 @@ export const getRepsBlobs = async (containerName) => {
includeMetadata: true,
includeSnapshots: false,
includeTags: true,
includeVersions: true,
includeVersions: true
};
let blobObj = [];
@@ -1360,14 +1406,27 @@ export const getRepsBlobs = async (containerName) => {
listOptions
)) {
const blobClient = containerClient.getBlobClient(blob.name);
//console.log("getreps blob:", blob);
blob.name.split("/")[2].indexOf("_rep.json") > 0 &&
blob.name.split("/")[2].indexOf("undefined") < 0 &&
// Filter out soft-deleted/stale tag entries and malformed names.
const namePart = blob.name.split("/")[2] ?? "";
if (
namePart.indexOf("_rep.json") <= 0 ||
namePart.indexOf("undefined") >= 0
)
continue;
try {
const properties = await blobClient.getProperties();
blobObj.push({
"name": blob.name.split("/")[2],
"name": namePart,
"path": blob.name,
"size": blobClient.getProperties().contentLength,
"size": properties.contentLength
});
} catch (error) {
if (error?.statusCode === 404) continue;
throw error;
}
}
//console.log("blobObjwwwww:", blobObj);
@@ -1390,73 +1449,43 @@ export const getRepsFilesBlobs = async (
const blobObj = [];
for await (const blob of containerClient.listBlobsFlat({
prefix: casefolderID + "/" + filenamePrefix + "/files/",
prefix: casefolderID + "/" + filenamePrefix + "/files/"
})) {
let blobDocumentType = blob.name
.split("/")[2]
.slice(0, blob.name.split("/")[2].indexOf("_"));
const blobPathParts = blob.name.split("/");
const casefolderPath = blobPathParts[0] + "/" + blobPathParts[1];
const repFileName = blobPathParts[3];
const contentLength = blob.properties.contentLength;
console.log(blob.name);
//consoleLogger(blob.name);
blobObj.push({
"name": blob.name.split("/")[3],
"name": repFileName,
"path": blob.name,
"documentType": getDocumentTypeFromFilename(
blob.name.split("/")[3]
),
"documentType": getDocumentTypeFromFilename(repFileName),
"versionId": blob.versionId,
"isCurrentVersion": blob.isCurrentVersion,
"contentLength": blob.properties.contentLength,
"contentLength": contentLength,
"filenameprefix": filenamePrefix,
"filepath":
"/api/file/downloadblob?container=" +
containerName +
"&casefolderID=" +
blob.name.split("/")[0] +
"/" +
blob.name.split("/")[1] +
"&blobname=" +
blob.name.split("/")[3],
"hashedfilepath": hashAPIPath(
"/api/file/downloadblob?container=" +
containerName +
"&casefolderID=" +
blob.name.split("/")[0] +
"/" +
blob.name.split("/")[1] +
"&blobname=" +
blob.name
),
"deletepath":
"/api/file/deleteblob?container=" +
containerName +
"&casefolderID=" +
blob.name.split("/")[0] +
"/" +
blob.name.split("/")[1] +
"&blobname=" +
blob.name.split("/")[3],
"hasheddeletepath": hashAPIPath(
"/api/file/deleteblob?container=" +
containerName +
"&casefolderID=" +
blob.name.split("/")[0] +
"/" +
blob.name.split("/")[1] +
"&blobname=" +
blob.name.split("/")[3]
),
"hashgetblobs": hashAPIPath(
"/api/file/getbloblist?container=" +
containerName +
"&casefolderID=" +
blob.name.split("/")[0] +
"/" +
blob.name.split("/")[1]
),
"filepath": buildDownloadBlobQueryPath({
containerName,
casefolderID: casefolderPath,
blobname: repFileName
}),
...buildHashMetadataPaths({
containerName,
casefolderID: casefolderPath,
blobname: repFileName
}),
"deletepath": buildDeleteBlobQueryPath({
containerName,
casefolderID: casefolderPath,
blobname: repFileName,
encodeCasefolderID: false,
encodeBlobname: false
})
});
}
//console.log("blobObj:", blobObj);
//consoleLogger("blobObj:", blobObj);
return blobObj;
};
@@ -1496,7 +1525,7 @@ export const createQueueSas = async (queueName) => {
var sas_signature_val = {
expiresOn: TEN_MINUTES_AFTER_NOW,
permissions: permission,
queueName: queueName,
queueName: queueName
};
const sasToken = generateQueueSASQueryParameters(
@@ -1540,7 +1569,7 @@ export const createCaseCompleteMessage = async (
caseReference +
"_case.json",
filespath:
endpointURL + "/" + containerName + "/" + caseReference + "/files",
endpointURL + "/" + containerName + "/" + caseReference + "/files"
// uploadUrl: fileRecord.uploadUrl,
// filename: fileRecord.file.name,
@@ -1601,7 +1630,7 @@ export const createRepCompleteMessage = async (
caseReference +
"/" +
filename +
"/files",
"/files"
};
console.log("======================", message, "=====================");
@@ -1627,7 +1656,7 @@ export const listContainers = async (
includeDeleted: false,
includeMetadata: true,
includeSystem: true,
prefix: containerNamePrefix,
prefix: containerNamePrefix
};
console.log(
"\n\n\n=================================================\n",
@@ -1670,7 +1699,7 @@ export const listBlobHierarchical = async (
includeTags: true, // include indexable tags
includeUncommitedBlobs: false, // include uncommitted blobs
includeVersions: false, // include all blob version
prefix: "", // filter by blob name prefix
prefix: "" // filter by blob name prefix
};
let i = 1;
@@ -1699,3 +1728,100 @@ export const listBlobHierarchical = async (
}
}
};
function formatTimeSince(date) {
if (!date) return "Unknown date";
const now = new Date();
const diffMs = now - date;
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays < 7) {
return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`;
} else {
const diffWeeks = Math.floor(diffDays / 7);
return `${diffWeeks} week${diffWeeks !== 1 ? "s" : ""} ago`;
}
}
function formatDateDisplay(date) {
return date
? `${date.toLocaleString("en-GB")} (${formatTimeSince(date)})`
: "Unknown";
}
export async function listBlobHierarchicalForUser(containerClient) {
const blobNames = [];
for await (const blob of containerClient.listBlobsFlat()) {
const lastModifiedDate = blob.properties?.lastModified || null;
const createdOnDate = blob.properties?.createdOn || null;
let displayDate = "";
if (createdOnDate && lastModifiedDate) {
if (
createdOnDate.toDateString() === lastModifiedDate.toDateString()
) {
// Same day — show just one
displayDate = `Date: ${formatDateDisplay(lastModifiedDate)}`;
} else {
// Different days — show both
displayDate = `Created: ${formatDateDisplay(
createdOnDate
)}, Modified: ${formatDateDisplay(lastModifiedDate)}`;
}
} else if (lastModifiedDate) {
displayDate = `Modified: ${formatDateDisplay(lastModifiedDate)}`;
} else if (createdOnDate) {
displayDate = `Created: ${formatDateDisplay(createdOnDate)}`;
} else {
displayDate = "Unknown date";
}
blobNames.push({
name: blob.name,
size: blob.properties.contentLength || 0,
lastModified: lastModifiedDate
? lastModifiedDate.toISOString()
: null,
createdOn: createdOnDate ? createdOnDate.toISOString() : null,
displayDate
});
}
return blobNames;
}
export async function listContainersForUser(
blobServiceClient,
containerNamePrefix,
emailAddress
) {
const options = {
includeDeleted: false,
includeMetadata: true,
includeSystem: true,
prefix: containerNamePrefix
};
const results = [];
for await (const containerItem of blobServiceClient.listContainers(
options
)) {
const containerClient = blobServiceClient.getContainerClient(
containerItem.name
);
const blobNames = await listBlobHierarchicalForUser(
containerClient
).catch(console.error);
if (blobNames && blobNames.length > 0) {
results.push({
container: containerItem.name,
blobs: blobNames
});
}
}
return results; // array of non-empty containers
}
+18
View File
@@ -0,0 +1,18 @@
# Actions Clients
This folder contains extracted client wrappers from `actions/index.js` as part of the Priority 1 refactor plan.
Current extracted clients:
- `relayClient` (shared hash-signing helper used by account/portal/document direct services)
- `endpointClient` (shared JSON request helpers for GET and generic axios config requests)
Current usage includes:
- relay hash-signing helper reuse across account/portal/document direct services
- shared endpoint JSON request helpers reused by admin/notify/integration/reference-data direct services
Notes:
- Core helper extraction and compatibility barrel support remain in place.
- Additional client extraction (`fileClient`, `notifyClient`) can be layered in incrementally without changing public exports from `actions/index.js`.
+11
View File
@@ -0,0 +1,11 @@
import axios from "axios";
export const getJson = async (url, config) => {
const response = await axios.get(url, config);
return response.data;
};
export const requestJson = async (config) => {
const response = await axios(config);
return response.data;
};
+22
View File
@@ -0,0 +1,22 @@
import { getJson, requestJson } from "./endpointClient";
import { getSignedJson, postSignedJson } from "./signedRequestClient";
export const getFileJson = (url) => {
return getJson(url);
};
export const getSignedFileJson = async (queryUrl) => {
return getSignedJson(queryUrl);
};
export const downloadFileBlob = (url) => {
return requestJson({
method: "get",
url,
responseType: "blob"
});
};
export const postSignedFileJson = async (queryUrl, data, config = {}) => {
return postSignedJson(queryUrl, data, config);
};
+37
View File
@@ -0,0 +1,37 @@
const encodeRouteParam = (value) => encodeURIComponent(String(value));
export const buildFileQuery = (path, params = {}, options = {}) => {
const { encode = false } = options;
const entries = Object.entries(params).filter(([, value]) => {
return value !== undefined && value !== null;
});
if (entries.length === 0) {
return path;
}
const query = entries
.map(([key, value]) => {
if (!encode) {
return `${key}=${String(value)}`;
}
return `${encodeRouteParam(key)}=${encodeRouteParam(value)}`;
})
.join("&");
return `${path}?${query}`;
};
export const withBaseUrl = (baseUrl, route) => {
return `${baseUrl}${route}`;
};
export const appendQuerySuffix = (route, suffix = "") => {
return `${route}${suffix}`;
};
export const appendHashSuffix = (route, hashBuilder) => {
return appendQuerySuffix(route, hashBuilder(route));
};
+5
View File
@@ -0,0 +1,5 @@
export * from "./relayClient";
export * from "./endpointClient";
export * from "./fileClient";
export * from "./fileRouteBuilder";
export * from "./signedRequestClient";
+22
View File
@@ -0,0 +1,22 @@
import axios from "axios";
import { hashAPIPath } from "../core/hash";
export const buildHashedQueryUrl = async (queryUrl) => {
try {
const signRes = await axios.get(
"/api/endpoint/gethash_api?path=" + encodeURIComponent(queryUrl)
);
if (!signRes?.data?.hash) {
throw new Error("Hash signature unavailable");
}
return queryUrl + signRes.data.hash;
} catch (error) {
if (typeof window === "undefined" && process.env.HASHKEY) {
return queryUrl + hashAPIPath(queryUrl);
}
throw error;
}
};
+45
View File
@@ -0,0 +1,45 @@
import { requestJson } from "./endpointClient";
import { buildHashedQueryUrl } from "./relayClient";
export const buildSignedUrl = async (queryUrl, config = {}) => {
const { baseUrl = "" } = config;
return `${baseUrl}${await buildHashedQueryUrl(queryUrl)}`;
};
const signedRequestJson = async ({ method, queryUrl, data, config = {} }) => {
const { baseUrl, ...requestConfig } = config;
const signedUrl = await buildSignedUrl(queryUrl, { baseUrl });
return requestJson({
method,
url: signedUrl,
data,
...requestConfig
});
};
export const getSignedJson = (queryUrl, config = {}) => {
return signedRequestJson({
method: "get",
queryUrl,
config
});
};
export const postSignedJson = (queryUrl, data, config = {}) => {
return signedRequestJson({
method: "post",
queryUrl,
data,
config
});
};
export const deleteSignedJson = (queryUrl, config = {}) => {
return signedRequestJson({
method: "delete",
queryUrl,
config
});
};
+15
View File
@@ -0,0 +1,15 @@
const port = parseInt(process.env.PORT, 10) || 3000;
export const BASE_URL =
typeof window !== "undefined"
? window.apiRoot
: process.env.API_ROOT || `http://localhost:${port}`;
export const API_PATH = "/api/data/v8.2/";
export const WEBAPI_URL =
process.env.RELAY_ROOT ||
"https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/";
export const ACCESS_TOKEN_ENDPOINT = process.env.ACCESS_TOKEN_ENDPOINT;
export const TENANT_ID = process.env.TENANT;
+11
View File
@@ -0,0 +1,11 @@
export const isNonEmptyString = (value) => {
return typeof value === "string" && value.trim().length > 0;
};
export const sanitizeString = (value) => {
return typeof value === "string" ? value.trim() : "";
};
export const escapeODataString = (value) => {
return sanitizeString(value).replace(/'/g, "''");
};
+22
View File
@@ -0,0 +1,22 @@
import CryptoJS from "crypto-js";
const WORDKEY = process.env.HASHKEY;
export const hashAPIPath = (queryPath) => {
var hashlink = CryptoJS.HmacSHA256(
queryPath,
CryptoJS.enc.Hex.parse(WORDKEY)
);
hashlink = hashlink.toString(CryptoJS.enc.Hex);
return (queryPath.indexOf("?") > -1 ? "&hash=" : "?hash=") + hashlink;
};
export const hashString = (stringToHash) => {
let hashedStr = CryptoJS.AES.encrypt(stringToHash, "pedw");
return hashedStr.toString();
};
export const dehashString = (stringToDeHash) => {
let dehashedStr = CryptoJS.AES.decrypt(decodeURI(stringToDeHash), "pedw");
return dehashedStr.toString(CryptoJS.enc.Utf8);
};
+50
View File
@@ -0,0 +1,50 @@
export const azureHeaders = (access_token) => {
return {
headers: {
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
"Accept": "application/json;odata.metadata=none",
"Prefer": 'odata.include-annotations="*",return=representation',
"Content-Type": "application/json",
"Authorization": "Bearer " + access_token
}
};
};
export const azureHeadersNoOdata = (access_token) => {
return {
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + access_token
}
};
};
export const azureHeadersPaged = (access_token) => {
return {
headers: {
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
"Accept": "application/json;odata.metadata=none",
"Prefer":
'odata.include-annotations="*",return=representation, odata.maxpagesize=10',
"Content-Type": "application/json",
"Authorization": "Bearer " + access_token
}
};
};
export const azureHeadersPagedCustom = (access_token, showNumberOfRecords) => {
return {
headers: {
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
"Accept": "application/json;odata.metadata=none",
"Prefer":
'odata.include-annotations="*",return=representation, odata.maxpagesize=' +
showNumberOfRecords,
"Content-Type": "application/json",
"Authorization": "Bearer " + access_token
}
};
};
+106
View File
@@ -0,0 +1,106 @@
import _ from "lodash";
const MASK = "[REDACTED]";
const redactString = (value) => {
if (typeof value !== "string") return value;
return value
.replace(
/([a-zA-Z0-9._%+-]{1,})@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g,
`${MASK}@${MASK}`
)
.replace(/Bearer\s+[A-Za-z0-9._\-]+/gi, `Bearer ${MASK}`)
.replace(/("client_secret"\s*:\s*")[^"]+("?)/gi, `$1${MASK}$2`)
.replace(/("access_token"\s*:\s*")[^"]+("?)/gi, `$1${MASK}$2`);
};
export const redactSensitive = (value) => {
if (value == null) return value;
if (typeof value === "string") {
return redactString(value);
}
try {
const json = JSON.stringify(value);
return redactString(json);
} catch (_error) {
return MASK;
}
};
export const getIP = (req) => {
const forwarded = req.headers["x-forwarded-for"];
const ip =
typeof forwarded === "string"
? forwarded.split(/, /)[0]
: req.socket.remoteAddress;
console.info(
"\n====================\n Client IP address: " + ip,
"\n=====================\n"
);
};
export const consoleLogger = (err) => {
console.log(
"\n\n/////////////////////////////////////////////////\nRaw Error " +
redactSensitive(err) +
"\n\n/////////////////////////////////////////////////\n"
);
var errStr =
"\n\n/////////////////////////////////////////////////\nServer Error " +
"\n" +
(_.has(err, "name") ? "Name: " + err.name + "\n" : "") +
(_.has(err, "code") ? "Code: " + err.code + "\n" : "") +
(_.has(err, "query") ? "Query: " + err.query + "\n" : "") +
(_.has(err, "message") ? "Message: " + err.message + "\n" : "") +
(_.has(err, "request.url")
? "request.url: " + err.request.url + "\n"
: "") +
(_.has(err, "response.status") ? "Status: " + err.code + "\n" : "") +
(_.has(err, "response.statusText")
? "\nStatusText: " + err.response.status + "\n"
: "") +
(_.has(err, "response.request.socket.remoteAddress")
? "\nRequest IP: " +
err.response.request.socket.remoteAddress +
"\n"
: "") +
(_.has(err, "response.data.error.message")
? "\nMessage: " +
redactSensitive(err.response.data.error.message) +
"\n"
: "") +
(_.has(err, "response.config.url")
? "\nRequest URL: " + redactSensitive(err.config.url) + "\n"
: "") +
(_.has(err, "response.headers.date")
? "\nRequest Time: " + err.response.headers.date + "\n"
: "") +
(_.has(err, "config.url")
? "\nAxios config url: " + redactSensitive(err.config.url) + "\n"
: "") +
(_.has(err, "config.url")
? "\nAxios message url: " + redactSensitive(err.message) + "\n"
: "") +
(_.has(err, "config.data")
? "\nRequest payload: " + redactSensitive(err.config.data) + "\n"
: "") +
"\n/////////////////////////////////////////////////\n";
console.log(errStr);
};
export const conLog = (err) => {
var errStr =
"\n\n/////////////////////////////////////////////////\nResponse: " +
"\n" +
err +
"\n/////////////////////////////////////////////////\n\n";
console.log(errStr);
return errStr;
};
+41
View File
@@ -0,0 +1,41 @@
import { requestJson } from "../clients/endpointClient";
import { ACCESS_TOKEN_ENDPOINT, TENANT_ID } from "./env";
import { consoleLogger } from "./logger";
const cache = {};
const tokenBody =
"grant_type=" +
process.env.GRANT_TYPE +
"&client_id=" +
process.env.CLIENT_ID +
"&client_secret=" +
process.env.CLIENT_SECRET +
"&scope=" +
"https://" +
process.env.RELAYURI +
"/.default";
const tokenConfig = {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Cache-Control": "no-cache"
}
};
export const getToken = async () => {
try {
const data = await requestJson({
method: "post",
url: `${ACCESS_TOKEN_ENDPOINT}${TENANT_ID}/oauth2/v2.0/token`,
data: tokenBody,
...tokenConfig
});
cache.tokenResponse = data;
return data;
} catch (error) {
consoleLogger(error);
return error;
}
};
+9 -2162
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -4,7 +4,7 @@ export const getSelectQuery = (appealTypeName) => {
return "&$select=pinswg_detailedeiascreeningrequired,pinswg_speacialistcaseprocess,modifiedon,_pinswg_appellant_value,pinswg_name,pinswg_siteaddresscounty,pinswg_siteaddressline1,pinswg_siteaddressline2,pinswg_siteaddresspostcode,pinswg_siteaddresstown,_pinswg_associatedlpa_value,pinswg_casedecisiondate,pinswg_dateeventrequested,pinswg_decision,pinswg_procedure,pinswg_questionnaireduedate,statuscode,pinswg_startdateoftheevent,pinswg_typeofevent,pinswg_startdate,pinswg_otherpartiesstatement,pinswg_finalcommentsduedate,pinswg_proofsofevidencewrittenstatementsofeviden,pinswg_speacialistcaseprocess,pinswg_statementduedate";
break;
case "pinswg_callinss77s":
return "&$select=pinswg_eiarequired,modifiedon,_pinswg_appellant_value,pinswg_name,pinswg_siteaddresscounty,pinswg_siteaddressline1,pinswg_siteaddressline2,pinswg_siteaddresspostcode,pinswg_siteaddresstown,_pinswg_associatedlpa_value,pinswg_casedecisiondate,pinswg_dateeventrequested,pinswg_decision,pinswg_finalcommentsduedate,pinswg_procedure,pinswg_proofsofevidencewrittenstatementsofeviden,pinswg_questionnaireduedate,pinswg_startdate,statuscode,pinswg_dateexportedtoiss,pinswg_startdateofevent,pinswg_typeofevent,pinswg_statementsduedate";
return "&$select=pinswg_eiarequired,modifiedon,_pinswg_appellant_value,pinswg_name,pinswg_siteaddresscounty,pinswg_siteaddressline1,pinswg_siteaddressline2,pinswg_siteaddresspostcode,pinswg_siteaddresstown,_pinswg_associatedlpa_value,pinswg_casedecisiondate,pinswg_dateeventrequested,pinswg_decision,pinswg_finalcommentsduedate,pinswg_procedure,pinswg_proofsofevidencewrittenstatementsofeviden,pinswg_questionnaireduedate,pinswg_startdate,statuscode,pinswg_dateexportedtoiss,pinswg_startdateofevent,pinswg_typeofevent,pinswg_statementsduedate,pinswg_otherpartiesstatement";
break;
case "pinswg_commonlands":
return "&$select=pinswg_detailedeiascreeningrequired,modifiedon,_pinswg_appellant_value,pinswg_name,pinswg_siteaddresscounty,pinswg_siteaddressline1,pinswg_siteaddressline2,pinswg_siteaddresspostcode,pinswg_siteaddresstown,_pinswg_associatedlpa_value,pinswg_casedecisiondate,pinswg_dateeventrequested,pinswg_decision,pinswg_finalcommentsduedate,pinswg_otherpartiesstatement,pinswg_procedure,pinswg_proofsofevidencewrittenstatementsofeviden,pinswg_questionnaireduedate,pinswg_relevantauthorityname,pinswg_startdate,statuscode,pinswg_starttimeoftheevent,pinswg_typeofevent,pinswg_statementsduedate";
@@ -60,8 +60,8 @@ export const getSelectQuery = (appealTypeName) => {
// process.env.RELAYPATH == "test-pedw-hc"
// );
return process.env.RELAYPATH == "dev-pedw-hc"
? "&$select=pinswg_eiarequired,modifiedon,_pinswg_appellant_value,pinswg_name,pinswg_siteaddressline1,pinswg_siteaddressline2,pinswg_siteaddresspostcode,pinswg_siteaddresstown,_pinswg_localplanningauthority_value,pinswg_casedecisiondate,pinswg_dicision,pinswg_dateeventrequested,pinswg_finalcommentsduedate,pinswg_procedure,pinswg_questionnaireduedate,pinswg_startdate,pinswg_statementduedate,statuscode,pinswg_startdatetimeiftheevent,pinswg_representation_period_end_date,pinswg_extend_representation_date,pinswg_typeofevent"
: "&$select=pinswg_eiarequired,modifiedon,_pinswg_appellant_value,pinswg_name,pinswg_siteaddressline1,pinswg_siteaddressline2,pinswg_siteaddresspostcode,pinswg_siteaddresstown,_pinswg_localplanningauthority_value,pinswg_casedecisiondate,pinswg_dicision,pinswg_dateeventrequested,pinswg_finalcommentsduedate,pinswg_procedure,pinswg_questionnaireduedate,pinswg_startdate,pinswg_statementduedate,statuscode,pinswg_startdatetimeiftheevent,pinswg_typeofevent";
? "&$select=pinswg_eiarequired,modifiedon,_pinswg_appellant_value,pinswg_name,pinswg_siteaddressline1,pinswg_siteaddressline2,pinswg_siteaddresspostcode,pinswg_siteaddresstown,pinswg_siteaddresscounty,_pinswg_localplanningauthority_value,pinswg_casedecisiondate,pinswg_dicision,pinswg_dateeventrequested,pinswg_finalcommentsduedate,pinswg_procedure,pinswg_questionnaireduedate,pinswg_startdate,pinswg_statementduedate,statuscode,pinswg_startdatetimeiftheevent,pinswg_representation_period_end_date,pinswg_extend_representation_date,pinswg_typeofevent"
: "&$select=pinswg_eiarequired,modifiedon,_pinswg_appellant_value,pinswg_name,pinswg_siteaddressline1,pinswg_siteaddressline2,pinswg_siteaddresspostcode,pinswg_siteaddresstown,pinswg_siteaddresscounty,_pinswg_localplanningauthority_value,pinswg_casedecisiondate,pinswg_dicision,pinswg_dateeventrequested,pinswg_finalcommentsduedate,pinswg_procedure,pinswg_questionnaireduedate,pinswg_startdate,pinswg_statementduedate,statuscode,pinswg_startdatetimeiftheevent,pinswg_typeofevent,pinswg_developmentdescription,pinswg_whyisinspectorneededonsite,pinswg_outlinehealthandsafetyissuesonsite,pinswg_additionalappealsmadedetails,pinswg_chosenprocedureexplanation,pinswg_dateofapplication,pinswg_lpaapplicationreference,pinswg_caseworkreason,pinswg_dateoflpadecision,pinswg_areaofsiteinhectaresdec,pinswg_floorspaceinsquaremeters,pinswg_ownershipcertificate,pinswg_siteviewablefromroad,pinswg_costappliedforindicator,pinswg_s106unilateralsubmitted";
break;
case "pinswg_planningconditionss73s79s":
return "&$select=pinswg_detailedeiascreeningrequiered,modifiedon,_pinswg_appellant_value,pinswg_name,pinswg_siteaddressline1,pinswg_siteaddressline2,pinswg_siteaddresspostcode,pinswg_siteaddresstown,_pinswg_associatedlpa_value,pinswg_casedecisiondate,pinswg_dateeventrequested,pinswg_decision,pinswg_finalcommentsduedate,pinswg_otherpartiesstatement,pinswg_procedure,pinswg_questionnaireduedate,pinswg_startdate,pinswg_statementduedate,statuscode,pinswg_startdateofevent,pinswg_typeofevent";
@@ -73,7 +73,7 @@ export const getSelectQuery = (appealTypeName) => {
return "&$select=pinswg_detailedeiascreeningrequired,pinswg_specialistcaseprocess,modifiedon,_pinswg_appellant_value,pinswg_name,pinswg_siteaddressline1,pinswg_siteaddressline2,pinswg_siteaddresspostcode,pinswg_siteaddresstown,_pinswg_associatedlpa_value,pinswg_casedecisiondate,pinswg_dateeventrequested,pinswg_decision,pinswg_finalcommentsduedate,pinswg_otherpartiesstatement,pinswg_procedure,pinswg_proofsofevidencewrittenstatementsofeviden,pinswg_questionnaireduedate,pinswg_relevantauthorityname,pinswg_startdate,statuscode,pinswg_startdateoftheevent,pinswg_typeofevent,pinswg_statementduedate,pinswg_specialistcaseprocess";
break;
case "pinswg_ldps":
return "&$select=pinswg_eiarequired,modifiedon,pinswg_name";
return "&$select=modifiedon,pinswg_name";
break;
case "pinswg_nonvalidations":
return "&$select=modifiedon,_pinswg_appellant_value,pinswg_name,pinswg_siteaddressline1,pinswg_siteaddressline2,pinswg_siteaddresspostcode,pinswg_siteaddresstown,_pinswg_associatedlpa_value,statuscode";
@@ -82,7 +82,7 @@ export const getSelectQuery = (appealTypeName) => {
return "&$select=pinswg_eiarequired,pinswg_anticipatedgridreferenceeastingtext,pinswg_anticipatedgridreferencenorthingtext,_pinswg_appellant_value,pinswg_name,pinswg_addressline1,pinswg_addressline2,pinswg_postcode,pinswg_addresstown,_pinswg_associatedlpa_value,pinswg_addresscounty,pinswg_applicationacceptedasvalid,pinswg_applicationrejectedasinvalid,pinswg_choiceofprocedure,pinswg_confirmrequesttovaryapplication,pinswg_dateeventrequested,pinswg_dateofdecision,pinswg_dateofrequesttovarytheapplication,pinswg_dateofsubmissionofapplication,pinswg_dateprojectpublishedonwebsite,pinswg_deadlineforsubmissionofapplication,pinswg_endofrepresentationperiod,pinswg_inspectorreportsubmittedtowelshgovernment,pinswg_procedureconfirmed,pinswg_projectdescription,pinswg_projectlocation,pinswg_recommendation,pinswg_rejectrequesttovaryapplication,pinswg_reportdueby,pinswg_suspensiondates,pinswg_suspensionenddates,pinswg_suspensionstartdate,pinswg_validnoticeofintentiontosubmitanapplicati,pinswg_webaddress,pinswg_withdrawndate,pinswg_writtenacceptanceofnotification,statuscode,pinswg_casestage,pinswg_startdateofevent,pinswg_typeofevent";
case "pinswg_sipses":
// return "&$select=pinswg_anticipatedgridreferenceeastingtext,pinswg_anticipatedgridreferencenorthingtext,_pinswg_appellant_value,pinswg_name,pinswg_addressline1,pinswg_addressline2,pinswg_postcode,pinswg_addresstown,_pinswg_associatedlpa_value,pinswg_addresscounty,pinswg_applicationacceptedasvalid,pinswg_applicationrejectedasinvalid,pinswg_choiceofprocedure,pinswg_confirmrequesttovaryapplication,pinswg_dateeventrequested,pinswg_dateofdecision,pinswg_dateofrequesttovarytheapplication,pinswg_dateofsubmissionofapplication,pinswg_dateprojectpublishedonwebsite,pinswg_deadlineforsubmissionofapplication,pinswg_decision,pinswg_endofrepresentationperiod,pinswg_inspectorreportsubmittedtowelshgovernment,pinswg_procedureconfirmed,pinswg_projectdescription,pinswg_projectlocation,pinswg_recommendation,pinswg_rejectrequesttovaryapplication,pinswg_reportdueby,pinswg_suspensiondates,pinswg_suspensionenddates,pinswg_suspensionstartdates,pinswg_validnoticeofintentiontosubmitanapplicati,pinswg_webaddress,pinswg_withdrawndate,pinswg_writtenacceptanceofnotification,statuscode,pinswg_casestage,_pinswg_dnsids_value,pinswg_startdateofevent,pinswg_typeofevent";
return "&$select=_pinswg_projecttype_value,pinswg_eiarequired,pinswg_anticipatedgridrefeasting,pinswg_anticipatedgridrefnorthing,_pinswg_appellant_value,pinswg_name,_pinswg_associatedlpa_value,pinswg_choiceofprocedure,pinswg_decision,pinswg_projectdescription,pinswg_projectlocation,pinswg_recommendation,statuscode,pinswg_casestage,_pinswg_sipscase_value,pinswg_writtenacceptanceofnotification,pinswg_validnoticeofintentiontosubmit,pinswg_deadlineforsubmissionofapplication,pinswg_screeningstartdate,pinswg_eiatargetdate,pinswg_targetextended,pinswg_directionissueddate,pinswg_eiarequired,pinswg_scopingstartdate,pinswg_scopingtargetdate,pinswg_scopingtargetextended,pinswg_scopingoutcomeissueddate,pinswg_essubmitted,pinswg_deemedmarinelicenseappliedfor,pinswg_validationdeadlineextension,pinswg_casesuspensionstartdate,pinswg_casesuspensionenddate,pinswg_consultationopen,pinswg_consultationclose,pinswg_reportissuedtowelshministers,pinswg_decisionissuedtoapplicant,pinswg_decisionoutcome";
return "&$select=pinswg_casedeadline,_pinswg_projecttype_value,pinswg_eiarequired,pinswg_anticipatedgridrefeasting,pinswg_anticipatedgridrefnorthing,_pinswg_appellant_value,pinswg_name,_pinswg_associatedlpa_value,pinswg_choiceofprocedure,pinswg_decision,pinswg_projectdescription,pinswg_projectlocation,pinswg_recommendation,statuscode,pinswg_casestage,_pinswg_sipscase_value,pinswg_writtenacceptanceofnotification,pinswg_validnoticeofintentiontosubmit,pinswg_deadlineforsubmissionofapplication,pinswg_screeningstartdate,pinswg_eiatargetdate,pinswg_targetextended,pinswg_directionissueddate,pinswg_eiarequired,pinswg_scopingstartdate,pinswg_scopingtargetdate,pinswg_scopingtargetextended,pinswg_scopingoutcomeissueddate,pinswg_essubmitted,pinswg_deemedmarinelicenseappliedfor,pinswg_validationdeadlineextension,pinswg_consultationopen,pinswg_consultationclose,pinswg_reportissuedtowelshministers,pinswg_decisionissuedtoapplicant,pinswg_decisionoutcome,pinswg_redetermination,pinswg_redeterminationconsultationdeadline";
break;
default:
return "";
+108
View File
@@ -0,0 +1,108 @@
import { BASE_URL } from "../core/env";
import { consoleLogger } from "../core/logger";
import { getJson, requestJson } from "../clients/endpointClient";
import { getSignedJson } from "../clients/signedRequestClient";
export const getPersonalAccount = (contactid) => {
return getJson(
BASE_URL + "/api/endpoint/getpersonalaccount_api?contactid=" + contactid
).catch((error) => {
consoleLogger(error);
});
};
export const getLogin = (emailAddress, pwd) => {
return getJson(
"/api/endpoint/getlogin_api?emailAddress=" +
emailAddress +
"&pwd=" +
pwd
).catch((error) => {
consoleLogger(error);
});
};
export const updatePassword = (contactId, newpassword) => {
var queryUrl = "/api/endpoint/updateaccount_api?contactId=" + contactId;
var data = { "pinswg_custom_password": newpassword };
var config = {
method: "post",
url: queryUrl,
data: data
};
return requestJson(config).catch((error) => {
consoleLogger(error);
});
};
export const updateAccount = async (contactId, updateBody, ssr) => {
var queryUrl =
(ssr ? BASE_URL : "") +
"/api/endpoint/updateaccount_api?contactId=" +
contactId;
var data = updateBody;
var config = {
method: "post",
url: queryUrl,
data: data
};
return requestJson(config).catch((error) => {
consoleLogger(error);
});
};
export const createAccount = (formValues) => {
var data = formValues;
var queryUrl = "/api/endpoint/createaccount_api";
var config = {
method: "post",
url: queryUrl,
data: data
};
return requestJson(config).catch((error) => {
consoleLogger(error);
});
};
export const getEmailAccountCheck = (emailAddress) => {
return getJson(
"/api/endpoint/getemailaccountcheck_api?emailAddress=" + emailAddress
).catch((error) => {
consoleLogger(error);
});
};
export const getPortalLogin = async (emailAddress) => {
var queryUrl =
"/api/endpoint/getportallogin_api?emailAddress=" + emailAddress;
return getSignedJson(queryUrl, { baseUrl: BASE_URL }).catch((error) => {
consoleLogger(error);
return JSON.stringify(error);
});
};
export const getPortalLoginProxy = async (emailAddress) => {
var queryUrl =
"/api/endpoint/getportalloginproxy_api?emailAddress=" + emailAddress;
return getJson(queryUrl).catch((error) => {
consoleLogger(error);
return JSON.stringify(error);
});
};
export const getPreferredLanguage = async (email) => {
return getJson(
BASE_URL +
"/api/endpoint/getpreferredlanguage_api?emailAddress=" +
email
).catch((error) => {
consoleLogger(error);
return error.response;
});
};
+23
View File
@@ -0,0 +1,23 @@
import {
getPersonalAccount,
getLogin,
updatePassword,
updateAccount,
createAccount,
getEmailAccountCheck,
getPortalLogin,
getPortalLoginProxy,
getPreferredLanguage
} from "./accountDirectService";
export {
getPersonalAccount,
getLogin,
updatePassword,
updateAccount,
createAccount,
getEmailAccountCheck,
getPortalLogin,
getPortalLoginProxy,
getPreferredLanguage
};
+67
View File
@@ -0,0 +1,67 @@
import { BASE_URL } from "../core/env";
import { logAndReturnResponse } from "./httpServiceUtils";
import { getJson } from "../clients/endpointClient";
export const getNewAppeals = async (searchString) => {
try {
return await getJson(BASE_URL + "/api/admin/getnewappeals_api");
} catch (error) {
return logAndReturnResponse(error);
}
};
export const getNewAppealsPage = async (
searchString,
pageNumber,
orderBy,
fieldSort,
showNumberOfRecords
) => {
try {
return await getJson(
"/api/admin/getnewappeals_api?searchString=" +
searchString +
"&pageNumber=" +
pageNumber +
"&orderby=" +
orderBy +
"&fieldSort=" +
fieldSort +
"&showNumberOfRecords=" +
showNumberOfRecords
);
} catch (error) {
return logAndReturnResponse(error);
}
};
export const getNewDocumentsPaged = async (
pageNumber,
orderBy,
fieldSort,
showNumberOfRecords,
documentType,
documentOrigin,
selectedWeeks
) => {
try {
return await getJson(
"/api/admin/getlatestdocuments_api?pageNumber=" +
pageNumber +
"&orderby=" +
orderBy +
"&fieldSort=" +
fieldSort +
"&showNumberOfRecords=" +
showNumberOfRecords +
"&documentType=" +
documentType +
"&numberWeeks=" +
selectedWeeks +
"&documentOrigin=" +
documentOrigin
);
} catch (error) {
return logAndReturnResponse(error);
}
};
+7
View File
@@ -0,0 +1,7 @@
import {
getNewAppeals,
getNewAppealsPage,
getNewDocumentsPaged
} from "./adminDirectService";
export { getNewAppeals, getNewAppealsPage, getNewDocumentsPaged };
+284
View File
@@ -0,0 +1,284 @@
import { BASE_URL } from "../core/env";
import { consoleLogger } from "../core/logger";
import { logAndReturnResponse } from "./httpServiceUtils";
import { getJson, requestJson } from "../clients/endpointClient";
import { buildFileQuery, withBaseUrl } from "../clients/fileRouteBuilder";
export const getCaseMessage = (searchString) => {
const route = buildFileQuery("/api/endpoint/getcasemessage_api", {
id: searchString
});
return getJson(withBaseUrl(BASE_URL, route)).catch(logAndReturnResponse);
};
export const getIncidentbyID = (searchString) => {
const route = buildFileQuery("/api/endpoint/getincidentbyid_api", {
searchString
});
return getJson(withBaseUrl(BASE_URL, route)).catch(logAndReturnResponse);
};
export const getIsPublishedbyID = (searchString) => {
const route = buildFileQuery("/api/endpoint/getispublishedbyid_api", {
searchString
});
return getJson(route).catch(logAndReturnResponse);
};
export const getPartSavedAppeal = (searchString) => {
const route = buildFileQuery("/api/endpoint/getpartsavedappeal_api", {
searchString
});
return getJson(withBaseUrl(BASE_URL, route)).catch(logAndReturnResponse);
};
export const getSIPSEvents = async (caseid) => {
const route = buildFileQuery("/api/endpoint/getsipsevents_api", {
caseid
});
return getJson(withBaseUrl(BASE_URL, route)).catch(logAndReturnResponse);
};
export const getSIPSMedia = async (caseid) => {
const route = buildFileQuery("/api/endpoint/getsipsmedia_api", {
caseid
});
return getJson(withBaseUrl(BASE_URL, route)).catch(logAndReturnResponse);
};
export const getAppealID = (
caseReference,
updateFormCollection,
primaryAttribute
) => {
const route = buildFileQuery("/api/endpoint/getappealid_api", {
updateFormCollection,
primaryAttribute,
caseReference
});
return getJson(route)
.then((data) => {
const result = Object.entries(data.value[0]).filter(
([key]) => !key.startsWith("_")
)[0][1];
var appealID = result;
return appealID;
})
.catch((error) => {
consoleLogger(error);
});
};
export const createNewCase = (
appealTypeId,
lpaID,
contactid,
createBody,
containerName
) => {
appealTypeId = parseInt(appealTypeId);
var data = createBody;
var queryUrl = buildFileQuery("/api/endpoint/createcase_api", {
appealTypeId,
lpaID,
contactid,
containername: containerName
});
var config = {
method: "post",
url: withBaseUrl(BASE_URL, queryUrl),
data: data
};
return requestJson(config).catch((error) => {
consoleLogger(error);
});
};
export const createNewCaseBlob = (
appealTypeId,
lpaID,
contactid,
createBody,
containerName,
lpaName
) => {
appealTypeId = parseInt(appealTypeId);
var data = createBody;
data.pinswg_lpaname = lpaName;
data.createdon = new Date();
var queryUrl = buildFileQuery("/api/file/createcase_api", {
appealTypeId,
lpaID,
contactid,
containername: containerName
});
var config = {
method: "post",
url: queryUrl,
data: data
};
return requestJson(config).catch((error) => {
consoleLogger(error);
});
};
export const updateCase = async (
incidentId,
updateBody,
updateFormCollection,
primaryAttribute,
caseReference
) => {
var data = updateBody;
var appealObj = await getAppealID(
caseReference,
updateFormCollection,
primaryAttribute
);
var queryUrl = buildFileQuery("/api/endpoint/updatecase_api", {
updateFormCollection,
appealObj
});
var config = {
method: "post",
url: queryUrl,
data: updateBody
};
return requestJson(config).catch((error) => {
consoleLogger(error);
});
};
export const updateCaseBlob = async (
incidentId,
updateBody,
updateFormCollection,
primaryAttribute,
caseReference
) => {
var data = updateBody;
var appealObj = await getAppealID(
caseReference,
updateFormCollection,
primaryAttribute
);
var queryUrl = buildFileQuery("/api/file/updatecase_api", {
updateFormCollection,
appealObj
});
var config = {
method: "post",
url: queryUrl,
data: updateBody
};
return requestJson(config).catch((error) => {
consoleLogger(error);
});
};
export const patchCase = async (incidentid) => {
var queryUrl = buildFileQuery("/api/endpoint/patchcase_api", {
incidentid
});
return getJson(queryUrl).catch((error) => {
//console.log("this error:", error);
});
};
export const getCase = (incidentID) => {
const route = buildFileQuery("/api/endpoint/getcase_api", {
incidentID
});
return getJson(withBaseUrl(BASE_URL, route)).catch((error) => {
consoleLogger(error);
});
};
export const getCaseByID = (incidentID) => {
const route = buildFileQuery("/api/endpoint/getcasebyid_api", {
incidentID
});
return getJson(withBaseUrl(BASE_URL, route)).catch((error) => {
consoleLogger(error);
});
};
export const getAppealPDFDocs = (incidentID) => {
const route = buildFileQuery("/api/endpoint/getappealpdfdocuments_api", {
incidentid: incidentID
});
return getJson(withBaseUrl(BASE_URL, route)).catch((error) => {
consoleLogger(error);
});
};
export const getAppealPDFDocument = async (incidentid) => {
const route = buildFileQuery("/api/endpoint/getappealpdfdocuments_api", {
incidentid
});
return getJson(withBaseUrl(BASE_URL, route)).catch((error) => {
consoleLogger(error);
return error.response;
});
};
export const getPortalModuleDetails = async (appealType, caseReference) => {
const route = buildFileQuery("/api/endpoint/getportalmoduledetails_api", {
appealType,
caseReference: encodeURI(caseReference)
});
return getJson(withBaseUrl(BASE_URL, route)).catch((error) => {
consoleLogger(error);
});
};
export const getPortalModuleDetailsProxy = (appealType, caseReference) => {
const route = buildFileQuery(
"/api/endpoint/getportalmoduledetailsproxy_api",
{
appealType,
caseReference: caseReference.replace(/\'/g, "''")
}
);
return getJson(route).catch((error) => {
consoleLogger(error);
});
};
export const getCaseStatus = async (appealType, caseReference) => {
const route = buildFileQuery("/api/endpoint/getStatusCounts_api", {});
return getJson(withBaseUrl(BASE_URL, route)).catch((error) => {
consoleLogger(error);
});
};
+43
View File
@@ -0,0 +1,43 @@
import {
getCaseMessage,
getIncidentbyID,
getSIPSEvents,
getSIPSMedia,
getAppealID,
createNewCase,
createNewCaseBlob,
updateCase,
updateCaseBlob,
patchCase,
getCase,
getCaseByID,
getAppealPDFDocs,
getAppealPDFDocument,
getPartSavedAppeal,
getIsPublishedbyID,
getPortalModuleDetails,
getPortalModuleDetailsProxy,
getCaseStatus
} from "./caseDirectService";
export {
getCaseMessage,
getIncidentbyID,
getSIPSEvents,
getSIPSMedia,
getAppealID,
createNewCase,
createNewCaseBlob,
updateCase,
updateCaseBlob,
patchCase,
getCase,
getCaseByID,
getAppealPDFDocs,
getAppealPDFDocument,
getPartSavedAppeal,
getIsPublishedbyID,
getPortalModuleDetails,
getPortalModuleDetailsProxy,
getCaseStatus
};
+394
View File
@@ -0,0 +1,394 @@
import { BASE_URL } from "../core/env";
import { consoleLogger } from "../core/logger";
import { hashAPIPath } from "../core/hash";
import {
buildFileQuery,
withBaseUrl,
appendQuerySuffix,
appendHashSuffix
} from "../clients/fileRouteBuilder";
import {
getFileJson,
getSignedFileJson,
downloadFileBlob,
postSignedFileJson
} from "../clients/fileClient";
const MAX_FILES_PER_UPLOAD_BATCH = 10;
export const getAwaitingSubmissionFromBlob = (containerName) => {
const route = buildFileQuery("/api/file/getawaitingsubmissionfromblob", {
container: containerName
});
return getFileJson(
withBaseUrl(BASE_URL, appendHashSuffix(route, hashAPIPath))
).catch((error) => {
consoleLogger(error);
});
};
export const getRepsFromBlob = (containerName) => {
const route = buildFileQuery("/api/file/getrepsblob", {
container: containerName
});
return getFileJson(
withBaseUrl(BASE_URL, appendHashSuffix(route, hashAPIPath))
).catch((error) => {
consoleLogger(error);
});
};
export const getRepsFromBlobProxy = async (containerName) => {
const route = buildFileQuery("/api/file/getrepsblobproxy", {
container: containerName
});
return getFileJson(route).catch((error) => {
consoleLogger(error);
});
};
export const getAwaitingSubmissionFromBlobProxy = async (containerName) => {
const route = buildFileQuery(
"/api/file/getawaitingsubmissionfromblobproxy",
{
container: containerName
}
);
return getFileJson(withBaseUrl(BASE_URL, route)).catch((error) => {
consoleLogger(error);
});
};
export const getFilesFromBlobproxy = (containerName, casefolderID) => {
const route = buildFileQuery("/api/file/getbloblistproxy", {
container: containerName,
casefolderID
});
return getFileJson(route).catch((error) => {
consoleLogger(error);
});
};
export const deleteAwaitingSubmissionsFromBlob = (
containerID,
casefolderID
) => {
var queryUrl = buildFileQuery("/api/file/deleteblobcase", {
container: containerID,
casefolderID
});
return getSignedFileJson(queryUrl).catch((error) => {
consoleLogger(error);
});
};
export const deleteMyRepresentationsFromBlob = (
containerID,
casefolderID,
repfile
) => {
var queryUrl = buildFileQuery("/api/file/deleteblobrep", {
container: containerID,
casefolderID,
repfile
});
return getSignedFileJson(queryUrl).catch((error) => {
consoleLogger(error);
});
};
export const uploadFiles = async (
formValues,
filesObj,
containerID,
casefolderID
) => {
let files = filesObj;
var formData = new FormData();
formData.append("appealData", JSON.stringify(formValues));
formData.append("containerID", containerID);
formData.append("casefolderID", casefolderID);
for (let i = 0; i < files.length; i++) {
for (let j = 0; j < files[i].length; j++) {
formData.append(files[i][j].name, files[i][j]);
}
}
var queryUrl = "/api/file/upload";
try {
return await postSignedFileJson(queryUrl, formData);
} catch (error) {
consoleLogger(error);
}
};
export const uploadSingleFile = async (
filesObj,
containerID,
casefolderID,
options = {}
) => {
let files = filesObj;
try {
const queryUrl = "/api/file/uploadsinglefile";
const uploadResponse = {
data: [],
invalidFiles: []
};
let cumulativeUploaded = 0;
let cumulativeInvalid = 0;
const totalFiles = files.length;
const totalChunks = Math.ceil(totalFiles / MAX_FILES_PER_UPLOAD_BATCH);
for (
let index = 0;
index < files.length;
index += MAX_FILES_PER_UPLOAD_BATCH
) {
const chunkIndex =
Math.floor(index / MAX_FILES_PER_UPLOAD_BATCH) + 1;
const fileChunk = files.slice(
index,
index + MAX_FILES_PER_UPLOAD_BATCH
);
const formData = new FormData();
formData.append("containerID", containerID);
formData.append("casefolderID", casefolderID);
for (let i = 0; i < fileChunk.length; i++) {
formData.append(fileChunk[i].name, fileChunk[i]);
}
const chunkResult = await postSignedFileJson(queryUrl, formData);
if (Array.isArray(chunkResult?.data)) {
uploadResponse.data.push(...chunkResult.data);
cumulativeUploaded += chunkResult.data.length;
}
if (Array.isArray(chunkResult?.invalidFiles)) {
uploadResponse.invalidFiles.push(...chunkResult.invalidFiles);
cumulativeInvalid += chunkResult.invalidFiles.length;
}
if (typeof options.onChunkComplete === "function") {
options.onChunkComplete({
chunkIndex,
totalChunks,
totalFiles,
cumulativeUploaded,
cumulativeInvalid,
processedFiles: cumulativeUploaded + cumulativeInvalid
});
}
}
return uploadResponse;
} catch (error) {
consoleLogger(error);
}
};
export const uploadRepFiles = async (
formValues,
filesObj,
containerID,
casefolderID
) => {
var formData = new FormData();
formData.append("appealData", JSON.stringify(formValues));
formData.append("containerID", containerID);
formData.append("casefolderID", casefolderID);
formData.append("repOrAppeal", true);
var queryUrl = "/api/file/upload";
try {
return await postSignedFileJson(queryUrl, formData);
} catch (error) {
consoleLogger(error);
}
};
export const generateRepPDF = async (
formValues,
containerID,
casefolderID,
options = {}
) => {
var queryUrl =
"/api/file/generatepdf" + (options.download ? "?download=true" : "");
formValues.containerID = containerID;
try {
return await postSignedFileJson(queryUrl, formValues, {
...(options.download ? { responseType: "blob" } : {})
});
} catch (error) {
consoleLogger(error);
}
};
export const generateAppealPDF = async (
formValues,
containerID,
casefolderID,
appealType,
fileList,
options = {}
) => {
Object.assign(formValues, {
"filesList": fileList
});
var queryUrl =
"/api/file/generateappealpdf?appealType=" +
appealType +
(options.download ? "&download=true" : "");
try {
return await postSignedFileJson(queryUrl, formValues, {
...(options.download ? { responseType: "blob" } : {})
});
} catch (error) {
consoleLogger(error);
}
};
export const getFilesFromBlob = (containerName, casefolderID) => {
const route = buildFileQuery("/api/file/getbloblist", {
container: containerName,
casefolderID
});
return getFileJson(
withBaseUrl(BASE_URL, appendHashSuffix(route, hashAPIPath))
).catch((error) => {
consoleLogger(error);
});
};
export const getFilesFromBlobHashed = (
containerName,
getblobshash,
casefolderID
) => {
const route = buildFileQuery("/api/file/getbloblist", {
container: containerName,
casefolderID
});
return getFileJson(appendQuerySuffix(route, getblobshash)).catch(
(error) => {
consoleLogger(error);
}
);
};
export const deleteBlob = async (
containerName,
blobName,
deleteblobhash,
casefolderID
) => {
const route = buildFileQuery(
"/api/file/deleteblob",
{
container: containerName,
casefolderID,
blobname: blobName
},
{
encode: true
}
);
return getFileJson(appendQuerySuffix(route, deleteblobhash)).catch(
(error) => {
consoleLogger(error);
}
);
};
export const deleteRepBlob = async (
containerName,
blobName,
deleteblobhash,
casefolderID,
filenamePrefix
) => {
const route = buildFileQuery(
"/api/file/deleteblob",
{
container: containerName,
casefolderID: casefolderID + "/" + filenamePrefix,
blobname: blobName
},
{
encode: true
}
);
return getFileJson(appendQuerySuffix(route, deleteblobhash)).catch(
(error) => {
consoleLogger(error);
}
);
};
export const downloadBlob = (containerName, blobName) => {
const queryUrl = withBaseUrl(
BASE_URL,
buildFileQuery("/api/file/downloadblob", {
container: containerName.toLowerCase(),
blobname: blobName
})
);
return downloadFileBlob(queryUrl).catch((error) => {
consoleLogger(error);
});
};
export const getProgressFromBlob = async (containerName, casereference) => {
const route = buildFileQuery(
"/api/file/getprogressobjblob",
{
container: containerName,
casefolderID: casereference
},
{
encode: true
}
);
return getFileJson(
withBaseUrl(BASE_URL, appendHashSuffix(route, hashAPIPath))
).catch((error) => {
consoleLogger(error);
});
};
export const createContainerProxy = (containerName) => {
var route = buildFileQuery("/api/file/setupcontainer", {
ident: containerName
});
var queryUrl = appendHashSuffix(route, hashAPIPath);
return getFileJson(withBaseUrl(BASE_URL, queryUrl)).catch((error) => {
consoleLogger(error);
return JSON.stringify(error);
});
};
+43
View File
@@ -0,0 +1,43 @@
import {
getAwaitingSubmissionFromBlob,
getRepsFromBlob,
getRepsFromBlobProxy,
getAwaitingSubmissionFromBlobProxy,
deleteAwaitingSubmissionsFromBlob,
deleteMyRepresentationsFromBlob,
uploadFiles,
uploadSingleFile,
uploadRepFiles,
generateRepPDF,
generateAppealPDF,
getFilesFromBlob,
getFilesFromBlobproxy,
getFilesFromBlobHashed,
deleteBlob,
deleteRepBlob,
downloadBlob,
getProgressFromBlob,
createContainerProxy
} from "./documentDirectService";
export {
getAwaitingSubmissionFromBlob,
getRepsFromBlob,
getRepsFromBlobProxy,
getAwaitingSubmissionFromBlobProxy,
deleteAwaitingSubmissionsFromBlob,
deleteMyRepresentationsFromBlob,
uploadFiles,
uploadSingleFile,
uploadRepFiles,
generateRepPDF,
generateAppealPDF,
getFilesFromBlob,
getFilesFromBlobproxy,
getFilesFromBlobHashed,
deleteBlob,
deleteRepBlob,
downloadBlob,
getProgressFromBlob,
createContainerProxy
};
+19
View File
@@ -0,0 +1,19 @@
import { consoleLogger } from "../core/logger";
export const buildEmptyValueErrorResponse = (error) => {
return {
value: [],
errorCode: error.response.status,
errorMsg: error.response.statusText
};
};
export const logAndReturnResponse = (error) => {
consoleLogger(error);
return error.response;
};
export const logAndReturnEmptyValueErrorResponse = (error) => {
consoleLogger(error);
return buildEmptyValueErrorResponse(error);
};
+9
View File
@@ -0,0 +1,9 @@
export * from "./searchService";
export * from "./caseService";
export * from "./accountService";
export * from "./portalService";
export * from "./documentService";
export * from "./referenceDataService";
export * from "./notifyService";
export * from "./adminService";
export * from "./integrationService";
@@ -0,0 +1,19 @@
import { consoleLogger } from "../core/logger";
import { requestJson } from "../clients/endpointClient";
export const createCRMTask = async (formValues) => {
var queryUrl = "/api/endpoint/createcrmtask_api";
var data = formValues;
var config = {
method: "post",
url: queryUrl,
data: data
};
try {
return await requestJson(config);
} catch (error) {
consoleLogger(error);
}
};
+3
View File
@@ -0,0 +1,3 @@
import { createCRMTask } from "./integrationDirectService";
export { createCRMTask };
+27
View File
@@ -0,0 +1,27 @@
import { consoleLogger } from "../core/logger";
import { requestJson } from "../clients/endpointClient";
export const sendEmail = async (
templateId,
emailAddress,
personalisation,
reference
) => {
var mailData = {
"templateId": templateId,
"emailAddress": emailAddress,
"reference": reference,
"personalisation": personalisation
};
try {
return await requestJson({
method: "post",
url: "/api/email/notify",
data: mailData
});
} catch (error) {
consoleLogger(error);
throw error;
}
};
+3
View File
@@ -0,0 +1,3 @@
import { sendEmail } from "./notifyDirectService";
export { sendEmail };
+313
View File
@@ -0,0 +1,313 @@
import { BASE_URL } from "../core/env";
import { consoleLogger } from "../core/logger";
import { getJson, requestJson } from "../clients/endpointClient";
import {
deleteSignedJson,
buildSignedUrl
} from "../clients/signedRequestClient";
import {
buildFileQuery,
withBaseUrl,
appendQuerySuffix
} from "../clients/fileRouteBuilder";
export const getMyCases = (loggedInUserId) => {
const route = buildFileQuery("/api/endpoint/getmycases_api", {
loggedInUserId
});
return getJson(withBaseUrl(BASE_URL, route)).catch((error) => {
consoleLogger(error);
});
};
export const getMyInvolvements = async (loggedInUserId) => {
const route = buildFileQuery("/api/endpoint/getmyinvolvements_api", {
loggedInUserId
});
return getJson(withBaseUrl(BASE_URL, route)).catch((error) => {
consoleLogger(error);
});
};
export const getMyLPACases = (lpaid) => {
const route = buildFileQuery("/api/endpoint/getmylpacases_api", {
lpaid
});
return getJson(withBaseUrl(BASE_URL, route)).catch((error) => {
consoleLogger(error);
});
};
export const getMyRepresentations = (loggedInUserId) => {
const route = buildFileQuery("/api/endpoint/getmyrepresentations_api", {
loggedInUserId
});
return getJson(withBaseUrl(BASE_URL, route)).catch((error) => {
consoleLogger(error);
});
};
export const getMyRepresentationsProxy = (loggedInUserId) => {
const route = buildFileQuery(
"/api/endpoint/getmyrepresentationsproxy_api",
{
loggedInUserId
}
);
return getJson(route).catch((error) => {
consoleLogger(error);
});
};
export const getRepresentations = (incidentID) => {
const route = buildFileQuery("/api/endpoint/getrepresentations_api", {
incidentID
});
return getJson(route).catch((error) => {
consoleLogger(error);
});
};
export const getRepresentationsProxy = (incidentID) => {
const route = buildFileQuery("/api/endpoint/getrepresentationsproxy_api", {
incidentID
});
return getJson(route).catch((error) => {
consoleLogger(error);
});
};
export const getWatchedCases = (loggedInUserId) => {
const route = buildFileQuery("/api/endpoint/getwatchedcases_api", {
loggedInUserId
});
return getJson(withBaseUrl(BASE_URL, route)).catch((error) => {
consoleLogger(error);
});
};
export const getWatchedCasesProxy = (loggedInUserId) => {
const route = buildFileQuery("/api/endpoint/getwatchedcasesproxy_api", {
loggedInUserId
});
return getJson(route).catch((error) => {
consoleLogger(error);
});
};
export const getAwaitingSubmissionProxy = (loggedInUserId) => {
const route = buildFileQuery(
"/api/endpoint/getawaitingsubmissionproxy_api",
{
loggedInUserId
}
);
return getJson(route).catch((error) => {
consoleLogger(error);
});
};
export const getAwaitingSubmission = (loggedInUserId) => {
const route = buildFileQuery("/api/endpoint/getawaitingsubmission_api", {
loggedInUserId
});
return getJson(withBaseUrl(BASE_URL, route)).catch((error) => {
consoleLogger(error);
});
};
export const createWatchedCases = async (formValues) => {
var data = formValues;
var queryUrl = buildFileQuery("/api/endpoint/createwatchedcases_api");
var config = {
method: "post",
url: queryUrl,
data: data
};
try {
return await requestJson(config);
} catch (error) {
consoleLogger(error);
}
};
export const deleteMyRepresentations = (myRepresentationsID) => {
var queryUrl = buildFileQuery("/api/endpoint/deletemyrepresentations_api", {
myRepresentationsID
});
return deleteSignedJson(queryUrl, {
headers: {
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
"Accept": "application/json;odata.metadata=none",
"Prefer": 'odata.include-annotations="*",return=representation',
"Content-Type": "application/json"
}
}).catch((error) => {
consoleLogger(error);
});
};
export const deleteAwaitingSubmissions = (incidentID) => {
var queryUrl = buildFileQuery(
"/api/endpoint/deleteawaitingsubmissions_api",
{
incidentID
}
);
var config = {
method: "delete",
url: queryUrl
};
return requestJson(config).catch((error) => {
consoleLogger(error);
});
};
export const deleteWatchedCases = async (watchedCaseID) => {
var queryUrl = buildFileQuery("/api/endpoint/deletewatchedcases_api", {
watchedCaseID
});
return deleteSignedJson(queryUrl).catch((error) => {
consoleLogger(error);
});
};
export const sendCaseCompleteMessage = async (
containerID,
caseReference,
inv
) => {
var hashQueryPath = buildFileQuery(
"/api/file/createappealcompletemessage_api",
{
container: containerID,
tempcaseref: caseReference
}
);
var queryUrl = buildFileQuery("/api/file/createappealcompletemessage_api", {
container: containerID,
tempcaseref: caseReference,
inv
});
var signedQueryUrl = await buildSignedUrl(hashQueryPath);
queryUrl = appendQuerySuffix(
queryUrl,
signedQueryUrl.replace(hashQueryPath, "")
);
var config = {
method: "get",
url: queryUrl
};
return requestJson(config).catch((error) => {
consoleLogger(error);
});
};
export const sendCaseCompleteMessageProxy = async (
containerID,
caseReference
) => {
var queryUrl = buildFileQuery(
"/api/file/createappealcompletemessageproxy_api",
{
container: containerID,
tempcaseref: caseReference
}
);
var config = {
method: "get",
url: queryUrl
};
return requestJson(config).catch((error) => {
consoleLogger(error);
});
};
export const sendRepCompleteMessage = async (
containerID,
caseReference,
fileName
) => {
var hashQueryPath = buildFileQuery(
"/api/file/createrepcompletemessage_api",
{
container: containerID,
tempcaseref: caseReference,
repid: fileName
}
);
var queryUrl = await buildSignedUrl(hashQueryPath);
var config = {
method: "get",
url: queryUrl
};
return requestJson(config).catch((error) => {
consoleLogger(error);
});
};
export const setRepInvolvment = async (caseid, contactid, involvement) => {
var queryUrl = buildFileQuery("/api/file/createrepinvolvement_api");
var data = {
"incidentid": caseid,
"contactid": contactid,
"involvement": involvement
};
var config = {
method: "post",
url: queryUrl,
data: data
};
try {
return await requestJson(config);
} catch (error) {
consoleLogger(error);
}
};
export const setCaseInvolvment = async (caseid, contactid) => {
var queryUrl = buildFileQuery("/api/file/createrepinvolvement_api");
var data = { "incidentid": caseid, "contactid": contactid };
var config = {
method: "post",
url: queryUrl,
data: data
};
try {
return await requestJson(config);
} catch (error) {
consoleLogger(error);
}
};
+45
View File
@@ -0,0 +1,45 @@
import {
getMyCases,
getMyInvolvements,
getMyLPACases,
getMyRepresentations,
getMyRepresentationsProxy,
getRepresentations,
getRepresentationsProxy,
getWatchedCases,
getWatchedCasesProxy,
getAwaitingSubmission,
getAwaitingSubmissionProxy,
createWatchedCases,
deleteWatchedCases,
deleteMyRepresentations,
deleteAwaitingSubmissions,
sendCaseCompleteMessage,
sendCaseCompleteMessageProxy,
sendRepCompleteMessage,
setRepInvolvment,
setCaseInvolvment
} from "./portalDirectService";
export {
getMyCases,
getMyInvolvements,
getMyLPACases,
getMyRepresentations,
getMyRepresentationsProxy,
getRepresentations,
getRepresentationsProxy,
getWatchedCases,
getWatchedCasesProxy,
getAwaitingSubmission,
getAwaitingSubmissionProxy,
createWatchedCases,
deleteWatchedCases,
deleteMyRepresentations,
deleteAwaitingSubmissions,
sendCaseCompleteMessage,
sendCaseCompleteMessageProxy,
sendRepCompleteMessage,
setRepInvolvment,
setCaseInvolvment
};
@@ -0,0 +1,58 @@
import { BASE_URL } from "../core/env";
import { consoleLogger } from "../core/logger";
import { logAndReturnEmptyValueErrorResponse } from "./httpServiceUtils";
import { getJson } from "../clients/endpointClient";
export const getAppealsTypes = () => {
return getJson(BASE_URL + "/api/endpoint/getappealtypes_api").catch(
logAndReturnEmptyValueErrorResponse
);
};
export const getProjectTypes = () => {
return getJson(BASE_URL + "/api/endpoint/getprojecttypes_api").catch(
logAndReturnEmptyValueErrorResponse
);
};
export const getAppealsTypesForNewAppeal = () => {
return getJson(
BASE_URL + "/api/endpoint/getappealtypesfornewappeal_api"
).catch(logAndReturnEmptyValueErrorResponse);
};
export const getLPA = () => {
return getJson(BASE_URL + "/api/endpoint/getlpa_api").catch(
logAndReturnEmptyValueErrorResponse
);
};
export const getFormData = (whichForm) => {
return getJson(
BASE_URL + "/api/endpoint/getformdata_api?whichForm=" + whichForm
).catch((error) => {
consoleLogger(error);
});
};
export const getMandatoryFields = (whichForm) => {
return getJson(
BASE_URL + "/api/endpoint/getmandatoryfields_api?whichForm=" + whichForm
).catch((error) => {
consoleLogger(error);
});
};
export const getPickLists = (whichForm) => {
return getJson(
BASE_URL + "/api/endpoint/getpicklists_api?whichForm=" + whichForm
).catch((error) => {
consoleLogger(error);
});
};
export const getNotice = () => {
return getJson(BASE_URL + "/api/notices").catch((error) => {
consoleLogger(error);
});
};
+21
View File
@@ -0,0 +1,21 @@
import {
getAppealsTypes,
getProjectTypes,
getAppealsTypesForNewAppeal,
getLPA,
getFormData,
getMandatoryFields,
getPickLists,
getNotice
} from "./referenceDataDirectService";
export {
getAppealsTypes,
getProjectTypes,
getAppealsTypesForNewAppeal,
getLPA,
getFormData,
getMandatoryFields,
getPickLists,
getNotice
};
+261
View File
@@ -0,0 +1,261 @@
import { BASE_URL } from "../core/env";
import { consoleLogger } from "../core/logger";
import { getJson } from "../clients/endpointClient";
import {
logAndReturnResponse,
logAndReturnEmptyValueErrorResponse
} from "./httpServiceUtils";
export const getBasicSearch = (searchString) => {
return getJson(
BASE_URL +
"/api/endpoint/getbasicsearch_api?searchString=" +
searchString
).catch(logAndReturnResponse);
};
export const getBasicDNSURLSearch = (searchString) => {
return getJson(
BASE_URL +
"/api/endpoint/getbasicdnsurlsearch_api?searchString=" +
searchString
).catch(logAndReturnResponse);
};
export const getAddressSearchPaged = (
searchString,
pageNumber,
orderBy,
fieldSort,
showNumberOfRecords
) => {
return getJson(
"/api/endpoint/getbasicsearch_by_address_paged_api?searchString=" +
searchString +
"&pageNumber=" +
pageNumber +
"&orderby=" +
orderBy +
"&fieldSort=" +
fieldSort +
"&showNumberOfRecords=" +
showNumberOfRecords
).catch(logAndReturnResponse);
};
export const getBasicSearchPaged = (
searchString,
pageNumber,
orderBy,
fieldSort,
showNumberOfRecords
) => {
return getJson(
"/api/endpoint/getbasicsearchpaged_api?searchString=" +
searchString +
"&pageNumber=" +
pageNumber +
"&orderby=" +
orderBy +
"&fieldSort=" +
fieldSort +
"&showNumberOfRecords=" +
showNumberOfRecords
).catch(logAndReturnResponse);
};
export const getDNSCoords = () => {
return getJson(BASE_URL + "/api/endpoint/getdnscoords_api").catch(
logAndReturnEmptyValueErrorResponse
);
};
export const getDNSList = (searchString) => {
return getJson(BASE_URL + "/api/endpoint/getdnslist_api").catch(
logAndReturnResponse
);
};
export const getBasicDNSSearch = (searchString) => {
return getJson(BASE_URL + "/api/endpoint/getbasicdnssearch_api").catch(
logAndReturnResponse
);
};
export const getBasicDNSSearchPaged = (
pageNumber,
orderBy,
fieldSort,
showNumberOfRecords
) => {
return getJson(
"/api/endpoint/getbasicdnssearchpaged_api?pageNumber=" +
pageNumber +
"&orderby=" +
orderBy +
"&fieldSort=" +
fieldSort +
"&showNumberOfRecords=" +
showNumberOfRecords
).catch(logAndReturnResponse);
};
export const getAdvancedSearch = (searchString) => {
return getJson(
BASE_URL +
"/api/endpoint/getadvancedsearch_api?searchstring=" +
JSON.stringify(searchString)
).catch(logAndReturnEmptyValueErrorResponse);
};
export const getAdvancedSearchPaged = (
searchString,
pageNumber,
orderBy,
fieldSort,
showNumberOfRecords
) => {
return getJson(
"/api/endpoint/getadvancedsearchpaged_api?searchstring=" +
JSON.stringify(searchString) +
"&pageNumber=" +
pageNumber +
"&orderby=" +
orderBy +
"&fieldSort=" +
fieldSort +
"&showNumberOfRecords=" +
showNumberOfRecords
).catch(logAndReturnEmptyValueErrorResponse);
};
export const getBasicSearchDetails = async (
appealTypeName,
caseReference,
primaryIdAttribute,
incidentID
) => {
return getJson(
BASE_URL +
"/api/endpoint/getbasicsearchdetails_api?appealTypeName=" +
appealTypeName +
"&primaryIdAttribute=" +
primaryIdAttribute +
"&incidentID=" +
incidentID
).catch((error) => {
consoleLogger(error);
});
};
export const getBasicPartSavedDetails = (
appealTypeName,
caseReference,
primaryIdAttribute,
incidentID
) => {
return getJson(
BASE_URL +
"/api/endpoint/getbasicpartsaveddetails_api?appealTypeName=" +
appealTypeName +
"&primaryIdAttribute=" +
primaryIdAttribute +
"&incidentID=" +
incidentID
).catch((error) => {
consoleLogger(error);
});
};
export const getBasicSearchDetailsPaged = async (
appealTypeName,
caseReference,
primaryIdAttribute,
incidentIDs
) => {
if (!incidentIDs || incidentIDs.length === 0) return [];
const filter = incidentIDs
.map((id) => {
const suffix =
primaryIdAttribute === "pinswg_sipscase"
? `_${primaryIdAttribute}_value`
: `_${primaryIdAttribute}s_value`;
return `${suffix} eq ${id}`;
})
.join(" or ");
const url = `/api/endpoint/getbasicsearchdetailspaged_api?appealTypeName=${appealTypeName}&primaryIdAttribute=${primaryIdAttribute}&incidentID=${encodeURIComponent(
filter
)}`;
try {
const data = await getJson(url);
return data.value || [];
} catch (error) {
consoleLogger(error);
return [];
}
};
export const getSearchDocumentDetails = (incidentID) => {
return getJson(
"/api/endpoint/getsearchdocumentdetails_api?incidentid=" + incidentID
).catch((error) => {
consoleLogger(error);
throw error;
});
};
export const getSearchDocumentTypes = (incidentID) => {
return getJson(
"/api/endpoint/getsearchdocumentTypes_api?incidentid=" + incidentID
).catch((error) => {
consoleLogger(error);
});
};
export const getSearchDocumentDetailsPaged = (
incidentID,
pageNumber,
orderBy,
fieldSort,
showNumberOfRecords,
documentType
) => {
return getJson(
"/api/endpoint/getsearchdocumentdetailspaged_api?incidentid=" +
incidentID +
"&pageNumber=" +
pageNumber +
"&orderby=" +
orderBy +
"&fieldSort=" +
fieldSort +
"&showNumberOfRecords=" +
showNumberOfRecords +
"&documentType=" +
documentType
).catch((error) => {
consoleLogger(error);
});
};
export const getLinkedCases = (parentIncidentid) => {
return getJson(
"/api/endpoint/getlinkedcases_api?parentincidentid=" + parentIncidentid
).catch((error) => {
consoleLogger(error);
});
};
export const getAddressSearch = async (searchString) => {
const params = new URLSearchParams(searchString);
const str = params.toString();
var queryUrl =
BASE_URL + "/api/endpoint/getbasicsearch_by_address_api?" + str;
return getJson(queryUrl).catch(logAndReturnEmptyValueErrorResponse);
};
+41
View File
@@ -0,0 +1,41 @@
import {
getBasicSearch,
getAddressSearch,
getAddressSearchPaged,
getBasicSearchPaged,
getAdvancedSearch,
getAdvancedSearchPaged,
getBasicDNSURLSearch,
getBasicDNSSearch,
getBasicDNSSearchPaged,
getDNSCoords,
getDNSList,
getBasicSearchDetails,
getBasicSearchDetailsPaged,
getBasicPartSavedDetails,
getSearchDocumentDetails,
getSearchDocumentTypes,
getSearchDocumentDetailsPaged,
getLinkedCases
} from "./searchDirectService";
export {
getBasicSearch,
getAddressSearch,
getAddressSearchPaged,
getBasicSearchPaged,
getAdvancedSearch,
getAdvancedSearchPaged,
getBasicDNSURLSearch,
getBasicDNSSearch,
getBasicDNSSearchPaged,
getDNSCoords,
getDNSList,
getBasicSearchDetails,
getBasicSearchDetailsPaged,
getBasicPartSavedDetails,
getSearchDocumentDetails,
getSearchDocumentTypes,
getSearchDocumentDetailsPaged,
getLinkedCases
};
+57
View File
@@ -0,0 +1,57 @@
# Refactor Session Handoff (Usable Prompt)
## Purpose
Copy/paste this directly into a new chat session to continue the current PEDW refactor stream with the right context and next-step planning.
## Copy/Paste Prompt
```
You are continuing an in-flight refactor stream in the PEDW FrontEnd repository.
Before any analysis or code changes:
1) Ask for the Azure DevOps work item ID (required).
2) Create a new branch from `origin/SIPS-Development` prefixed with that ID (e.g. `US12345-progress-update` or `TASK67890-progress-update`).
3) Confirm branch name, then continue.
Follow this order before implementing:
1) Read `.clinerules/default-rules.md` and `.clinerules/CONTRIBUTING_AI.md`
2) Read `GUARDRAILS.md`
3) Read current-state docs:
- `context/architecture.md`
- `context/current-state-scorecard.md`
- `context/next-work-plan-sequence-b.md`
- `context/test-coverage-map.md`
4) Read memory context:
- `memory-bank/change-log.md` (latest entries)
- `memory-bank/debt-list.md`
- `memory-bank/architect-review.md`
- `memory-bank/decisions.md`
- `memory-bank/open-questions.md`
Then provide (based on what you read):
1. Current state summary (what is done, what remains)
2. Recommended next slice from this exact shortlist (pick one):
- Slice B1.1: create shared signed-request helper(s) in `actions/clients/` and migrate one pilot path in `actions/services/portalDirectService.js`
- Slice B1.2: migrate remaining signed flows in `actions/services/documentDirectService.js` to the shared signed helper(s)
- Slice B2.1: auth logging hardening in `pages/api/auth/[...nextauth].js` (redacted structured logs, no behavior change)
- Slice B2.2: file/email logging hardening in selected `pages/api/file/**` and `pages/api/email/**`
3. Exact files to change
4. Validation plan (lint + targeted tests + EN/CY parity + negative-path checks)
5. Rollback plan
Constraints:
- Preserve behavior unless explicitly requested otherwise
- Do not weaken auth/session/CSP/security flows
- Keep EN/CY parity
- Keep scope to one bounded slice per commit
- Update `memory-bank/change-log.md` for non-trivial changes
After proposing the slice, wait for approval before editing files.
```
## Best use
- Start-of-day continuation
- Branch handover between engineers/agents
- Recovery after interrupted context window
+183
View File
@@ -0,0 +1,183 @@
# Refactor Slice Execution (Usable Prompts)
## Purpose
Copy/paste one of these prompts directly to execute a specific next slice (not just a template).
## Prompt A — Slice B1.1 (signed-request helper + pilot migration)
```
Act as a senior architect + senior software engineer working in PEDW FrontEnd.
Before any analysis or code changes:
1) Ask for the Azure DevOps work item ID (required).
2) Create a new branch from `origin/SIPS-Development` prefixed with that ID (e.g. `US12345-signed-helper-pilot`).
3) Confirm branch name and only then proceed.
Goal:
Implement Slice B1.1 only: introduce shared signed-request helper(s) and migrate one pilot signed path in portal service.
Exact scope:
- Add shared signed-request helper(s) in `actions/clients/` for signed GET/POST/DELETE execution.
- Migrate one pilot signed flow in `actions/services/portalDirectService.js` to use the helper.
- Do not migrate additional modules in this slice.
Mandatory context to read before coding:
- `.clinerules/default-rules.md`
- `GUARDRAILS.md`
- `context/architecture.md`
- `context/current-state-scorecard.md`
- `context/next-work-plan-sequence-b.md`
- `memory-bank/debt-list.md`
- latest relevant `memory-bank/change-log.md` entries
Required output before edits:
1. Scope and non-goals
2. Risks and how they are contained
3. Files to touch
4. Acceptance criteria
5. Validation checklist
Implementation constraints:
- Smallest viable diff
- Preserve API/route/public behavior
- Preserve auth/session/security and EN/CY parity
- No dependency churn unless required by scope
- No unrelated cleanup
Validation minimum:
- `npm run lint`
- `node tests/phase22/index.test.cjs`
- `node tests/phase7/service-behaviour.test.cjs`
- explicit negative-path verification for sensitive flows
Completion output:
1. Files changed
2. Behavior parity statement
3. Validation evidence (commands + pass/fail)
4. Rollback steps
5. `memory-bank/change-log.md` entry content
```
## Prompt B — Slice B1.2 (document signed-flow migration)
```
Act as a senior architect + senior software engineer working in PEDW FrontEnd.
Before any analysis or code changes:
1) Ask for the Azure DevOps work item ID (required).
2) Create a new branch from `origin/SIPS-Development` prefixed with that ID (e.g. `TASK67890-document-signed-migration`).
3) Confirm branch name and only then proceed.
Goal:
Implement Slice B1.2 only: migrate remaining signed flows in `documentDirectService` to shared signed-request helper(s) with no behavior change.
Exact scope:
- Update `actions/services/documentDirectService.js` signed GET/POST/DELETE paths to use shared signed helper(s).
- Preserve hash generation, headers, methods, and catch-path behavior.
- Do not modify unrelated services.
Mandatory context to read before coding:
- `.clinerules/default-rules.md`
- `GUARDRAILS.md`
- `context/architecture.md`
- `context/current-state-scorecard.md`
- `context/next-work-plan-sequence-b.md`
- `context/test-coverage-map.md`
- latest relevant `memory-bank/change-log.md` entries
Required output before edits:
1. Scope and non-goals
2. Risks and containment
3. Files to touch
4. Acceptance criteria
5. Validation checklist
Validation minimum:
- `npm run lint`
- `node tests/phase22/index.test.cjs`
- `node tests/phase7/service-behaviour.test.cjs`
Completion output:
1. Files changed
2. Behavior parity statement
3. Validation evidence
4. Rollback steps
5. `memory-bank/change-log.md` entry
```
## Prompt C — Slice B2.1 (auth logging hardening)
```
Act as a senior architect + senior software engineer working in PEDW FrontEnd.
Before any analysis or code changes:
1) Ask for the Azure DevOps work item ID (required).
2) Create a new branch from `origin/SIPS-Development` prefixed with that ID (e.g. `US12345-auth-logging-hardening`).
3) Confirm branch name and only then proceed.
Goal:
Implement Slice B2.1 only: harden logging in `pages/api/auth/[...nextauth].js` using redacted structured logging with no behavior change.
Exact scope:
- Replace sensitive/verbose direct logs in auth route with structured logger usage.
- Do not alter auth/session/redirect functional behavior.
Mandatory context to read before coding:
- `.clinerules/default-rules.md`
- `GUARDRAILS.md`
- `context/architecture.md`
- `context/next-work-plan-sequence-b.md`
- `memory-bank/open-questions.md` (Q-001)
Validation minimum:
- `npm run lint`
- `node tests/phase22/auth-redirect-safety.test.cjs`
- targeted manual negative-path check for auth error logging paths
Completion output must include:
1. Redaction policy applied (fields excluded)
2. Files changed
3. Behavior parity statement
4. Validation evidence
5. Rollback plan
6. `memory-bank/change-log.md` update text
```
## Prompt D — Slice B2.2 (file/email logging hardening cluster)
```
Act as a senior architect + senior software engineer working in PEDW FrontEnd.
Before any analysis or code changes:
1) Ask for the Azure DevOps work item ID (required).
2) Create a new branch from `origin/SIPS-Development` prefixed with that ID (e.g. `TASK67890-file-email-logging-hardening`).
3) Confirm branch name and only then proceed.
Goal:
Implement Slice B2.2 only: harden logging in one bounded file/email route cluster using redacted structured logs.
Exact scope:
- Choose a small cluster in `pages/api/file/**` and/or `pages/api/email/**`.
- Replace risky direct logs with structured redacted logging.
- Preserve endpoint contracts and negative-path semantics.
Validation minimum:
- `npm run lint`
- targeted tests for changed handlers
- explicit negative-path check(s) for unauthorized/invalid input behavior
Completion output:
1. Cluster chosen and why
2. Files changed
3. Redaction fields/policy applied
4. Validation evidence
5. Rollback steps
6. `memory-bank/change-log.md` update text
```
## Best use
- Run Sequence B slices as independent, reviewable commits
- Start a fresh chat and execute one prompt at a time
- Keep each slice bounded with explicit rollback
+94
View File
@@ -0,0 +1,94 @@
import useTranslation from "next-translate/useTranslation";
import { parseCookies, setCookie } from "nookies";
import { useEffect, useRef, useState } from "react";
const COOKIE_NAME = "pedwAiPolicyNoticeHidden";
const COOKIE_DAYS = 7;
export default function AIPolicyNotice() {
let { t } = useTranslation();
const [hasCheckedCookie, setHasCheckedCookie] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const dismissButtonRef = useRef(null);
useEffect(() => {
const cookies = parseCookies();
const isHidden = cookies[COOKIE_NAME] === "true";
setIsOpen(!isHidden);
setHasCheckedCookie(true);
}, []);
useEffect(() => {
if (isOpen && dismissButtonRef.current) {
dismissButtonRef.current.focus();
}
}, [isOpen]);
const dismissNotice = () => {
const now = new Date();
const expDate = new Date(now);
expDate.setDate(now.getDate() + COOKIE_DAYS);
setCookie(null, COOKIE_NAME, "true", {
path: "/",
expires: expDate
});
setIsOpen(false);
};
if (!hasCheckedCookie || !isOpen) {
return null;
}
return (
<div
className="modal aiModal fade show"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "1rem"
}}
aria-hidden={false}
>
<div className="modal-dialog" style={{ margin: 0 }}>
<div
className="modal-content govuk-!-padding-6"
role="dialog"
aria-modal="true"
aria-labelledby="ai-policy-notice-title"
>
<h2
id="ai-policy-notice-title"
className="govuk-heading-m govuk-!-margin-bottom-3"
>
{t("home:ai-policy-notice-title")}
</h2>
<p className="govuk-body">
{t("home:ai-policy-notice-paragraph-one")}
</p>
<p className="govuk-body govuk-!-margin-bottom-4">
{t("home:ai-policy-notice-paragraph-two")}
</p>
<button
ref={dismissButtonRef}
type="button"
className="govuk-button govuk-!-margin-bottom-0"
data-module="govuk-button"
onClick={dismissNotice}
>
{t("home:ai-policy-notice-dismiss-button")}
</button>
</div>
</div>
<div
className="modal-backdrop fade show"
style={{ opacity: 0.2 }}
></div>
</div>
);
}
+5 -5
View File
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
import React, { useState } from "react";
import { connect } from "react-redux";
import { Field, formValueSelector, reduxForm } from "redux-form";
import { updatePassword } from "../../actions";
import { updatePassword } from "../../actions/services/accountService";
const RenderTextfield = ({
id,
@@ -60,7 +60,7 @@ const ChangePassword = (props) => {
handleSubmit,
value,
setPasswordUpdated,
passwordUpdated,
passwordUpdated
} = props;
let { t, lang } = useTranslation();
@@ -158,7 +158,7 @@ const mapStateToProps = (state) => {
searchResultsObj: state.searchResultsObj,
formData: state.formData,
appealType: state.appealType,
accountDetails: state.accountDetails,
accountDetails: state.accountDetails
//form: state.form,
};
};
@@ -167,7 +167,7 @@ const mapDispatchToProps = (dispatch) => {
return {
setCurrentSection: (currentSection) => {
dispatch(setCurrentSection(currentSection));
},
}
};
};
@@ -179,6 +179,6 @@ export default connect(
)(
reduxForm({
form: "changepasswordForm",
destroyOnUnmount: false,
destroyOnUnmount: false
})(ChangePassword)
);
+9 -9
View File
@@ -140,12 +140,12 @@ const RenderPickList = ({
input,
optionsArr,
meta: { touched, error },
errorMsg,
errorMsg
}) => {
let { t, lang } = useTranslation();
optionsArr = [
t("account:account-preferred-language-english") + ":846040001",
t("account:account-preferred-language-welsh") + ":846040000",
t("account:account-preferred-language-welsh") + ":846040000"
];
return (
<>
@@ -202,7 +202,7 @@ let PersonalDetails = (props) => {
load,
pristine,
reset,
submitting,
submitting
} = props;
let { t, lang } = useTranslation();
@@ -324,8 +324,8 @@ let PersonalDetails = (props) => {
label={t("account:account-telephone-label")}
/>
<Field
name="company"
id="company"
name="pinswg_companyname"
id="pinswg_companyname"
component={RenderTextfield}
type="text"
className="govuk-input govuk-input--width-20"
@@ -444,7 +444,7 @@ const mapStateToProps = (state) => {
searchResultsObj: state.searchResultsObj,
formData: state.formData,
appealType: state.appealType,
accountDetails: state.accountDetails,
accountDetails: state.accountDetails
//form: state.form,
};
};
@@ -453,7 +453,7 @@ const mapDispatchToProps = (dispatch) => {
return {
setCurrentSection: (currentSection) => {
dispatch(setCurrentSection(currentSection));
},
}
};
};
@@ -484,12 +484,12 @@ const selector = formValueSelector("personalDetailsForm"); // <-- same as form n
PersonalDetails = reduxForm({
form: "personalDetailsForm",
enableReinitialize: true, // this is needed!!
destroyOnUnmount: false,
destroyOnUnmount: false
})(PersonalDetails);
// You have to connect() to any reducers that you wish to connect to yourself
PersonalDetails = connect((state) => ({
initialValues: state.accountDetails.accountDetails, // pull initial values from account reducer
initialValues: state.accountDetails.accountDetails // pull initial values from account reducer
}))(PersonalDetails);
export default PersonalDetails;
+4 -4
View File
@@ -11,7 +11,7 @@ const BuildCheckPersonalDetails = (props) => {
handleSubmit,
value,
setPersonalDetailsComplete,
setAccountUpdatedComplete,
setAccountUpdatedComplete
} = props;
let { t, lang } = useTranslation();
@@ -142,7 +142,7 @@ const BuildCheckPersonalDetails = (props) => {
<dd className="govuk-summary-list__value">
{
props.props.props.form.personalDetailsForm.values
.company
.pinswg_companyname
}
</dd>
<dd className="govuk-summary-list__actions">
@@ -354,7 +354,7 @@ const mapStateToProps = (state) => {
searchResultsObj: state.searchResultsObj,
formData: state.formData,
appealType: state.appealType,
accountDetails: state.accountDetails,
accountDetails: state.accountDetails
//form: state.form,
};
};
@@ -363,7 +363,7 @@ const mapDispatchToProps = (dispatch) => {
return {
setCurrentSection: (currentSection) => {
dispatch(setCurrentSection(currentSection));
},
}
};
};
+13 -9
View File
@@ -3,7 +3,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { updateAccount } from "../../actions";
import { updateAccount } from "../../actions/services/accountService";
import { setCookie } from "nookies";
const PersonalDetailsComplete = (props) => {
@@ -11,7 +11,7 @@ const PersonalDetailsComplete = (props) => {
handleSubmit,
value,
setRegisterFormComplete,
setAccountCreatedComplete,
setAccountCreatedComplete
} = props;
let { t, lang } = useTranslation();
@@ -28,6 +28,8 @@ const PersonalDetailsComplete = (props) => {
const formValues = props.props?.props?.form?.personalDetailsForm?.values;
const prefLang = formValues?.pinswg_preferredlanguage;
const userLang = prefLang === "846040000" ? "cy" : "en"; // Move this outside useEffect
useEffect(() => {
// let contactid = props.accountDetails.loggedinUserId;
// let updateBody = props.props.props.form.personalDetailsForm.values;
@@ -37,10 +39,8 @@ const PersonalDetailsComplete = (props) => {
if (!loggedinUserId || !formValues) return;
console.log("=============== pref lang", prefLang);
const userLang = prefLang == "846040000" ? "cy" : "en";
setCookie(null, "pedw_locale", userLang, {
path: "/",
maxAge: 60 * 60 * 24 * 365,
path: "/"
});
console.log("Set cookie to:", userLang);
@@ -50,7 +50,7 @@ const PersonalDetailsComplete = (props) => {
}
doUpdate();
}, [loggedinUserId, formValues, prefLang]);
}, [loggedinUserId, formValues, prefLang, userLang]);
return (
<>
@@ -59,7 +59,11 @@ const PersonalDetailsComplete = (props) => {
{/* <p className="govuk-body">{t("account:account-complete-para-2")}</p> */}
<p className="govuk-body">
<Link href={"/"} className="govuk-link">
<Link
locale={userLang}
href={userLang === "cy" ? "/cy/fymhorth" : "/myportal"}
className="govuk-link"
>
{" "}
{t("account:account-complete-return-to-portal-link")}
</Link>
@@ -74,7 +78,7 @@ const mapStateToProps = (state) => {
searchResultsObj: state.searchResultsObj,
formData: state.formData,
appealType: state.appealType,
accountDetails: state.accountDetails,
accountDetails: state.accountDetails
//form: state.form,
};
};
@@ -83,7 +87,7 @@ const mapDispatchToProps = (dispatch) => {
return {
setCurrentSection: (currentSection) => {
dispatch(setCurrentSection(currentSection));
},
}
};
};
+10 -7
View File
@@ -8,7 +8,10 @@ import { connect } from "react-redux";
import { setAccCr, setLogout } from "../../store/accountDetails/action";
import { createAccount, getEmailAccountCheck } from "../../actions";
import {
createAccount,
getEmailAccountCheck
} from "../../actions/services/accountService";
const RegisterComplete = (props) => {
const {
@@ -18,7 +21,7 @@ const RegisterComplete = (props) => {
value,
setRegisterFormComplete,
setAccountCreatedComplete,
setAccCr,
setAccCr
} = props;
let { t, lang } = useTranslation();
@@ -34,7 +37,7 @@ const RegisterComplete = (props) => {
// was an appellant "pinswg_typeofinvolvement": 846040001,
Object.assign(accountBody, {
"pinswg_typeofinvolvement": 846040061,
"pinswg_typeofinvolvement": 846040061
});
accountBody = _.omit(accountBody, ["custom_password_check"]);
@@ -105,9 +108,9 @@ const RegisterComplete = (props) => {
<p className="govuk-body">
<a
onClick={() => {
setRegisterFormComplete(false),
(setRegisterFormComplete(false),
setAccountCreatedComplete(false),
setAccCr(false);
setAccCr(false));
}}
className="govuk-link"
>
@@ -130,7 +133,7 @@ const mapStateToProps = (state) => {
searchResultsObj: state.searchResultsObj,
formData: state.formData,
appealType: state.appealType,
accountDetails: state.accountDetails,
accountDetails: state.accountDetails
//form: state.form,
};
};
@@ -142,7 +145,7 @@ const mapDispatchToProps = (dispatch) => {
},
setLogout: () => {
dispatch(setLogout());
},
}
};
};
+16 -5
View File
@@ -66,7 +66,7 @@ const RegisterForm = (props) => {
setRegisterFormComplete,
setAccountCreatedComplete,
loggedInUserEmail,
emailaddress1,
emailaddress1
} = props;
let { t, lang } = useTranslation();
@@ -199,6 +199,17 @@ const RegisterForm = (props) => {
"account:register-new-account-phone-label"
)}
/>
<Field
name="company"
id="company"
component={RenderTextfield}
type="text"
className="govuk-input govuk-input--width-20"
validate={[required]}
label={t(
"account:register-new-account-company-label"
)}
/>
</fieldset>
<fieldset className="govuk-fieldset">
<legend className="govuk-fieldset__legend govuk-fieldset__legend--m">
@@ -312,8 +323,8 @@ const mapStateToProps = (state, props) => {
appealType: state.appealType,
//form: state.form,
initialValues: {
emailaddress1: props.loggedInUserEmail,
},
emailaddress1: props.loggedInUserEmail
}
};
};
@@ -321,7 +332,7 @@ const mapDispatchToProps = (dispatch) => {
return {
setCurrentSection: (currentSection) => {
dispatch(setCurrentSection(currentSection));
},
}
};
};
@@ -334,6 +345,6 @@ export default connect(
reduxForm({
form: "accountRegisterForm",
enableReinitialize: true,
destroyOnUnmount: false,
destroyOnUnmount: false
})(RegisterForm)
);
+40
View File
@@ -0,0 +1,40 @@
// components/admin/tabs/AccountsTab.js
import Link from "next/link";
export default function AccountsTab({ userData }) {
return (
<div>
<h1>User Accounts</h1>
<p>Number of accounts created: {userData.length}</p>
<dl className="govuk-summary-list govuk-!-margin-bottom-9 results">
<div className="govuk-summary-list__row results-headings">
<dd className="govuk-summary-list__key govuk-!-font-size-16">
Name
</dd>
<dd className="govuk-summary-list__key govuk-!-font-size-16">
Last accessed
</dd>
</div>
{userData.map((user) => (
<div key={user.id} className="govuk-summary-list__row">
<dd className="govuk-summary-list__value govuk-!-font-size-16 ">
<Link
target="_blank"
href={`/auth/signin2?email=${user.email}`}
>
{user.email}
</Link>
<br />
{user.id}
</dd>
<dd className="govuk-summary-list__value govuk-!-font-size-16 ">
Last accessed: {user.created}
</dd>
</div>
))}
</dl>
</div>
);
}
+571
View File
@@ -0,0 +1,571 @@
// components/admin/tabs/AppealsTab.js
import useTranslation from "next-translate/useTranslation";
import { useCallback, useState, useEffect } from "react";
import { JSONPath as jsonpath } from "jsonpath-plus";
import Link from "next/link";
import { useRouter } from "next/router";
import PaginationControl from "../../../components/search/pagination";
import { connect } from "react-redux";
import {
setSearchResults,
setSearchDetails
} from "../../../store/searchOutput/action";
import { getDetailsProxy, getSearchDetailsPaged } from "../../utils";
import { getAdvancedSearchPaged } from "../../../actions/services/searchService";
import { getNewAppealsPage } from "../../../actions/services/adminService";
import {
setCurrentPage,
setCurrentReference
} from "../../../store/currentView/action";
function AppealsTab(props) {
const [searchLoaded, setSearchLoaded] = useState(false);
const [showSpinnerState, setShowSpinnerState] = useState(false);
const [selectedOption, setSelectedOption] = useState(10);
const [orderByState, setOrderbyState] = useState("createdon");
const [fieldSortState, setfieldSortState] = useState("desc");
const [loginToWatch, setLoginToWatch] = useState("true");
const {
searchResultsObj: searchResultsState,
setSearchResults,
setSearchDetails
} = props;
const searchDetailsObj = searchResultsState.searchDetailsObj || {};
const resultsArr = searchResultsState.searchResultsObj.value || [];
const searchResultsObj = searchResultsState.searchResultsObj;
const { t } = useTranslation();
const router = useRouter();
const myportal = false;
const advancedSearch = false;
const searchString = "";
const spinnerState = () => {
setShowSpinnerState((prev) => !prev);
};
const pagesCount = Math.ceil(searchResultsObj["@odata.count"] / 10);
let currentPage = 0;
try {
currentPage = decodeURI(searchResultsObj["@odata.nextLink"]).split(
"skiptoken="
)[1];
currentPage =
typeof currentPage !== "undefined"
? parseInt(
decodeURI(currentPage)
.split('pagenumber="')[1]
.slice(
0,
decodeURI(currentPage)
.split('pagenumber="')[1]
.indexOf('"')
) - 1
)
: pagesCount;
} catch (e) {
currentPage = pagesCount;
}
const getSearchPageResults = useCallback(
(pageNumber, orderBy, fieldSort) => {
getNewAppealsPage(
searchString,
pageNumber,
orderBy,
fieldSort,
selectedOption
).then((data) => {
setSearchResults(data);
return getSearchDetailsPaged(data).then((details) => {
setSearchDetails(details);
setShowSpinnerState(false);
});
});
},
[selectedOption, setSearchDetails, setSearchResults]
);
useEffect(() => {
if (searchLoaded) {
if (selectedOption) {
setShowSpinnerState(true);
getSearchPageResults(1, orderByState, fieldSortState);
}
} else {
setSearchLoaded(true);
}
}, [
selectedOption,
fieldSortState,
orderByState,
searchLoaded,
getSearchPageResults,
router.query
]);
const ResultsView = (resultsArr) => {
return (
<>
<dl className="govuk-summary-list govuk-!-margin-bottom-9 results">
<div className="govuk-summary-list__row results-headings">
<dd className="govuk-summary-list__key govuk-!-font-size-16 results_wider ">
<button
type="button"
onClick={() => sortDataBy("title")}
className={
orderByState == "title"
? fieldSortState == "asc"
? `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16 sortAsc`
: `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16 sortDesc`
: `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16`
}
>
{t("search:searchresults-case-reference-label")}
</button>
</dd>
<dd className="govuk-summary-list__key govuk-!-font-size-16">
{t("search:searchresults-site-address-label")}
</dd>
<dd className="govuk-summary-list__key govuk-!-font-size-16">
<button
type="button"
onClick={() => sortDataBy("_customerid_value")}
className={
orderByState == "_customerid_value"
? fieldSortState == "asc"
? `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16 sortAsc`
: `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16 sortDesc`
: `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16`
}
>
{t("search:searchresults-applicant-label")}
</button>
</dd>
<dd className="govuk-summary-list__key govuk-!-font-size-16">
<button
type="button"
onClick={() =>
sortDataBy("_pinswg_associatedlpa_value")
}
className={
orderByState ==
"_pinswg_associatedlpa_value"
? fieldSortState == "asc"
? `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16 sortAsc`
: `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16 sortDesc`
: `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16`
}
>
{t("search:searchresults-authority-label")}
</button>
</dd>
<dd className="govuk-summary-list__key govuk-!-font-size-16">
<button
type="button"
onClick={() =>
sortDataBy("pinswg_appealcasetype")
}
className={
orderByState == "pinswg_appealcasetype"
? fieldSortState == "asc"
? `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16 sortAsc`
: `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16 sortDesc`
: `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16`
}
>
{t("search:searchresults-case-type-label")}
</button>
</dd>
<dd className="govuk-summary-list__key govuk-!-font-size-16">
{/* {t("search:searchresults-status-label")} */}
<button
type="button"
onClick={() => sortDataBy("statuscode")}
className={
orderByState == "statuscode"
? fieldSortState == "asc"
? `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16 sortAsc`
: `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16 sortDesc`
: `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16`
}
>
{t("search:searchresults-status-label")}
</button>
</dd>
<dd className="govuk-summary-list__key govuk-!-font-size-16 govuk-summary-list__action">
Origin
</dd>
</div>
{showSpinnerState == true ? (
<div className="govuk-summary-list__row">
<div className="govuk-!-margin-top-9 govuk-!-margin-bottom-9 govuk-!-font-weight-bold govuk-!-font-size-20 ">
{t("common:spinner-fetching-data")}
<img
className="data-loading-icon"
src="/assets/images/loading.gif"
alt={
router.locale == "cy"
? "Nôl data"
: "Fetching data"
}
/>
</div>
</div>
) : (
resultsArr.map((item, key) => {
let detailsObj = jsonpath({
path:
'$..[?(@ && @.ticketnumber=="' +
item.ticketnumber +
'")]',
json: searchDetailsObj,
eval: true
});
detailsObj = detailsObj[0] || {};
return (
<div
className="govuk-summary-list__row"
key={key}
>
<dd className="govuk-summary-list__value govuk-!-font-size-16 ">
<div></div>
{item.pinswg_publishtoweb == true ? (
<Link
target="_blank"
rel="noreferrer"
href={
(router.locale == "cy"
? myportal == true
? "/fymhorth/achos"
: "/achos"
: myportal == true
? "/myportal/case"
: "/case") +
"/" +
item.ticketnumber
}
className="govuk-link--no-underline"
onClick={() => {
setCurrentReference({
"statuscode":
item.statuscode,
"ticketnumber":
item.ticketnumber,
"currentReference":
item.title,
"currentType":
"searchResultsObj",
"incidentid":
item.incidentid,
"appealType":
item.pinswg_appealcasetype,
"showLoginCheck":
props.showLoginCheck,
"specialistProcess":
detailsObj.hasOwnProperty(
"pinswg_speacialistcaseprocess"
)
? detailsObj.pinswg_speacialistcaseprocess
: detailsObj.hasOwnProperty(
"pinswg_specialistcaseprocess"
)
? detailsObj.pinswg_specialistcaseprocess
: ""
});
}}
>
<span className="results-visually-hidden">
{t(
"search:searchresults-case-reference-label"
)}
:
</span>
{item.title}
</Link>
) : (
item.title
)}
<br />
{new Date(
item.createdon
).toLocaleString()}
</dd>
<dd className="govuk-summary-list__value govuk-!-font-size-16">
<span className="results-visually-hidden">
{t(
"search:searchresults-site-address-label"
)}
:
</span>
{_.has(
detailsObj,
"pinswg_projectlocation"
)
? detailsObj.pinswg_projectlocation !=
null
? detailsObj.pinswg_projectlocation.indexOf(
";"
) < 0
? detailsObj.pinswg_projectlocation
: router.locale == "cy"
? detailsObj.pinswg_projectlocation.split(
";"
)[1]
: detailsObj.pinswg_projectlocation.split(
";"
)[0]
: ""
: ""}
{_.has(
detailsObj,
"pinswg_siteaddressline1"
)
? detailsObj.pinswg_siteaddressline1
: ""}
{/* {detailsObj.pinswg_siteaddressline1 !=
null && <br />} */}
{_.has(
detailsObj,
"pinswg_siteaddressline1"
) &&
detailsObj.pinswg_siteaddressline1 !=
null && <br />}
{_.has(
detailsObj,
"pinswg_siteaddressline1"
)
? detailsObj.pinswg_siteaddressline2
: ""}
{_.has(
detailsObj,
"pinswg_siteaddressline2"
) &&
detailsObj.pinswg_siteaddressline2 !=
null && <br />}
{_.has(
detailsObj,
"pinswg_siteaddresstown"
)
? detailsObj.pinswg_siteaddresstown
: ""}
{_.has(
detailsObj,
"pinswg_siteaddresstown"
) &&
detailsObj.pinswg_siteaddresstown !=
null && <br />}
{_.has(
detailsObj,
"pinswg_siteaddresscounty"
)
? detailsObj.pinswg_siteaddresscounty
: ""}
{_.has(
detailsObj,
"pinswg_siteaddresscounty"
) &&
detailsObj.pinswg_siteaddresscounty !=
null && <br />}
{_.has(
detailsObj,
"pinswg_siteaddresspostcode"
)
? detailsObj.pinswg_siteaddresspostcode
: ""}
</dd>
<dd className="govuk-summary-list__value govuk-!-font-size-16">
<span className="results-visually-hidden">
{t(
"search:searchresults-applicant-label"
)}
:
</span>
{item.pinswg_appellantagent == 846040001
? item.pinswg_appellantfirstname +
" " +
item.pinswg_appellantlastname
: _.has(detailsObj, [
"_pinswg_appellant_value@OData.Community.Display.V1.FormattedValue"
])
? detailsObj[
"_pinswg_appellant_value@OData.Community.Display.V1.FormattedValue"
]
: ""}
</dd>
<dd className="govuk-summary-list__value govuk-!-font-size-16">
<span className="results-visually-hidden">
{t(
"search:searchresults-authority-label"
)}
:
</span>
{_.has(item, [
"_pinswg_associatedlpa_value@OData.Community.Display.V1.FormattedValue"
])
? router.locale == "cy"
? jsonpath({
path:
'$..[?(@ && @.value=="' +
item[
"_pinswg_associatedlpa_value@OData.Community.Display.V1.FormattedValue"
] +
'")].value_cy',
json: transLookup,
eval: true
})
: item[
"_pinswg_associatedlpa_value@OData.Community.Display.V1.FormattedValue"
] || "N/A"
: ""}
</dd>
<dd className="govuk-summary-list__value govuk-!-font-size-16">
<span className="results-visually-hidden">
{t(
"search:searchresults-case-type-label"
)}
:
</span>
{router.locale == "cy"
? jsonpath({
path:
'$..[?(@ && @.value=="' +
item[
"pinswg_appealcasetype@OData.Community.Display.V1.FormattedValue"
] +
'")].value_cy',
json: transLookup,
eval: true
})
: item[
"pinswg_appealcasetype@OData.Community.Display.V1.FormattedValue"
] || "N/A"}
</dd>
<dd className="govuk-summary-list__value govuk-!-font-size-16">
<span className="results-visually-hidden">
{t(
"search:searchresults-status-label"
)}
:
</span>
{router.locale == "cy"
? jsonpath({
path:
'$..[?(@ && @.value=="' +
item[
"statuscode@OData.Community.Display.V1.FormattedValue"
] +
'")].value_cy',
json: transLookup,
eval: true
})
: item[
"statuscode@OData.Community.Display.V1.FormattedValue"
] || "N/A"}
</dd>
<dd className="govuk-summary-list__value govuk-!-font-size-16">
{item[
"caseorigincode@OData.Community.Display.V1.FormattedValue"
] || "N/A"}
</dd>
</div>
);
})
)}
</dl>
<PaginationControl
showNoOfRecords={selectedOption}
currentPage={currentPage}
searchResultsObj={props.searchResultsObj.searchResultsObj}
orderBy={orderByState}
fieldSort={fieldSortState}
spinnerState={spinnerState}
getSearchPageResults={getSearchPageResults}
/>
</>
);
};
return (
<div>
<h1>Appeals</h1>
{searchResultsObj["@odata.count"] > 5 && (
<div id="recordCountSelect" className="">
<div className="govuk-form-group">
<label
className="govuk-label-s "
htmlFor="showNumberOfRecords"
>
{t("search:searchresults-show-records-label-a")}
<select
onChange={(e) => {
setSelectedOption(e.target.value);
props.setCurrentPage(1);
}}
value={selectedOption}
className="govuk-select"
id="showNumberOfRecords"
name="showNumberOfRecords"
>
<option value="5">5</option>
<option value="10">10</option>
<option value="20">20</option>
<option value="30">30</option>
<option value="50">50</option>
</select>{" "}
{t("search:searchresults-show-records-label-b")}
</label>
</div>
</div>
)}
{resultsArr.length > 0 ? (
ResultsView(resultsArr)
) : (
<div className="govuk-grid-row">
<div className="govuk-grid-column-two-thirds">
<h3>{t("search:searchresults-no-records-label")}</h3>
</div>
</div>
)}
</div>
);
}
const mapDispatchToProps = (dispatch) => {
return {
setCurrentReference: (currentReference) => {
dispatch(setCurrentReference(currentReference));
},
setSearchResults: (searchResultsObj) => {
dispatch(setSearchResults(searchResultsObj));
},
setSearchDetails: (searchDetailsObj) => {
dispatch(setSearchDetails(searchDetailsObj));
},
setCurrentPage: (currentPage) => {
dispatch(setCurrentPage(currentPage));
}
};
};
const mapStateToProps = (state) => {
return {
searchResultsObj: state.searchResultsObj
};
};
export default connect(mapStateToProps, mapDispatchToProps)(AppealsTab);
File diff suppressed because it is too large Load Diff
+266
View File
@@ -0,0 +1,266 @@
// components/admin/tabs/StorageTab.js
import { useState } from "react";
import Link from "next/link";
import { bytesToSize } from "../../../components/utils";
import {
deleteMyRepresentationsFromBlob,
deleteAwaitingSubmissionsFromBlob
} from "../../../actions/services/documentService";
import { sendRepCompleteMessage } from "../../../actions/services/portalService";
import { formatBlobDates } from "../utils/adminHelper";
export default function StorageTab({ data, repCompleteCount }) {
const [showRepJson, setShowRepJson] = useState(true);
const [showTmpFile, setShowTmpFile] = useState(true);
const [listData, setListData] = useState(data);
const toggleAll = () => {
const newState = !(showRepJson || showTmpFile);
setShowRepJson(newState);
setShowTmpFile(newState);
};
return (
<div>
<h1>Storage Account Contents</h1>
{repCompleteCount > 0 && (
<p>Number of Orphaned Reps: {repCompleteCount}</p>
)}
{/* Toggle buttons */}
<div style={{ marginBottom: "0.2rem" }}>
<button
className="govuk-button govuk-button--secondary"
onClick={() => setShowRepJson(!showRepJson)}
>
{showRepJson ? "Hide Reps" : "Show Reps"}
</button>
<button
className="govuk-button govuk-button--secondary"
onClick={() => setShowTmpFile(!showTmpFile)}
style={{ marginLeft: "0.5rem" }}
>
{showTmpFile ? "Hide Temp Appeal" : "Show Temp Appeal"}
</button>
<button
className="govuk-button govuk-button--secondary"
onClick={toggleAll}
style={{ marginLeft: "0.5rem" }}
>
Show/Hide All
</button>
</div>
{/* Render users & containers */}
{listData.map((user) => (
<div key={user.id} className="govuk-grid-row">
{user.containers.map((c) => (
<div key={c.container} className="card">
<div className="card-body">
<h3>
{c.container} -{" "}
<Link
href={`/auth/signin2?email=${user.email}`}
>
{user.email}
</Link>
</h3>
{/* folders */}
{c.folders.map((folder) => (
<div
key={folder.folderPath}
style={
showRepJson && folder.hasRepJson
? { display: "block" }
: showTmpFile &&
folder.hasTmpFile
? { display: "block" }
: { display: "none" }
}
>
<h4>{folder.folderPath || "Root"}</h4>
{/* Buttons for TMP + Rep actions go here (same logic as before) */}
{folder.hasTmpFile && (
<>
<button
onClick={() => {
confirm(
"Do you want to remove this unsubmitted appeal " +
folder.folderPath +
"?\n"
) &&
(deleteCaseItem(
c.container,
folder.folderPath,
setData
),
alert(
"Temp case Deleted!"
));
}}
>
Delete this TMP appeal
</button>
</>
)}
{folder.hasRepJson && (
<div>
{" "}
<button
title={
"Do you want to delete rep"
}
className=" cardModuleRemoveCase govuk-link govuk-link--no-underline govuk-link--inverse "
onClick={() => {
confirm(
"Do you want tyo delete rep " +
folder.repJsonBlob.name.split(
"/"
)[0] +
"?\n"
) &&
(deleteRepItem(
c.container,
folder.repJsonBlob.name.split(
"/"
)[0],
folder.repJsonBlob.name.split(
"/"
)[1]
),
alert(
"Rep Deleted!"
),
window.location.reload());
}}
>
Delete Rep
</button>
{folder.repComplete && (
<button
onClick={async () => {
const response =
await fetch(
"/api/file/editRepJson",
{
method: "POST",
headers:
{
"Content-Type":
"application/json"
},
body: JSON.stringify(
{
container:
c.container,
blobName:
folder
.repJsonBlob
.name
}
)
}
);
const result =
await response.json();
if (response.ok) {
(alert(
"repComplete removed!"
),
window.location.reload());
// Optionally refresh the page or update UI state here
} else {
alert(
"Error: " +
result.error
);
}
}}
>
Remove repComplete
</button>
)}
{folder.repComplete && (
<button
onClick={async () => {
confirm(
"Do you want resend the rep queue message " +
folder.repJsonBlob.name.split(
"/"
)[0] +
" --- " +
folder.repJsonBlob.name.split(
"/"
)[1]
) &&
(sendRepCompleteMessage(
c.container,
folder.repJsonBlob.name.split(
"/"
)[0],
folder.repJsonBlob.name.split(
"/"
)[1]
),
alert(
"rep queue message sent!"
),
window.location.reload());
}}
>
Recreate Queue Message
</button>
)}
</div>
)}
<ul>
{folder.blobs.map((blob) => (
<li key={blob.name}>
<Link
scroll={false}
href={`/api/file/downloadblob?container=${
c.container
}&casefolderID=${
folder.folderPath
}&blobname=${blob.name.substring(
blob.name.lastIndexOf(
"/"
) + 1
)}&hash=${
blob.hashedfilepath
}`}
className="govuk-body govuk-!-font-size-14 govuk-link"
>
{blob.name} (
{bytesToSize(
blob.contentLength ||
blob.size
)}
)
</Link>{" "}
{blob.lastModified && (
<span className="govuk-!-font-size-14">
{/* {
blob.displayDate
} */}
{formatBlobDates(
blob
)}
</span>
)}
</li>
))}
</ul>
</div>
))}
</div>
</div>
))}
</div>
))}
</div>
);
}
+81
View File
@@ -0,0 +1,81 @@
// components/admin/utils/adminStorage.js
/**
* Get the folder path up to the last slash.
* Used for grouping blobs by "case folder".
*/
export function getUpToLastSlash(url) {
let lastSlash = url.lastIndexOf("/");
url = url.substring(0, lastSlash);
url =
url.lastIndexOf("/files") > 0
? url.substring(0, url.lastIndexOf("/files"))
: url;
return url;
}
/**
* Convert a stream (Azure blob download) to a string.
*/
export async function streamToString(readableStream) {
return new Promise((resolve, reject) => {
const chunks = [];
readableStream.on("data", (data) => {
chunks.push(data.toString());
});
readableStream.on("end", () => {
resolve(chunks.join(""));
});
readableStream.on("error", reject);
});
}
/**
* Format how long ago a date occurred.
*/
export function formatTimeSince(date) {
if (!date) return "Unknown date";
const now = new Date();
const diffMs = now - date;
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays < 7) {
return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`;
} else {
const diffWeeks = Math.floor(diffDays / 7);
return `${diffWeeks} week${diffWeeks !== 1 ? "s" : ""} ago`;
}
}
/**
* Format Azure blob created/modified dates into a readable string.
*/
export function formatBlobDates(blob) {
const created = blob.createdOn ? new Date(blob.createdOn) : null;
const modified = blob.lastModified ? new Date(blob.lastModified) : null;
if (!created && !modified) return "Unknown";
// If both exist and are the same
if (created && modified && created.getTime() === modified.getTime()) {
return `${created.toLocaleString("en-GB")} (${formatTimeSince(
created
)})`;
}
// If modified exists and is newer than created
if (created && modified && modified.getTime() > created.getTime()) {
return `Created: ${created.toLocaleString("en-GB")} (${formatTimeSince(
created
)}), Modified: ${modified.toLocaleString("en-GB")} (${formatTimeSince(
modified
)})`;
}
// Only one date exists
const date = modified || created;
return `${date.toLocaleString("en-GB")} (${formatTimeSince(date)})`;
}
+14
View File
@@ -0,0 +1,14 @@
// lib/prisma.js
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis;
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: ["query", "error", "warn"],
});
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
+174
View File
@@ -0,0 +1,174 @@
// components/admin/utils/serverStorage.js
import { DefaultAzureCredential } from "@azure/identity";
import { BlobServiceClient } from "@azure/storage-blob";
import { PrismaClient } from "@prisma/client";
import CryptoJS from "crypto-js";
import { getUpToLastSlash, streamToString } from "../utils/adminHelper";
import { listContainersForUser } from "../../../actions/azurestorage";
import pLimit from "p-limit";
const prisma = global.prisma || new PrismaClient();
const WORDKEY = process.env.HASHKEY;
/**
* Generate HMAC hash for blob API link.
*/
function hashAPIPath(queryPath, WORDKEY) {
let hashlink = CryptoJS.HmacSHA256(
queryPath,
CryptoJS.enc.Hex.parse(WORDKEY)
);
return hashlink.toString(CryptoJS.enc.Hex);
}
/**
* Fetch blob structure + users for admin storage page.
*/
export async function fetchAdminStorageData() {
const accountName = process.env.AZURE_STORAGE_ACCOUNT_NAME;
const creds = new DefaultAzureCredential();
const blobServiceClient = new BlobServiceClient(
`https://${accountName}.blob.core.windows.net`,
creds
);
const prisma = global.prisma || new PrismaClient();
const users = await prisma.user.findMany();
const limit = pLimit(5);
let data = await Promise.all(
users.map(async (user) => {
limit(async () => {
const containers = await listContainersForUser(
blobServiceClient,
`${user.id}`,
user.email
);
const containersWithHashes = await Promise.all(
containers.map(async (container) => {
const groupedByFolder = await container.blobs.reduce(
async (accP, blob) => {
const acc = await accP;
const folderPath =
getUpToLastSlash(blob.name.trim()) || "";
const hashedfilepath = hashAPIPath(
`/api/file/downloadblob?container=${
container.container
}&casefolderID=${folderPath}&blobname=${blob.name
.substring(
blob.name.lastIndexOf("/") + 1
)
.trim()}`,
process.env.HASHKEY
);
const fileNameOnly = blob.name.substring(
blob.name.lastIndexOf("/") + 1
);
const isTmpFile =
fileNameOnly.startsWith("TMP-");
const blobWithHash = {
...blob,
hashedfilepath,
isTmpFile
};
let folderGroup = acc.find(
(f) => f.folderPath === folderPath
);
if (!folderGroup) {
folderGroup = {
folderPath,
blobs: [],
hasRepJson: false,
repJsonBlob: null,
repComplete: false,
hasTmpFile: false
};
acc.push(folderGroup);
}
folderGroup.blobs.push(blobWithHash);
if (isTmpFile) {
folderGroup.hasTmpFile = true;
}
// rep.json handling
if (
blob.name.toLowerCase().endsWith("rep.json")
) {
folderGroup.hasRepJson = true;
folderGroup.repJsonBlob = blobWithHash;
const containerClient =
blobServiceClient.getContainerClient(
container.container
);
const blockBlobClient =
containerClient.getBlockBlobClient(
blob.name
);
try {
const downloadResponse =
await blockBlobClient.download(0);
const downloaded = await streamToString(
downloadResponse.readableStreamBody
);
const json = JSON.parse(downloaded);
folderGroup.repComplete = Boolean(
json.repComplete
);
} catch (err) {
console.error(
"Error reading rep.json",
err
);
folderGroup.repComplete = false;
}
}
return acc;
},
Promise.resolve([])
);
return { ...container, folders: groupedByFolder };
})
);
return containersWithHashes.length > 0
? {
id: user.id,
email: user.email,
containers: containersWithHashes
}
: null;
});
})
);
data = data.filter(Boolean);
let userData = users
.filter((user) => user.email && user.email.trim() !== "")
.map((user) => ({
id: user.id,
email: user.email,
created: user.emailVerified?.toLocaleString("en-GB") || ""
}));
let repCompleteCount = 0;
data.forEach((user) => {
user.containers.forEach((container) => {
container.folders.forEach((folder) => {
if (folder.repComplete === true) repCompleteCount++;
});
});
});
return { data, userData, repCompleteCount };
}
+274 -1154
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -27,6 +27,7 @@ const Case = (props) => {
watchedCasesDetails={props.watchedCasesDetails}
awaitingSubmission={props.awaitingSubmission}
caseReference={props.caseReference}
ticketnumber={props.ticketnumber}
currentType={props.currentType}
appealTypeID={props.appealTypeID}
searchResultsObj={props.searchResultsObj}
@@ -40,7 +41,9 @@ const Case = (props) => {
docsOffline={props.props.docsOffline}
documentDetailsObj={props.documentDetailsObj}
showFilteredDocs={props.showFilteredDocs}
showLoginCheck={props.showLoginCheck}
eventsObj={props.eventsObj}
mediaObj={props.mediaObj}
/>
{showRepresentations == true && (
<RepresentationList
+67 -105
View File
@@ -5,21 +5,23 @@ import { useRouter } from "next/router";
import { useCallback, useEffect, useRef, useState } from "react";
import { connect } from "react-redux";
import transLookup from "../../data/lookuptranslations.json";
import { formatDates } from "../utils";
import { formatDates, getDocLink } from "../utils";
import PaginationControl from "./pagination";
import { sendGAEvent } from "@next/third-parties/google";
import {
getSearchDocumentDetails,
getSearchDocumentDetailsPaged,
getSearchDocumentTypes,
} from "../../actions";
getSearchDocumentTypes
} from "../../actions/services/searchService";
import { setCurrentPage } from "../../store/currentView/action";
import { setDocumentDetails } from "../../store/searchOutput/action";
import DocumentLink from "../utils/downloads";
import { useDownloadQueue } from "../utils/downloadmanager";
const DocumentDetails = (props) => {
let { t } = useTranslation();
let { t, lang } = useTranslation();
const firstRender = useRef(false);
const {
incidentid,
@@ -30,14 +32,14 @@ const DocumentDetails = (props) => {
setDocumentDetails,
setCurrentPage,
docsOffline,
showFilteredDocs,
showFilteredDocs
} = props;
const router = useRouter();
const { locale } = router;
var documentDetailsArr = documentDetailsObj || [];
const { startDownload, getStatus } = useDownloadQueue(3);
const [selectedOption, setSelectedOption] = useState(10);
const [documentTypes, setDocumentTypes] = useState([{}]);
const [selectedDocumentType, setSelectedDocumentType] = useState("all");
@@ -55,7 +57,7 @@ const DocumentDetails = (props) => {
setCheckedItems((prev) => ({
...prev,
[name]: checked,
[name]: checked
}));
setSelectAll(false);
};
@@ -88,37 +90,7 @@ const DocumentDetails = (props) => {
setCurrentPage(1)
);
};
let ShowDocLinks = false;
let checkShowDocLinks = docsOffline != false ? true : false;
const getDocLink = (docsOffline) => {
var startTime = docsOffline.split(",")[0];
var endTime = docsOffline.split(",")[1];
// console.log(startTime);
// console.log(endTime);
var currentDate = new Date();
var startDate = new Date(currentDate.getTime());
startDate.setHours(startTime.split(":")[0]);
startDate.setMinutes(startTime.split(":")[1]);
startDate.setSeconds(startTime.split(":")[2]);
var endDate = new Date(currentDate.getTime());
endDate.setHours(endTime.split(":")[0]);
endDate.setMinutes(endTime.split(":")[1]);
endDate.setSeconds(endTime.split(":")[2]);
// console.log(startDate);
// console.log(endDate);
// console.log(currentDate);
var valid = startDate < currentDate && endDate > currentDate;
ShowDocLinks = valid;
};
checkShowDocLinks && getDocLink(docsOffline);
let ShowDocLinks = getDocLink(docsOffline);
const DocumentView = (documentDetailsArr) => {
documentDetailsArr = documentDetailsArr.value;
@@ -160,7 +132,7 @@ const DocumentDetails = (props) => {
? jsonpath({
path: '$..[?(@ && @.value=="All")].value_cy',
json: transLookup,
eval: true,
eval: true
})
: "All"}
</option>
@@ -179,7 +151,7 @@ const DocumentDetails = (props) => {
item.pinswg_isharedocumentlocationsLabel +
'")].value_cy',
json: transLookup,
eval: true,
eval: true
})
: item.pinswg_isharedocumentlocationsLabel ||
t(
@@ -287,12 +259,9 @@ const DocumentDetails = (props) => {
{/* {t("case:documents-date-published-label")} */}
<button
type="button"
onClick={() =>
sortDataBy("pinswg_documentpublisheddate")
}
onClick={() => sortDataBy("createdon")}
className={
orderByState ==
"pinswg_documentpublisheddate"
orderByState == "createdon"
? fieldSortState == "asc"
? `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16 sortAsc`
: `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16 sortDesc`
@@ -329,7 +298,7 @@ const DocumentDetails = (props) => {
incidentid +
"')]",
json: item,
sanbox: {},
sanbox: {}
});
detailsObj = item;
return (
@@ -361,7 +330,7 @@ const DocumentDetails = (props) => {
}
key={key}
onClick={() => {
toggleDownLoadLink(key),
(toggleDownLoadLink(key),
sendGAEvent(
"event",
"DownloadedFile",
@@ -369,9 +338,9 @@ const DocumentDetails = (props) => {
caseReference:
props.caseReference,
filename:
detailsObj.pinswg_name,
detailsObj.pinswg_name
}
);
));
}}
>
<span className="results-visually-hidden">
@@ -419,7 +388,7 @@ const DocumentDetails = (props) => {
] +
'")].value_cy',
json: transLookup,
sanbox: {},
eval: true
})
: detailsObj[
"pinswg_isharedocumentlocations@OData.Community.Display.V1.FormattedValue"
@@ -507,7 +476,7 @@ const DocumentDetails = (props) => {
</p>
<div>
<button onClick={clearCheckboxes}>
{t("case:documents-clear-filter-label")}
{t("search:clear-filter-button-label")}
</button>
</div>
<div className="govuk-checkboxes govuk-checkboxes--small">
@@ -584,8 +553,8 @@ const DocumentDetails = (props) => {
'$..[?(@ && @.value=="' +
item.pinswg_isharedocumentlocationsLabel +
'")].value_cy',
jsonL: transLookup,
sanbox: {},
json: transLookup,
eval: true
})
: item.pinswg_isharedocumentlocationsLabel ||
t(
@@ -605,13 +574,13 @@ const DocumentDetails = (props) => {
const data = [
...document.querySelectorAll(
".govuk-checkboxes__input:checked"
),
)
].map((e) => e.value);
setSelectedDocumentType(data);
setCurrentPage(1);
}}
>
{t("case:documents-filter-button")}
{t("search:filter-documents-label")}
</button>
</div>
{documentDetailsObj["@odata.count"] > 5 && (
@@ -700,14 +669,9 @@ const DocumentDetails = (props) => {
{/* {t("case:documents-date-published-label")} */}
<button
type="button"
onClick={() =>
sortDataBy(
"pinswg_documentpublisheddate"
)
}
onClick={() => sortDataBy("createdon")}
className={
orderByState ==
"pinswg_documentpublisheddate"
orderByState == "createdon"
? fieldSortState == "asc"
? `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16 sortAsc`
: `govuk-link govuk-link--no-underline govuk-!-font-weight-bold govuk-!-font-size-16 sortDesc`
@@ -746,7 +710,7 @@ const DocumentDetails = (props) => {
incidentid +
"')]",
json: item,
eval: true,
eval: true
});
detailsObj = item;
return (
@@ -772,7 +736,28 @@ const DocumentDetails = (props) => {
)}
{!ShowDocLinks ? (
<Link
<>
<DocumentLink
key={
detailsObj.pinswg_documentid
}
id={
detailsObj.pinswg_documentid
}
detailsObj={
detailsObj
}
caseReference={
caseReference
}
startDownload={
startDownload
}
getStatus={
getStatus
}
/>
{/* <Link
scroll={false}
href={
detailsObj.pinswg_hashlink
@@ -806,7 +791,8 @@ const DocumentDetails = (props) => {
)
? detailsObj.pinswg_name
: ""}
</Link>
</Link> */}
</>
) : (
<>
<span className="results-visually-hidden">
@@ -841,7 +827,7 @@ const DocumentDetails = (props) => {
] +
'")].value_cy',
json: transLookup,
eval: true,
eval: true
})
: detailsObj[
"pinswg_isharedocumentlocations@OData.Community.Display.V1.FormattedValue"
@@ -858,36 +844,14 @@ const DocumentDetails = (props) => {
:
</span>
{/* {
detailsObj.pinswg_documentpublisheddate
}
{
detailsObj.pinswg_latestpublisheddate
} */}
{detailsObj.pinswg_documentpublisheddate !=
null
? formatDates(
detailsObj.pinswg_documentpublisheddate
)
: formatDates(
detailsObj.pinswg_latestpublisheddate
)}
{/* {_.has(
detailsObj,
"pinswg_documentpublisheddate"
)
? formatDates(
detailsObj.pinswg_documentpublisheddate
)
: _.has(
{_.has(
detailsObj,
"pinswg_latestpublisheddate"
)
? formatDates(
detailsObj.pinswg_latestpublisheddate
)
: ""} */}
: ""}
</dd>
{/* <dd className="govuk-summary-list__value govuk-!-font-size-16">
<span className="results-visually-hidden">
@@ -931,9 +895,8 @@ const DocumentDetails = (props) => {
selectedOption,
selectedDocumentType
)
// .then((data) => data)
.then((data) => data)
.then((data) => {
setDocumentSearchState(true);
setDocumentDetails(data);
setShowSpinnerState(false);
});
@@ -942,7 +905,7 @@ const DocumentDetails = (props) => {
props.incidentid,
selectedOption,
selectedDocumentType,
setDocumentDetails,
setDocumentDetails
]
);
@@ -1003,10 +966,8 @@ const DocumentDetails = (props) => {
};
const [showDocumentSearchState, setDocumentSearchState] = useState(false);
const [orderByState, setOrderbyState] = useState(
"pinswg_documentpublisheddate"
);
const [fieldSortState, setfieldSortState] = useState("desc");
const [orderByState, setOrderbyState] = useState("createdon");
const [fieldSortState, setfieldSortState] = useState("asc");
let documentSearchState = () => {
showDocumentSearchState == true
@@ -1016,14 +977,14 @@ const DocumentDetails = (props) => {
const sortDataBy = (whichField) => {
setOrderbyState(whichField);
setfieldSortState("desc");
setfieldSortState("asc");
getDocumentResults(1, whichField, fieldSortState);
whichField == orderByState
? fieldSortState == "asc"
? setfieldSortState("desc")
: setfieldSortState("asc")
: setfieldSortState("desc");
? fieldSortState == "desc"
? setfieldSortState("asc")
: setfieldSortState("desc")
: setfieldSortState("asc");
setCurrentPage(1);
setShowSpinnerState(true);
};
@@ -1058,7 +1019,7 @@ const DocumentDetails = (props) => {
return docObj;
};
//const searchDocumentResultsObj = docDetailsObj();
const searchDocumentResultsObj = docDetailsObj();
}, [
incidentid,
setDocumentDetails,
@@ -1068,6 +1029,7 @@ const DocumentDetails = (props) => {
orderByState,
getDocumentTypes,
getDocumentResults,
lang
]);
return (
<div className="govuk-grid-row">
@@ -1097,7 +1059,7 @@ const DocumentDetails = (props) => {
const mapStateToProps = (state) => {
return {
...state,
...state
};
};
@@ -1108,7 +1070,7 @@ const mapDispatchToProps = (dispatch) => {
},
setCurrentPage: (currentPage) => {
dispatch(setCurrentPage(currentPage));
},
}
};
};
+115 -72
View File
@@ -3,9 +3,16 @@ import { setCurrentPage } from "../../store/currentView/action";
import { connect } from "react-redux";
import useTranslation from "next-translate/useTranslation";
import { formatDates } from "../utils";
import {
getLivePublishedEvent,
isEventLiveNow
} from "./summary/utils/liveEvent";
import { useRouter } from "next/router";
const EventsDetails = (props) => {
let { t } = useTranslation();
const router = useRouter();
const { locale } = router;
const EventView = (eventsArr) => {
eventsArr = eventsArr.value;
@@ -23,8 +30,6 @@ const EventsDetails = (props) => {
return obj.hasOwnProperty(property) && obj[property] !== null;
}
//console.log(eventsArr.length > 0);
return (
eventsArr.length > 0 && (
<div className="govuk-grid-row">
@@ -63,76 +68,26 @@ const EventsDetails = (props) => {
{item.pinswg_eventname}
</div>
)}
</div>
<div className="eventColumn">
{hasOwnPropertyAndNotNull(
item,
"pinswg_eventaddressline1"
[
"pinswg_typeofevent@OData.Community.Display.V1.FormattedValue"
]
) && (
<>
<div>
<b>
{t(
"case:event-address-label"
"case:event-type-of-event-label"
)}
</b>
:
</div>
<div>
:{" "}
{
item.pinswg_eventaddressline1
}
</div>
</>
)}
{hasOwnPropertyAndNotNull(
item,
"pinswg_eventaddressline2"
) && (
<div>
{
item.pinswg_eventaddressline2
item[
"pinswg_typeofevent@OData.Community.Display.V1.FormattedValue"
]
}
</div>
)}
{hasOwnPropertyAndNotNull(
item,
"pinswg_eventaddresstown"
) && (
<div>
{
item.pinswg_eventaddresstown
}
</div>
)}
{hasOwnPropertyAndNotNull(
item,
"pinswg_eventaddresscounty"
) && (
<div>
{
item.pinswg_eventaddresscounty
}
</div>
)}
{hasOwnPropertyAndNotNull(
item,
"pinswg_eventaddresscounty"
) && (
<div>
{
item.pinswg_eventaddresspostcode
}
</div>
)}
</div>
{/* type of event
start date date of event - end date of event
if virtual event no address showNoOfRecords
show event address */}
<div className="eventColumn">
{hasOwnPropertyAndNotNull(
item,
"pinswg_dateeventrequested"
@@ -171,27 +126,115 @@ const EventsDetails = (props) => {
<div className="eventColumn">
{hasOwnPropertyAndNotNull(
item,
[
"pinswg_typeofevent@OData.Community.Display.V1.FormattedValue",
]
"pinswg_eventaddressline1"
) && (
<div>
<>
<div
style={{
display: "flex"
}}
>
<b>
{t(
"case:event-type-of-event-label"
"case:event-address-label"
)}
</b>
:{" "}
:
<div
style={{
display:
"flex",
marginLeft:
"10px",
flexDirection:
"column"
}}
>
{hasOwnPropertyAndNotNull(
item,
"pinswg_eventaddressline1"
) && (
<span>
{
item[
"pinswg_typeofevent@OData.Community.Display.V1.FormattedValue"
]
item.pinswg_eventaddressline1
}
</div>
</span>
)}
{hasOwnPropertyAndNotNull(
item,
"pinswg_eventaddressline2"
) && (
<span>
{
item.pinswg_eventaddressline2
}
</span>
)}
{hasOwnPropertyAndNotNull(
item,
"pinswg_eventaddresstown"
) && (
<span>
{
item.pinswg_eventaddresstown
}
</span>
)}
{hasOwnPropertyAndNotNull(
item,
"pinswg_eventaddresscounty"
) && (
<span>
{
item.pinswg_eventaddresscounty
}
</span>
)}
{hasOwnPropertyAndNotNull(
item,
"pinswg_eventaddresscounty"
) && (
<span>
{
item.pinswg_eventaddresspostcode
}
</span>
)}
</div>
</div>
</>
)}
</div>
{/* type of event
start date date of event - end date of event
if virtual event no address showNoOfRecords
show event address */}
{hasOwnPropertyAndNotNull(
item,
"pinswg_linktotheevent"
) &&
isEventLiveNow(item) && (
<div className="eventColumn">
<a
href={
locale === "en"
? item.pinswg_linktotheevent
: item.pinswg_linktotheeventstreamwelsh
}
className="govuk-link"
target="_blank"
rel="noopener noreferrer"
>
{t(
"case:live-event-link"
)}
</a>
</div>
)}
</div>
</>
)
)}{" "}
</div>
@@ -203,7 +246,7 @@ const EventsDetails = (props) => {
const mapStateToProps = (state) => {
return {
...state,
...state
};
};
@@ -214,7 +257,7 @@ const mapDispatchToProps = (dispatch) => {
},
setCurrentPage: (currentPage) => {
dispatch(setCurrentPage(currentPage));
},
}
};
};
+646
View File
@@ -0,0 +1,646 @@
// // import { setEventDetails } from "../../store/searchOutput/action";
// // import { setCurrentPage } from "../../store/currentView/action";
// // import { connect } from "react-redux";
// // import useTranslation from "next-translate/useTranslation";
// // import { formatDates } from "../utils";
// // const MediaDetails = (props) => {
// // let { t } = useTranslation();
// // const MediaView = (mediaArr) => {
// // mediaArr = mediaArr.value;
// // return mediaArr.map((item, key) => {
// // <div>{item.pinswg_name}</div>;
// // });
// // };
// // const mediaArr = props.mediaObj?.value || [];
// // // event publishiing flag set to true displa
// // function hasOwnPropertyAndNotNull(obj, property) {
// // return obj.hasOwnProperty(property) && obj[property] !== null;
// // }
// // //console.log(eventsArr.length > 0);
// // return (
// // mediaArr.length > 0 && (
// // <div className="govuk-grid-row">
// // <div className="card">
// // <div className="card-body">
// // <h2 className="govuk-heading-m ">
// // {t("case:event-title")}Media
// // </h2>
// // {mediaArr.map(
// // (item, key) =>
// // item.pinswg_publishtoweb && (
// // <>
// // {" "}
// // <div className="">
// // <div className="">
// // <div>
// // <b>
// // {" "}
// // {t(
// // "case:event-name-label",
// // )}
// // </b>
// // : {item.pinswg_name}
// // </div>
// // {hasOwnPropertyAndNotNull(
// // item,
// // "pinswg_description",
// // ) && (
// // <div>
// // <b>
// // {t(
// // "case:event-event-name-label",
// // )}
// // </b>
// // :{" "}
// // {
// // item.pinswg_description
// // }
// // </div>
// // )}
// // </div>
// // </div>
// // </>
// // ),
// // )}{" "}
// // </div>
// // </div>
// // </div>
// // )
// // );
// // };
// // const mapStateToProps = (state) => {
// // return {
// // ...state,
// // };
// // };
// // const mapDispatchToProps = (dispatch) => {
// // return {
// // setEventDetails: (mediaObj) => {
// // dispatch(setEventDetails(mediaObj));
// // },
// // setCurrentPage: (currentPage) => {
// // dispatch(setCurrentPage(currentPage));
// // },
// // };
// // };
// // export default connect(mapStateToProps, mapDispatchToProps)(MediaDetails);
// import { useEffect, useMemo, useState } from "react";
// import { connect } from "react-redux";
// import useTranslation from "next-translate/useTranslation";
// import { useRouter } from "next/router";
// const extractYouTubeId = (input) => {
// if (!input) return null;
// if (/^[a-zA-Z0-9_-]{11}$/.test(input)) return input;
// try {
// const url = new URL(input);
// if (url.hostname.includes("youtu.be"))
// return url.pathname.slice(1) || null;
// const v = url.searchParams.get("v");
// if (v) return v;
// const embedMatch = url.pathname.match(/\/embed\/([^/]+)/);
// if (embedMatch?.[1]) return embedMatch[1];
// const shortsMatch = url.pathname.match(/\/shorts\/([^/]+)/);
// if (shortsMatch?.[1]) return shortsMatch[1];
// return null;
// } catch {
// return null;
// }
// };
// export default function MediaDetails({ mediaObj, whichTab }) {
// const router = useRouter();
// const mediaArr = mediaObj?.value || [];
// const videoItems = useMemo(() => {
// const isWelsh = router.locale === "cy";
// return (mediaArr || [])
// .filter((x) => x?.pinswg_publishtoweb)
// .map((item) => {
// const url = isWelsh
// ? item.pinswg_mediaLinkCY
// : item.pinswg_mediaLinkEN;
// const videoId = extractYouTubeId(url);
// if (!videoId) return null;
// return {
// key:
// item.pinswg_documentid ||
// `${item.pinswg_name}-${videoId}`,
// videoId,
// name: item.pinswg_name || "Video",
// description: item.pinswg_description || "",
// thumbnail: `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`,
// };
// })
// .filter(Boolean);
// }, [mediaArr, router.locale]);
// const [watchedIds, setWatchedIds] = useState(new Set());
// const [selectedIndex, setSelectedIndex] = useState(0);
// // Forces a fresh iframe instance whenever we need to hard-stop playback
// const [playerInstance, setPlayerInstance] = useState(0);
// const isActive = whichTab === "case-media";
// const selected = videoItems[selectedIndex] || null;
// // When leaving the media tab, hard-stop playback by unmounting (and bumping instance)
// useEffect(() => {
// if (!isActive) {
// setPlayerInstance((n) => n + 1);
// }
// }, [isActive]);
// // When list/locale changes, reset to first (no autoplay)
// useEffect(() => {
// setSelectedIndex(0);
// setPlayerInstance((n) => n + 1);
// }, [videoItems.length, router.locale]);
// if (!videoItems.length) return null;
// const embedBase = "https://www.youtube-nocookie.com/embed";
// // IMPORTANT:
// // - autoplay=0 for initial load
// // - autoplay=1 only when user clicks an item
// const [autoplay, setAutoplay] = useState(false);
// useEffect(() => {
// // whenever we leave tab or reset player, don't autoplay next time
// if (!isActive) setAutoplay(false);
// }, [isActive]);
// const embedSrc = selected
// ? `${embedBase}/${selected.videoId}?rel=0&modestbranding=1&playsinline=1&autoplay=${autoplay ? "1" : "0"}`
// : "";
// return (
// <div className="govuk-grid-row">
// <div className="card">
// <div className="card-body">
// <h2 className="govuk-heading-m">Media</h2>
// {/* Player: only render when tab is active */}
// {isActive && (
// <div className="videoWrapper govuk-!-margin-bottom-4">
// <iframe
// key={`${selected?.videoId}-${playerInstance}`}
// src={embedSrc}
// title={selected?.name || "YouTube video"}
// frameBorder="0"
// allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
// allowFullScreen
// />
// </div>
// )}
// <h3 className="govuk-heading-s">Videos</h3>
// <ul className="govuk-list govuk-list--spaced">
// {videoItems.map((v, idx) => {
// const isSelected = idx === selectedIndex;
// return (
// <li key={v.key}>
// <button
// type="button"
// className={`videoTabButton ${isSelected ? "active" : ""}`}
// onClick={() => {
// setSelectedIndex(idx);
// setAutoplay(true); // play only on user action
// setPlayerInstance((n) => n + 1); // ensure clean switch / stop previous
// setWatchedIds((prev) =>
// new Set(prev).add(v.videoId),
// );
// }}
// >
// <div className="videoTabContent">
// <div className="thumbnailWrapper">
// <img
// src={v.thumbnail}
// alt=""
// className="videoThumbnail"
// loading="lazy"
// />
// <span
// className="playIcon"
// aria-hidden="true"
// >
// ▶
// </span>
// {/* Duration badge (see section 2) */}
// {v.duration && (
// <span className="durationBadge">
// {v.duration}
// </span>
// )}
// {/* Watched indicator (see section 3) */}
// {watchedIds.has(v.videoId) && (
// <span className="watchedBadge">
// Watched
// </span>
// )}
// </div>
// <span className="videoTitle">
// {v.name}
// </span>
// </div>
// </button>
// </li>
// );
// })}
// </ul>
// </div>
// </div>
// <style jsx>{`
// .videoWrapper {
// position: relative;
// padding-bottom: 56.25%;
// height: 0;
// overflow: hidden;
// border: 1px solid #b1b4b6;
// border-radius: 4px;
// }
// .videoWrapper iframe {
// position: absolute;
// top: 0;
// left: 0;
// width: 100%;
// height: 100%;
// }
// .videoTabButton {
// width: 100%;
// text-align: left;
// background: transparent;
// border: 1px solid #b1b4b6;
// border-radius: 4px;
// padding: 10px 12px;
// cursor: pointer;
// }
// .videoTabButton:hover {
// background: #f3f2f1;
// }
// .videoTabButton.active {
// border-color: #1d70b8;
// box-shadow: inset 0 0 0 1px #1d70b8;
// background: #eef4ff;
// }
// .videoTabContent {
// display: flex;
// gap: 12px;
// align-items: center;
// }
// .videoThumbnail {
// width: 120px;
// aspect-ratio: 16 / 9;
// object-fit: cover;
// border-radius: 4px;
// border: 1px solid #b1b4b6;
// background: #000;
// }
// .videoTitle {
// font-weight: 600;
// }
// @media (max-width: 40.0625em) {
// .videoThumbnail {
// width: 90px;
// }
// }
// .thumbnailWrapper {
// position: relative;
// width: 120px;
// flex-shrink: 0;
// }
// .videoThumbnail {
// width: 100%;
// aspect-ratio: 16 / 9;
// object-fit: cover;
// border-radius: 4px;
// border: 1px solid #b1b4b6;
// background: #000;
// }
// /* ▶ Play icon */
// .playIcon {
// position: absolute;
// inset: 0;
// display: flex;
// align-items: center;
// justify-content: center;
// font-size: 32px;
// color: white;
// text-shadow: 0 0 6px rgba(0, 0, 0, 0.7);
// pointer-events: none;
// }
// /* ⏱ Duration badge */
// .durationBadge {
// position: absolute;
// bottom: 6px;
// right: 6px;
// background: rgba(0, 0, 0, 0.8);
// color: white;
// font-size: 12px;
// padding: 2px 6px;
// border-radius: 3px;
// }
// /* 👁 Watched badge */
// .watchedBadge {
// position: absolute;
// top: 6px;
// left: 6px;
// background: #00703c; /* GOV green */
// color: white;
// font-size: 11px;
// padding: 2px 6px;
// border-radius: 3px;
// }
// .videoTabContent {
// display: flex;
// gap: 12px;
// align-items: center;
// }
// .videoTitle {
// font-weight: 600;
// }
// `}</style>
// </div>
// );
// }
import { useEffect, useMemo, useState } from "react";
import useTranslation from "next-translate/useTranslation";
import { useRouter } from "next/router";
/**
* MediaDetails
* - Renders a YouTube player + a list of videos from mediaObj.value
* - Loads first item by default (no autoplay)
* - Plays only when user selects an item
* - Stops playback when leaving the "case-media" parent tab (pass whichTab)
* - Shows thumbnail with play overlay
* - Optional duration badge (if you supply v.duration)
* - "Watched" badge once a video has been played (session only)
*/
const extractYouTubeId = (input) => {
if (!input) return null;
// Already looks like a YouTube ID?
if (/^[a-zA-Z0-9_-]{11}$/.test(input)) return input;
try {
const url = new URL(input);
// youtu.be/<id>
if (url.hostname.includes("youtu.be")) {
return url.pathname.replace("/", "") || null;
}
// youtube.com/watch?v=<id>
const v = url.searchParams.get("v");
if (v) return v;
// youtube.com/embed/<id>
const embedMatch = url.pathname.match(/\/embed\/([^/]+)/);
if (embedMatch?.[1]) return embedMatch[1];
// youtube.com/shorts/<id>
const shortsMatch = url.pathname.match(/\/shorts\/([^/]+)/);
if (shortsMatch?.[1]) return shortsMatch[1];
return null;
} catch {
return null;
}
};
const MediaDetails = ({ mediaObj, whichTab, title, useNoCookie = true }) => {
const { t } = useTranslation();
const router = useRouter();
const sourceMedia = mediaObj?.value;
const isActive = whichTab ? whichTab === "case-media" : true;
// Build list of playable items (publishable only)
const videoItems = useMemo(() => {
const isWelsh = router.locale === "cy";
return (sourceMedia || [])
.filter((x) => x?.pinswg_publishtoweb)
.map((item) => {
const link = isWelsh
? item.pinswg_videoplatformurlwelsh
: item.pinswg_videoplatformurl;
const videoId = extractYouTubeId(link);
if (!videoId) return null;
return {
key:
item.pinswg_documentid ||
item.pinswg_mediaid ||
`${item.pinswg_eventrecordingid}-${videoId}`,
videoId,
name: item.pinswg_eventrecordingname || "Video",
description: item.pinswg_description || "",
// If you later store duration in CRM, map it here (e.g. "03:42")
duration: item.pinswg_duration || null,
thumbnailHQ: `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`,
thumbnailMax: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
raw: item
};
})
.filter(Boolean);
}, [sourceMedia, router.locale]);
const [selectedIndex, setSelectedIndex] = useState(0);
// Default false so first item loads but does not play.
const [autoplay, setAutoplay] = useState(false);
// Bump this to force iframe remount (hard stop playback).
const [playerInstance, setPlayerInstance] = useState(0);
// Track which videos have been played (session only).
const [watchedIds, setWatchedIds] = useState(() => new Set());
// When list or locale changes: reset selection and do not autoplay
useEffect(() => {
setSelectedIndex(0);
setAutoplay(false);
setPlayerInstance((n) => n + 1);
}, [videoItems.length, router.locale]);
// When leaving the media tab: hard stop playback
useEffect(() => {
if (!isActive) {
setAutoplay(false);
setPlayerInstance((n) => n + 1);
}
}, [isActive]);
const selected = videoItems[selectedIndex] || null;
const domain = useNoCookie
? "https://www.youtube-nocookie.com"
: "https://www.youtube.com";
const embedSrc = selected
? `${domain}/embed/${selected.videoId}?rel=0&modestbranding=1&playsinline=1&autoplay=${
autoplay ? "1" : "0"
}`
: "";
if (!videoItems.length) return null;
return (
<div className="govuk-grid-row">
<div className="card">
<div className="card-body">
<h2 className="govuk-heading-m">{t("case:media-title")}</h2>
{/* Player: unmount when not active (guarantees playback stops) */}
{isActive && (
<div className="videoWrapper govuk-!-margin-bottom-4">
<iframe
key={`${selected?.videoId}-${playerInstance}`}
src={embedSrc}
title={selected?.name || "YouTube video"}
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
/>
</div>
)}
{/* Selected info */}
{selected && (
<div className="govuk-!-margin-bottom-4">
<div className="govuk-body">
<b>{t("case:event-name-label")}</b>:{" "}
{selected.name}
</div>
{!!selected.description && (
<div className="govuk-body">
<b>{t("case:event-event-name-label")}</b>:{" "}
{selected.description}
</div>
)}
</div>
)}
{/* Video list */}
<h3 className="govuk-heading-s">
{t("case:video-heading")}
</h3>
<ul className="govuk-list govuk-list--spaced">
{videoItems.map((v, idx) => {
const isSelected = idx === selectedIndex;
return (
<li key={v.key}>
<button
type="button"
className={`videoTabButton ${
isSelected ? "active" : ""
}`}
aria-current={
isSelected ? "true" : undefined
}
onClick={() => {
setSelectedIndex(idx);
// Start playback only when user selects
setAutoplay(true);
// Force remount to stop previous + start clean
setPlayerInstance((n) => n + 1);
// Mark watched
setWatchedIds((prev) => {
const next = new Set(prev);
next.add(v.videoId);
return next;
});
}}
>
<div className="videoTabContent">
<div className="thumbnailWrapper">
<img
src={v.thumbnailMax}
onError={(e) => {
// fallback for videos without maxres
e.currentTarget.src =
v.thumbnailHQ;
}}
alt=""
className="videoThumbnail"
loading="lazy"
/>
<span
className="playIcon"
aria-hidden="true"
>
</span>
{!!v.duration && (
<span className="durationBadge">
{v.duration}
</span>
)}
{watchedIds.has(v.videoId) && (
<span className="watchedBadge">
Watched
</span>
)}
</div>
<span className="videoTitle">
{v.name}
</span>
</div>
</button>
</li>
);
})}
</ul>
</div>
</div>
</div>
);
};
export default MediaDetails;
+13 -12
View File
@@ -13,7 +13,7 @@ const PaginationControl = (props) => {
currentPage,
spinnerState,
getDocumentResults,
setCurrentPage,
setCurrentPage
} = props;
let { t } = useTranslation();
@@ -45,7 +45,7 @@ const PaginationControl = (props) => {
const range = {
start: Math.round(currentPage - delta / 2),
end: Math.round(currentPage + delta / 2),
end: Math.round(currentPage + delta / 2)
};
if (range.start - 1 === 1 || range.end + 1 === pageCount) {
@@ -89,7 +89,7 @@ const PaginationControl = (props) => {
<span
className="govuk-body govuk-link govuk-!-font-weight-bold govuk-link--no-underline activePaginationItem leftTextButton"
onClick={() => {
spinnerState(),
(spinnerState(),
setCurrentPage(
props.currentView.currentPage -
1
@@ -99,7 +99,7 @@ const PaginationControl = (props) => {
1,
orderBy,
fieldSort
);
));
}}
>
{t("common:previous-link-button")}
@@ -132,12 +132,12 @@ const PaginationControl = (props) => {
key={i}
onClick={() => {
spinnerState();
setCurrentPage(e),
(setCurrentPage(e),
getDocumentResults(
e,
orderBy,
fieldSort
);
));
}}
>
{e}
@@ -150,7 +150,7 @@ const PaginationControl = (props) => {
<span
className="govuk-body govuk-link govuk-!-font-weight-bold govuk-link--no-underline activePaginationItem rightTextButton"
onClick={() => {
spinnerState(),
(spinnerState(),
setCurrentPage(
props.currentView.currentPage +
1
@@ -160,7 +160,7 @@ const PaginationControl = (props) => {
1,
orderBy,
fieldSort
);
));
}}
>
{t("common:next-link-button")}
@@ -174,8 +174,9 @@ const PaginationControl = (props) => {
</div>
</div>
<div className="govuk-body-s mobilePageCount">
{t("common:pages-label")} {currentPage}{" "}
{t("common:of-label")} {pagesCount}
{t("common:pages-label")}{" "}
{props.currentView.currentPage} {t("common:of-label")}{" "}
{pagesCount}
</div>
</div>
)}
@@ -185,7 +186,7 @@ const PaginationControl = (props) => {
const mapStateToProps = (state) => {
return {
currentView: state.currentView,
currentView: state.currentView
};
};
@@ -193,7 +194,7 @@ const mapDispatchToProps = (dispatch) => {
return {
setCurrentPage: (currentPage) => {
dispatch(setCurrentPage(currentPage));
},
}
};
};
@@ -2,22 +2,22 @@ import useTranslation from "next-translate/useTranslation";
import { useRouter } from "next/router";
import { connect } from "react-redux";
import { Field, change, reduxForm } from "redux-form";
import { useState } from "react";
import { useEffect, useState } from "react";
import { bytesToSize, getThumbnailIconByExtension } from "../../utils";
import {
FileUploadField,
RenderCondtionalRadioList,
RenderPickList,
RenderRichMultiline,
shouldDisableButton,
shouldDisableButton
} from "./representationElements";
import RepresentationTypeSelectorBlock from "./elements/RepresentationTypeSelectorBlock";
import { useDropzone } from "react-dropzone";
import {
setRepresentationCapacity,
setRepresentationSubmit,
setFilesForRepresentations,
setFilesForRepresentations
} from "../../../store/currentView/action";
const ConsultationAgent = (props) => {
@@ -36,9 +36,23 @@ const ConsultationAgent = (props) => {
setSavingStatus,
savingStatus,
setFilesForRepresentations,
onRepresentationTypeChange,
onTypeSelectionConfirmed
} = props;
const { acceptedFiles, getRootProps, getInputProps } = useDropzone();
const persistedRepresentationType =
currentView?.caseReference?.repDetails?.representationType;
const [typeSelectionConfirmed, setTypeSelectionConfirmed] = useState(
!!persistedRepresentationType
);
useEffect(() => {
if (persistedRepresentationType) {
setTypeSelectionConfirmed(true);
}
}, [persistedRepresentationType]);
if (JSON.stringify(formObj) != "{}") {
setRepFileName(formObj.representationForm.values);
@@ -63,63 +77,43 @@ const ConsultationAgent = (props) => {
));
const required = (value) => (value ? undefined : "Required");
const selectedRepresentationType =
formObj?.representationForm?.values?.representationType ||
props.representationType;
const isTypeConfirmed =
!!selectedRepresentationType && typeSelectionConfirmed;
const selectedRepresentationTypeDisplay =
router.locale == "cy"
? selectedRepresentationType == "Questionnaire"
? "Holiadur"
: selectedRepresentationType == "Statement"
? "Datganiad"
: selectedRepresentationType == "Final comments"
? "Sylwadau terfynol"
: selectedRepresentationType
: selectedRepresentationType;
return (
<>
<div className="govuk-grid-row">
<div className="govuk-form-group">
{typeof formObj.representationForm.values
.representationType == "undefined" &&
props.representationType != "Select..." ? (
<>
<h2 className="govuk-heading-m ">
{t(
<RepresentationTypeSelectorBlock
t={t}
headingText={t(
"myrepresentations:consultation-from-an-agent-heading"
)}
</h2>
<label
className="govuk-label govuk-body-m"
htmlFor="representationType"
>
{t(
kindLabelText={t(
"myrepresentations:kind-of-consultation-label"
)}
</label>
<Field
name="representationType"
component={RenderPickList}
datafieldname="representationType"
optionsArr={
appellantApplicantRepresentationArr
}
isTypeSelected={isTypeConfirmed}
selectedTypeDisplay={selectedRepresentationTypeDisplay}
representationType={selectedRepresentationType}
setTypeSelectionConfirmed={setTypeSelectionConfirmed}
onRepresentationTypeChange={onRepresentationTypeChange}
onTypeSelectionConfirmed={onTypeSelectionConfirmed}
optionsArr={appellantApplicantRepresentationArr}
/>
</>
) : (
<label className="govuk-label govuk-body-m">
{t("myrepresentations:representation-type")}:{" "}
{router.locale == "cy"
? formObj.representationForm.values
.representationType == "Questionnaire"
? "Holiadur"
: formObj.representationForm.values
.representationType == "Statement"
? "Datganiad"
: formObj.representationForm.values
.representationType ==
"Final comments"
? "Sylwadau terfynol"
: formObj.representationForm.values
.representationType
: formObj.representationForm.values
.representationType}
</label>
)}
</div>
</div>
{(typeof props.representationType != "undefined" ||
typeof formObj.representationForm.values
.representationType != "undefined") && (
{isTypeConfirmed && (
<>
<div className="govuk-grid-row">
<div className="govuk-form-group">
@@ -146,8 +140,7 @@ const ConsultationAgent = (props) => {
<div className="govuk-grid-row">
<div className="govuk-form-group">
<p className="govuk-body">
{formObj.representationForm.values
.representationType ==
{selectedRepresentationType ==
"Consultation Response"
? t(
"myrepresentations:enter-comment-label"
@@ -160,8 +153,7 @@ const ConsultationAgent = (props) => {
className="govuk-fieldset"
aria-describedby="commentBox-hint"
>
{formObj.representationForm.values
.representationType ==
{selectedRepresentationType ==
"Consultation Response" && (
<div className="govuk-form-group">
<Field
@@ -265,8 +257,7 @@ const ConsultationAgent = (props) => {
</div>
</>
)}
{typeof formObj.representationForm.values.representationType !=
"undefined" && (
{isTypeConfirmed && (
<div className="govuk-grid-row">
<div className="govuk-button-group">
<button
@@ -275,7 +266,7 @@ const ConsultationAgent = (props) => {
data-module="govuk-button"
disabled={shouldDisableButton({
props,
formObj,
formObj
})}
>
{t("common:continue-button")}
@@ -349,7 +340,7 @@ const mapDispatchToProps = (dispatch) => ({
setFilesForRepresentations: (fileList) => {
dispatch(setFilesForRepresentations(fileList));
},
}
});
export default connect(
@@ -359,6 +350,6 @@ export default connect(
reduxForm({
form: "representationForm",
enableReinitialize: true,
destroyOnUnmount: false,
destroyOnUnmount: false
})(ConsultationAgent)
);
@@ -2,21 +2,21 @@ import useTranslation from "next-translate/useTranslation";
import { useRouter } from "next/router";
import { connect } from "react-redux";
import { Field, change, reduxForm } from "redux-form";
import { useState } from "react";
import { useEffect, useState } from "react";
import { bytesToSize, getThumbnailIconByExtension } from "../../utils";
import {
FileUploadField,
RenderPickList,
RenderRichMultiline,
shouldDisableButton,
shouldDisableButton
} from "./representationElements";
import RepresentationTypeSelectorBlock from "./elements/RepresentationTypeSelectorBlock";
import { useDropzone } from "react-dropzone";
import {
setRepresentationCapacity,
setRepresentationSubmit,
setFilesForRepresentations,
setFilesForRepresentations
} from "../../../store/currentView/action";
const ConsultationAppellant = (props) => {
@@ -35,9 +35,23 @@ const ConsultationAppellant = (props) => {
setSavingStatus,
savingStatus,
setFilesForRepresentations,
onRepresentationTypeChange,
onTypeSelectionConfirmed
} = props;
const { acceptedFiles, getRootProps, getInputProps } = useDropzone();
const persistedRepresentationType =
currentView?.caseReference?.repDetails?.representationType;
const [typeSelectionConfirmed, setTypeSelectionConfirmed] = useState(
!!persistedRepresentationType
);
useEffect(() => {
if (persistedRepresentationType) {
setTypeSelectionConfirmed(true);
}
}, [persistedRepresentationType]);
console.log(JSON.stringify(formObj) == "{}" ? "empty" : "value");
if (JSON.stringify(formObj) != "{}") {
@@ -62,66 +76,47 @@ const ConsultationAppellant = (props) => {
index === self.findIndex((t) => t.name === value.name)
);
const selectedRepresentationType =
formObj?.representationForm?.values?.representationType ||
props.representationType;
const isTypeConfirmed =
!!selectedRepresentationType && typeSelectionConfirmed;
const selectedRepresentationTypeDisplay =
router.locale == "cy"
? selectedRepresentationType == "Questionnaire"
? "Holiadur"
: selectedRepresentationType == "Statement"
? "Datganiad"
: selectedRepresentationType == "Final comments"
? "Sylwadau terfynol"
: selectedRepresentationType
: selectedRepresentationType;
return (
<>
<div className="govuk-grid-row">
<div className="govuk-form-group">
{typeof formObj.representationForm.values
.representationType == "undefined" &&
props.representationType != "Select..." ? (
<>
<h2 className="govuk-heading-m ">
{t(
<RepresentationTypeSelectorBlock
t={t}
headingText={t(
"myrepresentations:consultation-from-an-appellant-heading"
)}
</h2>
<label
className="govuk-label govuk-body-m"
htmlFor="representationType"
>
{t(
kindLabelText={t(
"myrepresentations:kind-of-consultation-label"
)}
</label>
<Field
name="representationType"
component={RenderPickList}
datafieldname="representationType"
optionsArr={
appellantApplicantRepresentationArr
}
isTypeSelected={isTypeConfirmed}
selectedTypeDisplay={selectedRepresentationTypeDisplay}
representationType={selectedRepresentationType}
setTypeSelectionConfirmed={setTypeSelectionConfirmed}
onRepresentationTypeChange={onRepresentationTypeChange}
onTypeSelectionConfirmed={onTypeSelectionConfirmed}
optionsArr={appellantApplicantRepresentationArr}
/>
</>
) : (
<label className="govuk-label govuk-body-m">
{t("myrepresentations:representation-type")}:{" "}
{router.locale == "cy"
? formObj.representationForm.values
.representationType == "Questionnaire"
? "Holiadur"
: formObj.representationForm.values
.representationType == "Statement"
? "Datganiad"
: formObj.representationForm.values
.representationType ==
"Final comments"
? "Sylwadau terfynol"
: formObj.representationForm.values
.representationType
: formObj.representationForm.values
.representationType}
</label>
)}
</div>
</div>
{(typeof props.representationType != "undefined" ||
typeof formObj.representationForm.values
.representationType != "undefined") && (
{isTypeConfirmed && (
<div className="govuk-grid-row">
<div className="govuk-form-group">
<p className="govuk-body">
{formObj.representationForm.values
.representationType ==
{selectedRepresentationType ==
"Consultation Response"
? t("myrepresentations:enter-comment-label")
: t(
@@ -132,8 +127,7 @@ const ConsultationAppellant = (props) => {
className="govuk-fieldset"
aria-describedby="commentBox-hint"
>
{formObj.representationForm.values
.representationType ==
{selectedRepresentationType ==
"Consultation Response" && (
<div className="govuk-form-group">
<Field
@@ -233,8 +227,7 @@ const ConsultationAppellant = (props) => {
</div>
)}
{typeof formObj.representationForm.values.representationType !=
"undefined" && (
{isTypeConfirmed && (
<div className="govuk-grid-row">
<div className="govuk-button-group">
<button
@@ -243,7 +236,7 @@ const ConsultationAppellant = (props) => {
data-module="govuk-button"
disabled={shouldDisableButton({
props,
formObj,
formObj
})}
>
{t("common:continue-button")}
@@ -253,7 +246,6 @@ const ConsultationAppellant = (props) => {
onClick={() => {
setRepresentationCapacity();
props.updateField(
"representationForm",
"representationType",
""
);
@@ -321,7 +313,7 @@ const mapDispatchToProps = (dispatch) => {
setFilesForRepresentations: (fileList) => {
dispatch(setFilesForRepresentations(fileList));
},
}
};
};
@@ -332,6 +324,6 @@ export default connect(
reduxForm({
form: "representationForm",
enableReinitialize: true,
destroyOnUnmount: false,
destroyOnUnmount: false
})(ConsultationAppellant)
);
@@ -2,22 +2,22 @@ import useTranslation from "next-translate/useTranslation";
import { useRouter } from "next/router";
import { connect } from "react-redux";
import { Field, change, reduxForm } from "redux-form";
import { useState } from "react";
import { useEffect, useState } from "react";
import { bytesToSize, getThumbnailIconByExtension } from "../../utils";
import {
FileUploadField,
RenderCondtionalRadioList,
RenderPickList,
RenderRichMultiline,
shouldDisableButton,
shouldDisableButton
} from "./representationElements";
import RepresentationTypeSelectorBlock from "./elements/RepresentationTypeSelectorBlock";
import { useDropzone } from "react-dropzone";
import {
setRepresentationCapacity,
setRepresentationSubmit,
setFilesForRepresentations,
setFilesForRepresentations
} from "../../../store/currentView/action";
const ConsultationInterestedPartyPerson = (props) => {
@@ -35,11 +35,25 @@ const ConsultationInterestedPartyPerson = (props) => {
setSavingStatus,
savingStatus,
setFilesForRepresentations,
onRepresentationTypeChange,
onTypeSelectionConfirmed
} = props;
const required = (value) => (value ? undefined : "Required");
const { acceptedFiles, getRootProps, getInputProps } = useDropzone();
const persistedRepresentationType =
props.currentView?.caseReference?.repDetails?.representationType;
const [typeSelectionConfirmed, setTypeSelectionConfirmed] = useState(
!!persistedRepresentationType
);
useEffect(() => {
if (persistedRepresentationType) {
setTypeSelectionConfirmed(true);
}
}, [persistedRepresentationType]);
if (JSON.stringify(formObj) != "{}") {
setRepFileName(formObj.representationForm.values);
@@ -62,67 +76,46 @@ const ConsultationInterestedPartyPerson = (props) => {
{file.path} - {file.size} bytes
</li>
));
const selectedRepresentationType =
formObj?.representationForm?.values?.representationType ||
props.representationType;
const isTypeConfirmed =
!!selectedRepresentationType && typeSelectionConfirmed;
const selectedRepresentationTypeDisplay =
router.locale == "cy"
? selectedRepresentationType == "Questionnaire"
? "Holiadur"
: selectedRepresentationType == "Statement"
? "Datganiad"
: selectedRepresentationType == "Final comments"
? "Sylwadau terfynol"
: selectedRepresentationType
: selectedRepresentationType;
return (
<>
<div className="govuk-grid-row">
<div className="govuk-form-group">
{typeof formObj.representationForm.values
.representationType == "undefined" &&
props.representationType != "Select..." ? (
<>
<h2 className="govuk-heading-m ">
{t(
<RepresentationTypeSelectorBlock
t={t}
headingText={t(
"myrepresentations:consultation-from-an-interested-person-heading"
)}
</h2>
<label
className="govuk-label govuk-body-m"
htmlFor="representationType"
>
{t(
kindLabelText={t(
"myrepresentations:kind-of-consultation-label"
)}
</label>
<Field
name="representationType"
component={RenderPickList}
datafieldname="representationType"
isTypeSelected={isTypeConfirmed}
selectedTypeDisplay={selectedRepresentationTypeDisplay}
representationType={selectedRepresentationType}
setTypeSelectionConfirmed={setTypeSelectionConfirmed}
onRepresentationTypeChange={onRepresentationTypeChange}
onTypeSelectionConfirmed={onTypeSelectionConfirmed}
optionsArr={interestedPersonRepresentationArr}
validate={[required]}
optionsArr={
interestedPersonRepresentationArr
}
errorMsg={t(
"myrepresentations:select-an-option-label"
)}
errorMsg={t("myrepresentations:select-an-option-label")}
/>
</>
) : (
<label className="govuk-label govuk-body-m">
{t("myrepresentations:representation-type")}:{" "}
{router.locale == "cy"
? formObj.representationForm.values
.representationType == "Questionnaire"
? "Holiadur"
: formObj.representationForm.values
.representationType == "Statement"
? "Datganiad"
: formObj.representationForm.values
.representationType ==
"Final comments"
? "Sylwadau terfynol"
: formObj.representationForm.values
.representationType
: formObj.representationForm.values
.representationType}
</label>
)}
</div>
</div>
{(typeof props.representationType != "undefined" ||
typeof formObj.representationForm.values
.representationType != "undefined") && (
{isTypeConfirmed && (
<>
<div className="govuk-grid-row">
<div className="govuk-form-group">
@@ -134,7 +127,7 @@ const ConsultationInterestedPartyPerson = (props) => {
t(
"myrepresentations:questionnaire-yes"
),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="representationOnBehalfOf"
className="govuk-radios__input"
@@ -154,8 +147,7 @@ const ConsultationInterestedPartyPerson = (props) => {
<div className="govuk-grid-row">
<div className="govuk-form-group">
<p className="govuk-body">
{formObj.representationForm.values
.representationType ==
{selectedRepresentationType ==
"Consultation Response"
? t(
"myrepresentations:enter-comment-label"
@@ -169,8 +161,7 @@ const ConsultationInterestedPartyPerson = (props) => {
aria-describedby="commentBox-hint"
>
{" "}
{formObj.representationForm.values
.representationType ==
{selectedRepresentationType ==
"Consultation Response" && (
<div className="govuk-form-group">
<Field
@@ -274,8 +265,7 @@ const ConsultationInterestedPartyPerson = (props) => {
</div>
</>
)}
{typeof formObj.representationForm.values.representationType !=
"undefined" && (
{isTypeConfirmed && (
<div className="govuk-grid-row">
<div className="govuk-button-group">
<button
@@ -284,7 +274,7 @@ const ConsultationInterestedPartyPerson = (props) => {
data-module="govuk-button"
disabled={shouldDisableButton({
props,
formObj,
formObj
})}
>
{t("common:continue-button")}
@@ -363,7 +353,7 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(change(form, field, newValue)),
setFilesForRepresentations: (fileList) => {
dispatch(setFilesForRepresentations(fileList));
},
}
});
export default connect(
@@ -373,6 +363,6 @@ export default connect(
reduxForm({
form: "representationForm",
enableReinitialize: true,
destroyOnUnmount: false,
destroyOnUnmount: false
})(ConsultationInterestedPartyPerson)
);
@@ -0,0 +1,42 @@
const RepresentationActionButtons = ({
t,
locale,
isSaving,
isContinueDisabled,
onSaveExit
}) => {
return (
<div className="govuk-grid-row">
<div className="govuk-button-group">
<button
type="submit"
className="govuk-button"
data-module="govuk-button"
disabled={isContinueDisabled}
>
{t("common:continue-button")}
</button>
{isSaving ? (
<span className=" progress-save-active">
{locale == "cy" ? "Nôl data" : "Saving data"}
<img
className="data-loading-icon"
src="/assets/images/loading.gif"
alt={locale == "cy" ? "Nôl data" : "Saving data"}
/>
</span>
) : (
<button
className="govuk-button progress-save "
onClick={onSaveExit}
>
{t("common:save-exit-button")}
</button>
)}
</div>
</div>
);
};
export default RepresentationActionButtons;
@@ -0,0 +1,205 @@
import { Field } from "redux-form";
import router from "next/router";
import Link from "next/link";
const getRepresentationTypeHint = (option, t) => {
const map = {
"Statement": t("myrepresentations:rep-type-statement-hint"),
"Final comments": t("myrepresentations:rep-type-final-comments-hint"),
"Questionnaire": t("myrepresentations:rep-type-questionnaire-hint"),
"Consultation Response": t(
"myrepresentations:rep-type-consultation-hint"
),
"Local Impact Report": t("myrepresentations:rep-type-lir-hint"),
"Marine Impact Report": t("myrepresentations:rep-type-mir-hint")
};
return map[option] || "";
};
const getRepresentationTypeLabel = (option) => {
if (router.locale !== "cy") {
return option;
}
if (option === "Questionnaire") {
return "Holiadur";
}
if (option === "Statement") {
return "Datganiad";
}
if (option === "Final comments") {
return "Sylwadau terfynol";
}
return option;
};
const RenderRepresentationTypeRadioList = ({
name,
optionsArr,
errorMsg,
onRepresentationTypeChange,
input,
meta: { touched, error }
}) => {
return (
<div
className={
touched && error
? "govuk-form-group govuk-form-group--error"
: "govuk-form-group"
}
>
{touched && error && (
<span id={`${name}-error`} className="govuk-error-message">
<span className="govuk-visually-hidden">Error:</span>{" "}
{errorMsg}
</span>
)}
<div className="govuk-radios govuk-radios--small">
{optionsArr.map((option, index) => {
const optionId = `${name}_${index}`;
const hintId = `${optionId}-hint`;
return (
<div
key={option.value || optionId}
className="govuk-radios__item representation-type-option"
>
<input
className="govuk-radios__input"
id={optionId}
name={input.name}
type="radio"
value={option.value}
checked={input.value === option.value}
onChange={() => input.onChange(option.value)}
aria-describedby={
option.hint ? hintId : undefined
}
/>
<label
className="govuk-label govuk-radios__label govuk-!-font-weight-bold "
htmlFor={optionId}
>
{option.label}
{option.hint && (
<div
id={hintId}
className="govuk-hint govuk-!-margin-top-2"
>
{option.hint}
</div>
)}
</label>
</div>
);
})}
</div>
</div>
);
};
const RepresentationTypeSelectorBlock = ({
t,
headingText,
kindLabelText,
isTypeSelected,
selectedTypeDisplay,
representationType,
setTypeSelectionConfirmed,
onRepresentationTypeChange,
onTypeSelectionConfirmed,
optionsArr,
validate,
errorMsg
}) => {
const enhancedOptions = (optionsArr || [])
.filter((option) => option && option !== "Select...")
.map((option) => ({
value: option,
label: getRepresentationTypeLabel(option),
hint: getRepresentationTypeHint(option, t)
}));
const hasSelectableOptions = enhancedOptions.length > 0;
return (
<div className="govuk-grid-row">
<div className="govuk-form-group">
{!isTypeSelected ? (
<>
<h2 className="govuk-heading-m ">{headingText}</h2>
<label
className="govuk-label govuk-body-m govuk-!-margin-bottom-2"
htmlFor="representationType"
>
{kindLabelText}
</label>
{hasSelectableOptions ? (
<>
<Field
name="representationType"
component={
RenderRepresentationTypeRadioList
}
onRepresentationTypeChange={
onRepresentationTypeChange
}
validate={validate}
optionsArr={enhancedOptions}
errorMsg={errorMsg}
/>
<button
type="button"
className="govuk-button"
data-module="govuk-button"
disabled={!representationType}
onClick={() => {
if (representationType) {
setTypeSelectionConfirmed(true);
onTypeSelectionConfirmed &&
onTypeSelectionConfirmed(
representationType
);
}
}}
>
{t("common:continue-button")}
</button>
</>
) : (
<>
<p className="govuk-body">
{t(
"myrepresentations:representation-type-unavailable-label"
)}
</p>
<Link className="govuk-link" href="/">
{t(
"myrepresentations:return-home-link-label"
)}
</Link>
</>
)}
</>
) : (
<label className="govuk-label govuk-body-m">
{t("myrepresentations:representation-type")}:{" "}
{selectedTypeDisplay}
</label>
)}
</div>
</div>
);
};
export default RepresentationTypeSelectorBlock;
File diff suppressed because it is too large Load Diff
@@ -2,22 +2,23 @@ import useTranslation from "next-translate/useTranslation";
import { useRouter } from "next/router";
import { connect } from "react-redux";
import { Field, change, reduxForm } from "redux-form";
import { useState } from "react";
import { useEffect, useState } from "react";
import { bytesToSize, getThumbnailIconByExtension } from "../../utils";
import {
FileUploadField,
RenderCondtionalRadioList,
RenderPickList,
RenderRichMultiline,
shouldDisableButton,
shouldDisableButton
} from "./representationElements";
import RepresentationActionButtons from "./elements/RepresentationActionButtons";
import RepresentationTypeSelectorBlock from "./elements/RepresentationTypeSelectorBlock";
import { useDropzone } from "react-dropzone";
import {
setRepresentationCapacity,
setRepresentationSubmit,
setFilesForRepresentations,
setFilesForRepresentations
} from "../../../store/currentView/action";
const RepAgent = (props) => {
@@ -36,9 +37,23 @@ const RepAgent = (props) => {
setSavingStatus,
savingStatus,
setFilesForRepresentations,
onRepresentationTypeChange,
onTypeSelectionConfirmed
} = props;
const { acceptedFiles, getRootProps, getInputProps } = useDropzone();
const persistedRepresentationType =
currentView?.caseReference?.repDetails?.representationType;
const [typeSelectionConfirmed, setTypeSelectionConfirmed] = useState(
!!persistedRepresentationType
);
useEffect(() => {
if (persistedRepresentationType) {
setTypeSelectionConfirmed(true);
}
}, [persistedRepresentationType]);
if (JSON.stringify(formObj) != "{}") {
setRepFileName(formObj.representationForm.values);
@@ -63,61 +78,49 @@ const RepAgent = (props) => {
));
const required = (value) => (value ? undefined : "Required");
const selectedRepresentationType =
formObj?.representationForm?.values?.representationType ||
props.representationType;
const isTypeConfirmed =
!!selectedRepresentationType && typeSelectionConfirmed;
const selectedRepresentationTypeDisplay =
router.locale == "cy"
? selectedRepresentationType == "Questionnaire"
? "Holiadur"
: selectedRepresentationType == "Statement"
? "Datganiad"
: selectedRepresentationType == "Final comments"
? "Sylwadau terfynol"
: selectedRepresentationType
: selectedRepresentationType;
const handleSaveExit = () => {
let valuesObj = props.props.props.form[props.form].values || {};
delete valuesObj["_pinswg_appellant_value"];
updateRepresentation(valuesObj, false, true);
};
return (
<>
<div className="govuk-grid-row">
<div className="govuk-form-group">
{typeof formObj.representationForm.values
.representationType == "undefined" &&
props.representationType != "Select..." ? (
<>
<h2 className="govuk-heading-m ">
{t(
<RepresentationTypeSelectorBlock
t={t}
headingText={t(
"myrepresentations:representation-from-an-agent-heading"
)}
</h2>
<label
className="govuk-label govuk-body-m"
htmlFor="representationType"
>
{t("myrepresentations:kind-of-rep-label")}
</label>
<Field
name="representationType"
component={RenderPickList}
datafieldname="representationType"
optionsArr={
appellantApplicantRepresentationArr
}
kindLabelText={t("myrepresentations:kind-of-rep-label")}
isTypeSelected={isTypeConfirmed}
selectedTypeDisplay={selectedRepresentationTypeDisplay}
representationType={selectedRepresentationType}
setTypeSelectionConfirmed={setTypeSelectionConfirmed}
onRepresentationTypeChange={onRepresentationTypeChange}
onTypeSelectionConfirmed={onTypeSelectionConfirmed}
optionsArr={appellantApplicantRepresentationArr}
/>
</>
) : (
<label className="govuk-label govuk-body-m">
{t("myrepresentations:representation-type")}:{" "}
{router.locale == "cy"
? formObj.representationForm.values
.representationType == "Questionnaire"
? "Holiadur"
: formObj.representationForm.values
.representationType == "Statement"
? "Datganiad"
: formObj.representationForm.values
.representationType ==
"Final comments"
? "Sylwadau terfynol"
: formObj.representationForm.values
.representationType
: formObj.representationForm.values
.representationType}
</label>
)}
</div>
</div>
{(typeof props.representationType != "undefined" ||
typeof formObj.representationForm.values
.representationType != "undefined") && (
{isTypeConfirmed && (
<>
<div className="govuk-grid-row">
<div className="govuk-form-group">
@@ -197,7 +200,8 @@ const RepAgent = (props) => {
"myrepresentations:add-files-label"
)}
ticketnumber={
props.props.caseReference
props.currentView.caseReference
.ticketnumber
}
hint={"sdsd"}
fileList={
@@ -239,64 +243,17 @@ const RepAgent = (props) => {
</div>
</>
)}
{typeof formObj.representationForm.values.representationType !=
"undefined" && (
<div className="govuk-grid-row">
<div className="govuk-button-group">
<button
type="submit"
className="govuk-button"
data-module="govuk-button"
disabled={shouldDisableButton({
{isTypeConfirmed && (
<RepresentationActionButtons
t={t}
locale={router.locale}
isSaving={savingStatus}
isContinueDisabled={shouldDisableButton({
props,
formObj,
formObj
})}
>
{t("common:continue-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"
}
onSaveExit={handleSaveExit}
/>
</span>
) : (
<button
className="govuk-button progress-save "
onClick={() => {
let valuesObj =
props.props.props.form[props.form]
.values || {};
//console.log("valuesobj:", valuesObj);
delete valuesObj[
"_pinswg_appellant_value"
];
//console.log("on save:", valuesObj);
updateRepresentation(
valuesObj,
false,
true
);
}}
>
{t("common:save-exit-button")}
</button>
)}
</div>
</div>
)}
</>
);
@@ -309,7 +266,7 @@ const mapDispatchToProps = (dispatch) => ({
setFilesForRepresentations: (fileList) => {
dispatch(setFilesForRepresentations(fileList));
},
}
});
export default connect(
@@ -319,6 +276,6 @@ export default connect(
reduxForm({
form: "representationForm",
enableReinitialize: true,
destroyOnUnmount: false,
destroyOnUnmount: false
})(RepAgent)
);
@@ -2,21 +2,22 @@ import useTranslation from "next-translate/useTranslation";
import { useRouter } from "next/router";
import { connect } from "react-redux";
import { Field, change, reduxForm } from "redux-form";
import { useState } from "react";
import { useEffect, useState } from "react";
import { bytesToSize, getThumbnailIconByExtension } from "../../utils";
import {
FileUploadField,
RenderPickList,
RenderRichMultiline,
shouldDisableButton,
shouldDisableButton
} from "./representationElements";
import RepresentationActionButtons from "./elements/RepresentationActionButtons";
import RepresentationTypeSelectorBlock from "./elements/RepresentationTypeSelectorBlock";
import { useDropzone } from "react-dropzone";
import {
setRepresentationCapacity,
setRepresentationSubmit,
setFilesForRepresentations,
setFilesForRepresentations
} from "../../../store/currentView/action";
const RepAppellant = (props) => {
@@ -35,9 +36,23 @@ const RepAppellant = (props) => {
setSavingStatus,
savingStatus,
setFilesForRepresentations,
onRepresentationTypeChange,
onTypeSelectionConfirmed
} = props;
const { acceptedFiles, getRootProps, getInputProps } = useDropzone();
const persistedRepresentationType =
currentView?.caseReference?.repDetails?.representationType;
const [typeSelectionConfirmed, setTypeSelectionConfirmed] = useState(
!!persistedRepresentationType
);
useEffect(() => {
if (persistedRepresentationType) {
setTypeSelectionConfirmed(true);
}
}, [persistedRepresentationType]);
console.log(JSON.stringify(formObj) == "{}" ? "empty" : "value");
if (JSON.stringify(formObj) != "{}") {
@@ -62,59 +77,49 @@ const RepAppellant = (props) => {
index === self.findIndex((t) => t.name === value.name)
);
const selectedRepresentationType =
formObj?.representationForm?.values?.representationType ||
props.representationType;
const isTypeConfirmed =
!!selectedRepresentationType && typeSelectionConfirmed;
const selectedRepresentationTypeDisplay =
router.locale == "cy"
? selectedRepresentationType == "Questionnaire"
? "Holiadur"
: selectedRepresentationType == "Statement"
? "Datganiad"
: selectedRepresentationType == "Final comments"
? "Sylwadau terfynol"
: selectedRepresentationType
: selectedRepresentationType;
const handleSaveExit = () => {
let valuesObj = props.props.props.form[props.form].values || {};
delete valuesObj["_pinswg_appellant_value"];
updateRepresentation(valuesObj, false, true);
};
return (
<>
<div className="govuk-grid-row">
<div className="govuk-form-group">
{typeof formObj.representationForm.values
.representationType == "undefined" &&
props.representationType != "Select..." ? (
<>
<h2 className="govuk-heading-m ">
{t(
<RepresentationTypeSelectorBlock
t={t}
headingText={t(
"myrepresentations:representation-from-an-appellant-heading"
)}
</h2>
<label
className="govuk-label govuk-body-m"
htmlFor="representationType"
>
{t("myrepresentations:kind-of-rep-label")}
</label>
<Field
name="representationType"
component={RenderPickList}
datafieldname="representationType"
optionsArr={
appellantApplicantRepresentationArr
}
kindLabelText={t("myrepresentations:kind-of-rep-label")}
isTypeSelected={isTypeConfirmed}
selectedTypeDisplay={selectedRepresentationTypeDisplay}
representationType={selectedRepresentationType}
setTypeSelectionConfirmed={setTypeSelectionConfirmed}
onRepresentationTypeChange={onRepresentationTypeChange}
onTypeSelectionConfirmed={onTypeSelectionConfirmed}
optionsArr={appellantApplicantRepresentationArr}
/>
</>
) : (
<label className="govuk-label govuk-body-m">
{t("myrepresentations:representation-type")}:{" "}
{router.locale == "cy"
? formObj.representationForm.values
.representationType == "Questionnaire"
? "Holiadur"
: formObj.representationForm.values
.representationType == "Statement"
? "Datganiad"
: formObj.representationForm.values
.representationType ==
"Final comments"
? "Sylwadau terfynol"
: formObj.representationForm.values
.representationType
: formObj.representationForm.values
.representationType}
</label>
)}
</div>
</div>
{(typeof props.representationType != "undefined" ||
typeof formObj.representationForm.values
.representationType != "undefined") && (
{isTypeConfirmed && (
<div className="govuk-grid-row">
<div className="govuk-form-group">
<p className="govuk-body">
@@ -189,7 +194,10 @@ const RepAppellant = (props) => {
label={t(
"myrepresentations:add-files-label"
)}
ticketnumber={props.props.caseReference}
ticketnumber={
props.currentView.caseReference
.ticketnumber
}
hint={"sdsd"}
fileList={
props.currentView.fileList
@@ -227,64 +235,17 @@ const RepAppellant = (props) => {
</div>
)}
{typeof formObj.representationForm.values.representationType !=
"undefined" && (
<div className="govuk-grid-row">
<div className="govuk-button-group">
<button
type="submit"
className="govuk-button"
data-module="govuk-button"
disabled={shouldDisableButton({
{isTypeConfirmed && (
<RepresentationActionButtons
t={t}
locale={router.locale}
isSaving={savingStatus}
isContinueDisabled={shouldDisableButton({
props,
formObj,
formObj
})}
>
{t("common:continue-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"
}
onSaveExit={handleSaveExit}
/>
</span>
) : (
<button
className="govuk-button progress-save "
onClick={() => {
let valuesObj =
props.props.props.form[props.form]
.values || {};
//console.log("valuesobj:", valuesObj);
delete valuesObj[
"_pinswg_appellant_value"
];
//console.log("on save:", valuesObj);
updateRepresentation(
valuesObj,
false,
true
);
}}
>
{t("common:save-exit-button")}
</button>
)}
</div>
</div>
)}
</>
);
@@ -300,7 +261,7 @@ const mapDispatchToProps = (dispatch) => {
setFilesForRepresentations: (fileList) => {
dispatch(setFilesForRepresentations(fileList));
},
}
};
};
@@ -311,6 +272,6 @@ export default connect(
reduxForm({
form: "representationForm",
enableReinitialize: true,
destroyOnUnmount: false,
destroyOnUnmount: false
})(RepAppellant)
);
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import { useDropzone } from "react-dropzone";
import { Field } from "redux-form";
import RepDetails from "./representationDetails";
import { RenderRadioListWithText } from "./representationElements";
import { RenderRadioList } from "./representationElements";
let RepCapacitySelection = (props) => {
let { t } = useTranslation();
@@ -17,7 +17,7 @@ let RepCapacitySelection = (props) => {
casesObj,
validate,
handleSubmit,
setCurrentReference,
setCurrentReference
} = props;
const { acceptedFiles, getRootProps, getInputProps } = useDropzone();
const files = acceptedFiles.map((file) => (
@@ -36,6 +36,9 @@ let RepCapacitySelection = (props) => {
currentType={currentType}
casesObj={casesObj}
caseReference={props.props.caseReference}
ticketnumber={
props.props.currentView.caseReference.ticketnumber
}
searchResultsObj={props.props.searchResultsObj}
myRepresentations={props.props.myRepresentations}
props={props}
@@ -49,7 +52,7 @@ let RepCapacitySelection = (props) => {
{t(
"myrepresentations:capacity-select-your-details"
)}{" "}
- {props.firstValue}
- {props.firstValue}{" "}
</h2>
<Field
name="representationCapacity"
@@ -69,7 +72,7 @@ let RepCapacitySelection = (props) => {
errorMsg={t(
"myrepresentations:select-an-option-label"
)}
component={RenderRadioListWithText}
component={RenderRadioList}
validate={[required]}
/>
</div>
@@ -7,10 +7,10 @@ import { connect } from "react-redux";
import { useDropzone } from "react-dropzone";
import {
createWatchedCases,
sendEmail,
sendRepCompleteMessage,
setRepInvolvment,
} from "../../../actions";
setRepInvolvment
} from "../../../actions/services/portalService";
import { sendEmail } from "../../../actions/services/notifyService";
import { setRepresentationReset } from "../../../store/currentView/action";
const RepComplete = (props) => {
@@ -27,7 +27,7 @@ const RepComplete = (props) => {
setSubmitBack,
setRepresentationSubmitConfirmation,
setRepresentationMessageSent,
repFormData,
repFormData
} = props;
const { acceptedFiles, getRootProps, getInputProps } = useDropzone();
const files = acceptedFiles.map((file) => (
@@ -36,9 +36,22 @@ const RepComplete = (props) => {
</li>
));
const isSIPS =
props.props.currentView.caseReference.appealType === 846040002;
useEffect(() => {
const reference = "PEDW-REPRESENTATION-COMPLETE";
const templateId = "c49785e7-3217-40b6-b33f-dffc4e58d5c1";
const isWelsh = router.locale === "cy";
const reference = isSIPS
? "PEDW-CONSULTATION-COMPLETE" + isWelsh && "-CY"
: "PEDW-REPRESENTATION-COMPLETE" + isWelsh && "-CY";
const templateId = isSIPS
? isWelsh
? "7d27b968-6f63-4334-b1c4-2e4a69554212"
: "c3f373ca-df9d-4967-bea0-3ad3a57f8dcc"
: isWelsh
? "6e2f0111-b352-4074-91a2-760403cd185b"
: "c49785e7-3217-40b6-b33f-dffc4e58d5c1";
const emailAddress =
props.props.props.props.accountDetails.accountDetails.emailaddress1;
const personalisation = {
@@ -47,17 +60,18 @@ const RepComplete = (props) => {
"emailAddress":
props.props.props.props.accountDetails.accountDetails
.emailaddress1,
"linkExpiry": 10 * 60,
"linkExpiry": 10 * 60
};
props.props.currentView.representationMessageSent != true &&
(props.props.currentView.representationMessageSent != true &&
(setRepInvolvment(
props.props.currentView.caseReference.incidentid,
props.props.props.props.accountDetails.accountDetails.contactid
props.props.props.props.accountDetails.accountDetails.contactid,
props.props.currentView.representationCapacity
),
sendRepCompleteMessage(
props.props.props.props.accountDetails.containerID,
props.props.currentView.caseReference.currentReference,
props.props.currentView.caseReference.ticketnumber,
props.repFormData.repfile_name
),
sendEmail(templateId, emailAddress, personalisation, reference),
@@ -81,9 +95,10 @@ const RepComplete = (props) => {
? props.props.currentView.caseReference.repDetails
.representationType
: props.props.currentView.caseReference
.representationType,
.representationType
}),
setRepresentationMessageSent(true));
setRepresentationMessageSent(true)),
window.scrollTo({ top: 0, behavior: "smooth" }));
});
return (
<div id="rep-appellant">
@@ -95,6 +110,7 @@ const RepComplete = (props) => {
<p>{t("myrepresentations:rep-complete-para-one")}</p>
<p>{t("myrepresentations:rep-complete-para-two")}</p>
<p> {t("myrepresentations:rep-complete-para-three")}</p>
<p> {t("myrepresentations:rep-complete-para-four")}</p>
</div>
<div className="govuk-grid-row">
<div className="govuk-button-group">
@@ -121,7 +137,7 @@ const mapStateToProps = (state) => {
currentView: state.currentView,
myRepresentations: state.myRepresentations,
initialValues: state.currentView.caseReference.repDetails,
accountDetails: state.accountDetails,
accountDetails: state.accountDetails
//form: state.form,
};
};
@@ -4,6 +4,7 @@ import { useRouter } from "next/router";
import { useState } from "react";
import { useDropzone } from "react-dropzone";
import { connect } from "react-redux";
import { generateRepPDF } from "../../../actions/services/documentService";
import RepComplete from "./representationComplete";
import RepLPAQuestionnaireCheck_enforcement from "./representationQuestionnairesCheck/representationLPAQuestionnaireCheck_enforcement";
import RepLPAQuestionnaireCheck_s78 from "./representationQuestionnairesCheck/representationLPAQuestionnaireCheck_s78";
@@ -13,16 +14,19 @@ import {
getThumbnailIconByExtension,
formatDates,
updateLinks,
getDocLink
} from "../../utils";
import {
setCurrentReference,
setCurrentView,
setCurrentView
} from "../../../store/currentView/action";
const RepCompleteSubmit = (props) => {
let { t } = useTranslation();
const [confirmSections, setConfirmSections] = useState(false);
const [downloadQuestionnaire, setDownloadQuestionnaire] = useState(false);
const [downloadInProgress, setDownloadInProgress] = useState(false);
const router = useRouter();
const { locale } = router;
@@ -41,7 +45,11 @@ const RepCompleteSubmit = (props) => {
questionnaireCount,
finaliseAppealProcess,
setCurrentReference,
docsOffline
} = props;
let ShowDocLinks = getDocLink(docsOffline);
const { acceptedFiles, getRootProps, getInputProps } = useDropzone();
const files = acceptedFiles.map((file) => (
<li key={file.path}>
@@ -53,6 +61,13 @@ const RepCompleteSubmit = (props) => {
repFormData = updateLinks(repFormData);
const representationOnBehalfOfDisplay =
repFormData.representationOnBehalfOf === "Yes"
? t("myrepresentations:questionnaire-yes")
: repFormData.representationOnBehalfOf === "No"
? t("myrepresentations:questionnaire-no")
: repFormData.representationOnBehalfOf;
//const repDate = new Date();
// let day = repDate.getDate();
@@ -164,7 +179,7 @@ const RepCompleteSubmit = (props) => {
typeof filesUploadObj[key].name != "undefined" &&
buildAppealFilesArray.push({
"name": filesUploadObj[key].name,
"size": filesUploadObj[key].contentLength,
"size": filesUploadObj[key].contentLength
});
}
@@ -180,7 +195,7 @@ const RepCompleteSubmit = (props) => {
// : [];
let arrUniq = [
...new Map(buildAppealFilesArray.map((v) => [v.name, v])).values(),
...new Map(buildAppealFilesArray.map((v) => [v.name, v])).values()
];
buildAppealFilesArray = buildAppealFilesArray.filter(function (el) {
@@ -212,7 +227,7 @@ const RepCompleteSubmit = (props) => {
arrUniq = removeEmptyObjects(arrUniq);
Object.assign(formObj.representationForm.values, {
"filesList": arrUniq,
"filesList": arrUniq
});
let filterFiles = [];
@@ -249,6 +264,64 @@ const RepCompleteSubmit = (props) => {
}
};
const triggerQuestionnaireDownload = async () => {
if (
repFormData.representationType !== "Questionnaire" ||
!downloadQuestionnaire
) {
return;
}
if (downloadInProgress) {
return;
}
try {
setDownloadInProgress(true);
const containerID =
props.accountDetails?.containerID ||
props.props?.accountDetails?.containerID ||
props.props?.props?.accountDetails?.containerID;
const caseRef =
currentView?.caseReference?.ticketnumber ||
repFormData?.caseRef ||
repFormData?.ticketnumber;
if (!containerID || !caseRef) {
console.error(
"Questionnaire download skipped: missing containerID or case reference",
{ containerID, caseRef }
);
return;
}
repFormData.locale = router.locale == "cy" ? "cy" : "en";
const pdfBlob = await generateRepPDF(
repFormData,
containerID,
caseRef,
{ download: true }
);
if (pdfBlob) {
const blobUrl = window.URL.createObjectURL(pdfBlob);
const anchor = document.createElement("a");
anchor.href = blobUrl;
anchor.download = `${repFormData.repfile_name || "questionnaire"}.pdf`;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
window.URL.revokeObjectURL(blobUrl);
}
} catch (error) {
console.error("Questionnaire download failed", error);
} finally {
setDownloadInProgress(false);
}
};
return (
<>
{currentView.representationSubmitConfirmation == true ? (
@@ -340,7 +413,7 @@ const RepCompleteSubmit = (props) => {
<dd className="govuk-summary-list__value">
{
repFormData.representationOnBehalfOf
representationOnBehalfOfDisplay
}
</dd>
@@ -362,7 +435,7 @@ const RepCompleteSubmit = (props) => {
"appealType":
repFormData.appealType,
"repDetails":
repFormData,
repFormData
});
setRepresentationSubmit(
false
@@ -415,7 +488,7 @@ const RepCompleteSubmit = (props) => {
"appealType":
repFormData.appealType,
"repDetails":
repFormData,
repFormData
}
);
setRepresentationSubmit(
@@ -452,7 +525,7 @@ const RepCompleteSubmit = (props) => {
<span
className="multiline-elipsis"
dangerouslySetInnerHTML={{
__html: repFormData.representationComments,
__html: repFormData.representationComments
}}
/>
</dd>
@@ -476,7 +549,7 @@ const RepCompleteSubmit = (props) => {
"appealType":
repFormData.appealType,
"repDetails":
repFormData,
repFormData
});
setRepresentationSubmit(
false
@@ -556,7 +629,7 @@ const RepCompleteSubmit = (props) => {
"appealType":
repFormData.appealType,
"repDetails":
repFormData,
repFormData
}),
setRepresentationSubmit(
false
@@ -603,6 +676,8 @@ const RepCompleteSubmit = (props) => {
"myrepresentations:rep-check-tandc-para-three"
)}
</p>
<p></p>
{checkRepDatePassed() ? (
<div className="govuk-warning-text noticebanner">
<strong className="govuk-warning-text__text">
@@ -641,8 +716,63 @@ const RepCompleteSubmit = (props) => {
{t("myrepresentations:rep-wait-label")}
</p>
</div>
) : 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>
{t(
"myrepresentations:docsoffline-reps-label"
)}
{" "}
</strong>
</div>{" "}
<Link className="govuk-link" href="/">
{t(
"myrepresentations:return-to-myportal-link"
)}
</Link>
</>
) : (
<>
{repFormData.representationType ==
"Questionnaire" && (
<div className="govuk-checkboxes govuk-checkboxes--small">
<div className="govuk-checkboxes__item">
<input
name="pinswg_downloadQuestionnaire"
id="pinswg_downloadQuestionnaire"
type="checkbox"
className="govuk-checkboxes__input"
checked={
downloadQuestionnaire
}
onChange={(e) => {
setDownloadQuestionnaire(
e.target.checked
);
}}
/>
<label
className="govuk-label govuk-checkboxes__label"
htmlFor="pinswg_downloadQuestionnaire"
>
{t(
"myrepresentations:rep-check-tandc-para-five"
)}
</label>
</div>
</div>
)}
<div className="govuk-form-group">
<div className="govuk-checkboxes govuk-checkboxes--small">
<div className="govuk-checkboxes__item">
@@ -681,10 +811,11 @@ const RepCompleteSubmit = (props) => {
type="submit"
className="govuk-button"
data-module="govuk-button"
onClick={() => {
onClick={async () => {
// setRepresentationSubmitConfirmation(
// true
// );
await triggerQuestionnaireDownload();
setRepresentationSubmit(
true
);
@@ -721,7 +852,7 @@ const mapStateToProps = (state) => {
currentView: state.currentView,
myRepresentations: state.myRepresentations,
initialValues: state.currentView.caseReference.repDetails,
accountDetails: state.accountDetails,
accountDetails: state.accountDetails
//form: state.form,
};
};
@@ -730,7 +861,7 @@ const mapDispatchToProps = (dispatch) => {
return {
setCurrentReference: (currentReference) => {
dispatch(setCurrentReference(currentReference));
},
}
};
};
@@ -10,7 +10,7 @@ const RepDetails = (props) => {
const { currentType, casesObj, appealType } = props;
let { caseReference } = props;
let { caseReference, ticketnumber } = props;
let setCaseDetailsObj = router.query.hasOwnProperty("state")
? router.query.state == "edit"
@@ -29,7 +29,7 @@ const RepDetails = (props) => {
: caseReference;
detailsObj = jsonpath({
path: '$..[?(@ && @.pinswg_name=="' + caseReference + '")]',
path: '$..[?(@ && @.ticketnumber=="' + ticketnumber + '")]',
json: setCaseDetailsObj,
eval: true,
});
@@ -13,8 +13,8 @@ import {
deleteBlob,
deleteRepBlob,
getFilesFromBlobHashed,
getFilesFromBlobproxy,
} from "../../../actions";
getFilesFromBlobproxy
} from "../../../actions/services/documentService";
const LoadingMessage = () => <div>Loading the editor...</div>;
@@ -23,7 +23,7 @@ const ReactQuill = dynamic(
() => import("react-quill-new").then((mod) => mod.default),
{
ssr: false,
loading: () => <LoadingMessage />, // This is valid as long as it's a valid React component
loading: () => <LoadingMessage /> // This is valid as long as it's a valid React component
}
);
import { useDispatch } from "react-redux"; // Import the useDispatch hook
@@ -35,13 +35,13 @@ import {
parseISO,
subDays,
subMonths,
subYears,
subYears
} from "date-fns";
import {
bytesToSize,
getThumbnailIconByExtension,
updateLinks,
updateLinks
} from "../../utils";
import { useRouter } from "next/router";
@@ -258,6 +258,26 @@ export const RenderCondtionalRadioList = ({
}) => {
const required = (value) => (value ? undefined : "Required");
let { t } = useTranslation();
const yesValue = t("myrepresentations:questionnaire-yes");
const noValue = t("myrepresentations:questionnaire-no");
const normalizeYesNo = (rawValue) => {
if (rawValue === true) return "Yes";
if (rawValue === false) return "No";
if (rawValue === "Yes" || rawValue === yesValue) return "Yes";
if (rawValue === "No" || rawValue === noValue) return "No";
return rawValue;
};
const isYesSelected = normalizeYesNo(value) === "Yes";
const requiredIfYes = (fieldValue, allValues) => {
const selectedValue = normalizeYesNo(allValues?.[id]);
return selectedValue === "Yes" && !String(fieldValue || "").trim()
? "Required"
: undefined;
};
return (
<>
<div
@@ -304,8 +324,16 @@ export const RenderCondtionalRadioList = ({
{custom.hint}
</div>
<div className="govuk-radios">
{Object.keys(options).map((key, index) => (
<div key={key} className="govuk-!-margin-bottom-3">
{Object.keys(options).map((key, index) => {
const normalizedOptionValue = normalizeYesNo(
options[key]
);
return (
<div
key={key}
className="govuk-!-margin-bottom-3"
>
<div
className="govuk-radios__item"
data-children-count={key}
@@ -317,7 +345,7 @@ export const RenderCondtionalRadioList = ({
component="input"
type="radio"
className="govuk-radios__input"
value={options[key]}
value={normalizedOptionValue}
validate={[required]}
/>
<label
@@ -333,7 +361,8 @@ export const RenderCondtionalRadioList = ({
)}
</label>
</div>
{value == "Yes" && options[key] == "Yes" && (
{isYesSelected &&
normalizedOptionValue === "Yes" && (
<div
className="govuk-radios__conditional "
id={"conditional-" + id}
@@ -344,7 +373,9 @@ export const RenderCondtionalRadioList = ({
>
<label
className="govuk-label"
htmlFor={id + "_details"}
htmlFor={
id + "_details"
}
>
{t(
"myrepresentations:representation-onbehalfof-label"
@@ -354,9 +385,13 @@ export const RenderCondtionalRadioList = ({
className="govuk-input govuk-!-width-one-half"
id={id + "_details"}
name={id + "_details"}
component={RenderTextfield}
component={
RenderTextfield
}
type="text"
validate={[required]}
validate={[
requiredIfYes
]}
errorMsg={t(
"myrepresentations:is-required-label"
)}
@@ -365,7 +400,8 @@ export const RenderCondtionalRadioList = ({
</div>
)}
</div>
))}
);
})}
</div>
</fieldset>
</div>
@@ -546,7 +582,8 @@ export const RenderPickList = ({
? "Sylwadau ysgrifenedig"
: optionsArr[key] == "Inquiry"
? "Ymchwiliad"
: optionsArr[key] == "Hearing"
: optionsArr[key] ==
"Hearing"
? "Gwrandawiad"
: optionsArr[key]
: optionsArr[key]}
@@ -734,13 +771,19 @@ export const RenderMultiline = ({
input.onChange(valStr);
};
const handleBlur = () => {
const updated = updateLinks(editValue);
setEditValue(updated);
input.onChange(updated);
};
var modules = {
toolbar: [
[{ "header": [1, 2, false] }],
["bold", "italic", "underline", "blockquote"],
[{ "list": "ordered" }, { "list": "bullet" }],
["clean"],
],
["clean"]
]
};
return (
@@ -764,6 +807,7 @@ export const RenderMultiline = ({
theme="snow"
value={editValue}
onChange={changeEdit}
onBlur={handleBlur}
/>
<textarea
{...input}
@@ -795,18 +839,23 @@ export const RenderRichMultiline = ({
);
const changeEdit = (valStr) => {
valStr = updateLinks(valStr);
setEditValue(valStr);
input.onChange(valStr);
};
const handleBlur = () => {
const updated = updateLinks(editValue);
setEditValue(updated);
input.onChange(updated);
};
var modules = {
toolbar: [
[{ "header": [1, 2, false] }],
["bold", "italic", "underline", "blockquote"],
[{ "list": "ordered" }, { "list": "bullet" }],
["clean"],
],
["clean"]
]
};
return (
@@ -819,6 +868,7 @@ export const RenderRichMultiline = ({
theme="snow"
value={editValue}
onChange={changeEdit}
onBlur={handleBlur}
/>
<textarea
{...input}
@@ -874,19 +924,19 @@ const baseStyle = {
borderStyle: "dashed",
backgroundColor: "#fafafa",
color: "#bdbdbd",
transition: "border .3s ease-in-out",
transition: "border .3s ease-in-out"
};
const activeStyle = {
borderColor: "#2196f3",
borderColor: "#2196f3"
};
const acceptStyle = {
borderColor: "#00e676",
borderColor: "#00e676"
};
const rejectStyle = {
borderColor: "#ff1744",
borderColor: "#ff1744"
};
export const RenderFileUpload = (field) => {
@@ -897,7 +947,7 @@ export const RenderFileUpload = (field) => {
const style = useMemo(
() => ({
...baseStyle,
...baseStyle
// ...(isDragActive ? activeStyle : {}),
// ...(isDragAccept ? acceptStyle : {}),
// ...(isDragReject ? rejectStyle : {}),
@@ -939,10 +989,6 @@ export const RenderFileUpload = (field) => {
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
return "/assets/images/documenttypes/xlsx.png";
break;
case "application/zip":
return "/assets/images/documenttypes/zip.png";
break;
case "image/jpeg":
return "/assets/images/documenttypes/jpg.png";
case "image/png":
@@ -997,7 +1043,7 @@ export const RenderFileUpload = (field) => {
typeof filesUploadObj[key].name != "undefined" &&
buildAppealFilesArray.push({
"name": filesUploadObj[key].name,
"size": filesUploadObj[key].size,
"size": filesUploadObj[key].size
});
}
@@ -1046,9 +1092,12 @@ export const RenderFileUpload = (field) => {
});
};
const [errorMessage, setErrorMessage] = useState(null);
const [rejectedFiles, setRejectedFiles] = useState([]); // Store rejected files
const [completedUploadFiles, setCompletedUploadFiles] = useState(false);
const [uploadCountMessage, setUploadCountMessage] = useState("");
const [totalUploadFiles, setTotalUploadFiles] = useState(0);
const handleDropRejected = (fileRejections) => {
const errors = fileRejections
@@ -1064,6 +1113,15 @@ export const RenderFileUpload = (field) => {
"myrepresentations:fileupload-file-error-invalid-type-label"
)}`;
}
if (error.code === "filename-invalid-chars") {
return (
error.message ||
`${file.name} ${t(
"newappeal:new-appeal-fileupload-file-error-invalid-filename-label"
)}`
);
}
return `${file.name} ${t(
"myrepresentations:fileupload-file-error-invalid-label"
)}`;
@@ -1080,6 +1138,44 @@ export const RenderFileUpload = (field) => {
setRejectedFiles([]); // Clear any previously rejected file errorsss
};
const FILENAME_ALLOWED = /^[A-Za-z0-9 ._\-:()—']+$/;
const validateFilename = (file, t) => {
const name = file.name;
// block #
if (name.includes("#")) {
return {
code: "filename-invalid-chars",
message: `${name} ${t(
"myrepresentations:fileupload-file-error-invalid-filename-label"
)}`
};
}
// broader blocklist (optional but recommended)
if (!FILENAME_ALLOWED.test(name)) {
return {
code: "filename-invalid-chars",
message: `${name} ${t(
"myrepresentations:fileupload-file-error-invalid-filename-label"
)}`
};
}
// block characters < > " / \ | ? *
if (/[<>"/\\|?*]/.test(name)) {
return {
code: "filename-invalid-chars",
message: `${name} ${t(
"myrepresentations:fileupload-file-error-invalid-filename-label"
)}`
};
}
return null; // valid
};
return (
<>
<Dropzone
@@ -1091,14 +1187,32 @@ export const RenderFileUpload = (field) => {
[".docx"],
"image/tiff": [".tif", ".tiff"],
"image/jpeg": [".jpg", ".jpeg"],
"application/zip": [".zip"],
"image/png": [".png"],
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
[".xlsx"],
[".xlsx"]
}}
onDrop={(filesToUpload, e) => {
validator={(file) => validateFilename(file, t)}
onDrop={(acceptedFiles, fileRejections, e) => {
// build + display rejected messages (ALWAYS, even if some accepted)
if (fileRejections?.length) {
handleDropRejected(fileRejections);
} else {
// only clear rejections when nothing was rejected on this drop
setRejectedFiles([]);
setErrorMessage(null);
}
// if nothing accepted, stop (prevents showing only "uploading" when all invalid)
if (!acceptedFiles || acceptedFiles.length === 0) {
setCompletedUploadFiles(true); // optional: depends on your UX
setUploadCountMessage(0);
setTotalUploadFiles(0);
return;
}
setCompletedUploadFiles(false);
const renamedAcceptedFiles = filesToUpload.map(
const renamedAcceptedFiles = acceptedFiles.map(
(file) =>
new File(
[file],
@@ -1116,12 +1230,22 @@ export const RenderFileUpload = (field) => {
.values.repfile_name
}_-_Attachment_-_${file.name}`,
{
type: file.type,
type: file.type
}
)
);
field.input.onChange(renamedAcceptedFiles);
setUploadCountMessage(renamedAcceptedFiles.length);
if (typeof field.setFileCount === "function") {
field.setFileCount(
(field.uploadCount || 0) +
renamedAcceptedFiles.length
);
}
setUploadCountMessage(0);
setTotalUploadFiles(renamedAcceptedFiles.length);
uploadSingleFile(
renamedAcceptedFiles,
@@ -1129,7 +1253,12 @@ export const RenderFileUpload = (field) => {
field.ticketnumber +
"/" +
field.filenamePrefix.representationForm.values
.repfile_name
.repfile_name,
{
onChunkComplete: ({ cumulativeUploaded }) => {
setUploadCountMessage(cumulativeUploaded);
}
}
).then((data) => {
console.log(data);
let buildAppealFilesArray = [];
@@ -1140,7 +1269,7 @@ export const RenderFileUpload = (field) => {
typeof filesUploadObj[key].name != "undefined" &&
buildAppealFilesArray.push({
"name": filesUploadObj[key].name,
"size": filesUploadObj[key].size,
"size": filesUploadObj[key].size
});
}
@@ -1201,8 +1330,6 @@ export const RenderFileUpload = (field) => {
});
});
}}
onDropRejected={handleDropRejected}
onDropAccepted={handleDropAccepted}
>
{({ getRootProps, getInputProps }) => (
<>
@@ -1229,7 +1356,7 @@ export const RenderFileUpload = (field) => {
</>
)}
</Dropzone>
{rejectedFiles.length > 0 && completedUploadFiles == true && (
{rejectedFiles.length > 0 && (
<div
style={{ color: "red", marginTop: "10px" }}
className="govuk-body govuk-!-font-size-14 "
@@ -1241,17 +1368,20 @@ export const RenderFileUpload = (field) => {
</ul>
</div>
)}
{uploadCountMessage > 0 && completedUploadFiles == false && (
{totalUploadFiles > 0 && completedUploadFiles == false && (
<div className="govuk-body govuk-!-font-size-14">
{t("home:uploading-files-label", {
number: uploadCountMessage,
number:
totalUploadFiles > 0
? `${uploadCountMessage} of ${totalUploadFiles}`
: uploadCountMessage
})}
</div>
)}
{completedUploadFiles > 0 && uploadCountMessage >= 0 && (
<div className="govuk-body govuk-!-font-size-14">
{t("home:completed-uploading-files-label", {
number: uploadCountMessage,
number: uploadCountMessage
})}
</div>
)}
@@ -1338,7 +1468,7 @@ export const RenderFileUpload = (field) => {
?.values?.repfile_name
) +
"&blobname=" +
blob.name +
encodeURIComponent(blob.name) +
blob.hashedfilepath
}
className="govuk-body govuk-!-font-size-14 govuk-!-padding-left-5 govuk-link"
@@ -2,22 +2,23 @@ import useTranslation from "next-translate/useTranslation";
import { useRouter } from "next/router";
import { connect } from "react-redux";
import { Field, change, reduxForm } from "redux-form";
import { useState } from "react";
import { useEffect, useState } from "react";
import { bytesToSize, getThumbnailIconByExtension } from "../../utils";
import {
FileUploadField,
RenderCondtionalRadioList,
RenderPickList,
RenderRichMultiline,
shouldDisableButton,
shouldDisableButton
} from "./representationElements";
import RepresentationActionButtons from "./elements/RepresentationActionButtons";
import RepresentationTypeSelectorBlock from "./elements/RepresentationTypeSelectorBlock";
import { useDropzone } from "react-dropzone";
import {
setRepresentationCapacity,
setRepresentationSubmit,
setFilesForRepresentations,
setFilesForRepresentations
} from "../../../store/currentView/action";
const RepInterestedPartyPerson = (props) => {
@@ -35,11 +36,25 @@ const RepInterestedPartyPerson = (props) => {
setSavingStatus,
savingStatus,
setFilesForRepresentations,
onRepresentationTypeChange,
onTypeSelectionConfirmed
} = props;
const required = (value) => (value ? undefined : "Required");
const { acceptedFiles, getRootProps, getInputProps } = useDropzone();
const persistedRepresentationType =
props.currentView?.caseReference?.repDetails?.representationType;
const [typeSelectionConfirmed, setTypeSelectionConfirmed] = useState(
!!persistedRepresentationType
);
useEffect(() => {
if (persistedRepresentationType) {
setTypeSelectionConfirmed(true);
}
}, [persistedRepresentationType]);
if (JSON.stringify(formObj) != "{}") {
setRepFileName(formObj.representationForm.values);
@@ -62,65 +77,52 @@ const RepInterestedPartyPerson = (props) => {
{file.path} - {file.size} bytes
</li>
));
const selectedRepresentationType =
formObj?.representationForm?.values?.representationType ||
props.representationType;
const isTypeConfirmed =
!!selectedRepresentationType && typeSelectionConfirmed;
const selectedRepresentationTypeDisplay =
router.locale == "cy"
? selectedRepresentationType == "Questionnaire"
? "Holiadur"
: selectedRepresentationType == "Statement"
? "Datganiad"
: selectedRepresentationType == "Final comments"
? "Sylwadau terfynol"
: selectedRepresentationType
: selectedRepresentationType;
const handleSaveExit = () => {
let valuesObj = props.props.props.form[props.form].values || {};
delete valuesObj["_pinswg_appellant_value"];
updateRepresentation(valuesObj, false, true);
};
return (
<>
<div className="govuk-grid-row">
<div className="govuk-form-group">
{typeof formObj.representationForm.values
.representationType == "undefined" &&
props.representationType != "Select..." ? (
<>
<h2 className="govuk-heading-m ">
{t(
<RepresentationTypeSelectorBlock
t={t}
headingText={t(
"myrepresentations:consultation-from-an-interested-person-heading"
)}
</h2>
<label
className="govuk-label govuk-body-m"
htmlFor="representationType"
>
{t("myrepresentations:kind-of-rep-label")}
</label>
<Field
name="representationType"
component={RenderPickList}
datafieldname="representationType"
kindLabelText={t("myrepresentations:kind-of-rep-label")}
isTypeSelected={isTypeConfirmed}
selectedTypeDisplay={selectedRepresentationTypeDisplay}
representationType={selectedRepresentationType}
setTypeSelectionConfirmed={setTypeSelectionConfirmed}
onRepresentationTypeChange={onRepresentationTypeChange}
onTypeSelectionConfirmed={onTypeSelectionConfirmed}
optionsArr={interestedPersonRepresentationArr}
validate={[required]}
optionsArr={
interestedPersonRepresentationArr
}
errorMsg={t(
"myrepresentations:select-an-option-label"
)}
errorMsg={t("myrepresentations:select-an-option-label")}
/>
</>
) : (
<label className="govuk-label govuk-body-m">
{t("myrepresentations:representation-type")}:{" "}
{router.locale == "cy"
? formObj.representationForm.values
.representationType == "Questionnaire"
? "Holiadur"
: formObj.representationForm.values
.representationType == "Statement"
? "Datganiad"
: formObj.representationForm.values
.representationType ==
"Final comments"
? "Sylwadau terfynol"
: formObj.representationForm.values
.representationType
: formObj.representationForm.values
.representationType}
</label>
)}
</div>
</div>
{(typeof props.representationType != "undefined" ||
typeof formObj.representationForm.values
.representationType != "undefined") && (
{isTypeConfirmed && (
<>
<div className="govuk-grid-row">
<div className="govuk-form-group">
@@ -128,12 +130,7 @@ const RepInterestedPartyPerson = (props) => {
name="representationOnBehalfOf"
datafieldname="representationOnBehalfOf"
// label="In what capacity do you wish to make representations on this case?"
options={[
t(
"myrepresentations:questionnaire-yes"
),
t("myrepresentations:questionnaire-no"),
]}
options={["Yes", "No"]}
id="representationOnBehalfOf"
className="govuk-radios__input"
errorMsg={t(
@@ -189,7 +186,8 @@ const RepInterestedPartyPerson = (props) => {
"myrepresentations:add-files-label"
)}
ticketnumber={
props.props.caseReference
props.currentView.caseReference
.ticketnumber
}
hint={"sdsd"}
fileList={
@@ -231,82 +229,17 @@ const RepInterestedPartyPerson = (props) => {
</div>
</>
)}
{typeof formObj.representationForm.values.representationType !=
"undefined" && (
<div className="govuk-grid-row">
<div className="govuk-button-group">
<button
type="submit"
className="govuk-button"
data-module="govuk-button"
disabled={shouldDisableButton({
{isTypeConfirmed && (
<RepresentationActionButtons
t={t}
locale={router.locale}
isSaving={savingStatus}
isContinueDisabled={shouldDisableButton({
props,
formObj,
formObj
})}
>
{t("common:continue-button")}
</button>
{/*
<a
onClick={() => {
setRepresentationCapacity();
props.updateField(
"representationForm",
"representationCapacity",
""
);
props.updateField(
"representationForm",
"representationType",
""
);
}}
className="govuk-link"
>
{t("case:summary-back-button-label")}
</a> */}
{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"
}
onSaveExit={handleSaveExit}
/>
</span>
) : (
<button
className="govuk-button progress-save "
onClick={() => {
let valuesObj =
props.props.props.form[props.form]
.values || {};
//console.log("valuesobj:", valuesObj);
delete valuesObj[
"_pinswg_appellant_value"
];
//console.log("on save:", valuesObj);
updateRepresentation(
valuesObj,
false,
true
);
}}
>
{t("common:save-exit-button")}
</button>
)}
</div>
</div>
)}
</>
);
@@ -320,7 +253,7 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(change(form, field, newValue)),
setFilesForRepresentations: (fileList) => {
dispatch(setFilesForRepresentations(fileList));
},
}
});
export default connect(
@@ -330,6 +263,6 @@ export default connect(
reduxForm({
form: "representationForm",
enableReinitialize: true,
destroyOnUnmount: false,
destroyOnUnmount: false
})(RepInterestedPartyPerson)
);
@@ -10,9 +10,9 @@ import { useState, useEffect } from "react";
import {
FileUploadField,
RenderMultiline,
RenderPickList,
shouldDisableButton,
shouldDisableButton
} from "./representationElements";
import RepresentationTypeSelectorBlock from "./elements/RepresentationTypeSelectorBlock";
import RepresentationProgress_enforcement from "./representationProgress/representationProgress_enforcement";
import RepresentationProgress_s78 from "./representationProgress/representationProgress_s78";
import Enforcement_Questionnaire from "./representationQuestionnaires/representationLPAQuestionnaire_enforcement";
@@ -23,7 +23,7 @@ import {
setRepresentationSubmit,
setFilesForRepresentations,
setCurrentPage,
setCurrentReference,
setCurrentReference
} from "../../../store/currentView/action";
let RepLPA = (props) => {
@@ -49,6 +49,8 @@ let RepLPA = (props) => {
setSavingStatus,
savingStatus,
setFilesForRepresentations,
onRepresentationTypeChange,
onTypeSelectionConfirmed
} = props;
const { acceptedFiles, getRootProps, getInputProps } = useDropzone();
@@ -59,6 +61,36 @@ let RepLPA = (props) => {
));
const required = (value) => (value ? undefined : "Required");
const persistedRepresentationType =
currentView?.caseReference?.repDetails?.representationType;
const [typeSelectionConfirmed, setTypeSelectionConfirmed] = useState(
!!persistedRepresentationType
);
useEffect(() => {
if (persistedRepresentationType) {
setTypeSelectionConfirmed(true);
}
}, [persistedRepresentationType]);
const selectedRepresentationType =
formObj?.representationForm?.values?.representationType ||
props.representationType;
const isTypeConfirmed =
!!selectedRepresentationType && typeSelectionConfirmed;
const selectedRepresentationTypeDisplay =
router.locale == "cy"
? selectedRepresentationType == "Questionnaire"
? "Holiadur"
: selectedRepresentationType == "Statement"
? "Datganiad"
: selectedRepresentationType == "Final comments"
? "Sylwadau terfynol"
: selectedRepresentationType
: selectedRepresentationType;
const whichQuestionnaire = () => {
{
switch (currentView.caseReference.appealType) {
@@ -183,7 +215,7 @@ let RepLPA = (props) => {
currentView.caseReference.currentReference +
'")]',
json: setCaseDetailsObj,
eval: true,
eval: true
});
let setCaseResultssObj = router.query.hasOwnProperty("state")
@@ -202,7 +234,7 @@ let RepLPA = (props) => {
currentView.caseReference.currentReference +
'")]',
json: setCaseResultssObj,
eval: true,
eval: true
});
resultsObj = resultsObj[0];
@@ -225,62 +257,26 @@ let RepLPA = (props) => {
return (
<>
<div className="govuk-grid-row">
<div className="govuk-form-group">
{!_.has(
formObj["representationForm"],
["values", "representationType"] ||
typeof props.currentView.caseReference.repDetails !=
"undefined"
) ? (
<>
<label
className="govuk-label govuk-body-m"
htmlFor="representationType"
>
{t(
<RepresentationTypeSelectorBlock
t={t}
headingText={t(
"myrepresentations:representation-from-an-lpa-heading"
)}
</label>
<Field
id="representationType"
name="representationType"
component={RenderPickList}
datafieldname="representationType"
kindLabelText={t("myrepresentations:kind-of-rep-label")}
isTypeSelected={isTypeConfirmed}
selectedTypeDisplay={selectedRepresentationTypeDisplay}
representationType={selectedRepresentationType}
setTypeSelectionConfirmed={setTypeSelectionConfirmed}
onRepresentationTypeChange={onRepresentationTypeChange}
onTypeSelectionConfirmed={onTypeSelectionConfirmed}
optionsArr={appellantApplicantRepresentationArr}
validate={[required]}
errorMsg={t("myrepresentations:select-an-option-label")}
/>
</>
) : (
<label className="govuk-label govuk-body-m">
{t("myrepresentations:representation-type")}:{" "}
{router.locale == "cy"
? formObj.representationForm.values
.representationType == "Questionnaire"
? "Holiadur"
: formObj.representationForm.values
.representationType == "Statement"
? "Datganiad"
: formObj.representationForm.values
.representationType ==
"Final comments"
? "Sylwadau terfynol"
: formObj.representationForm.values
.representationType
: formObj.representationForm.values
.representationType}
</label>
)}
</div>
</div>
<div className="govuk-grid-row">
<div className="govuk-form-group">
{(_.has(formObj["representationForm"], [
"values",
"representationType",
]) ||
typeof props.representationType != "undefined") &&
(props.representationType != "Questionnaire" &&
props.representationType != "Select..." ? (
{isTypeConfirmed &&
(selectedRepresentationType != "Questionnaire" ? (
<>
<p className="govuk-body">
{t("myrepresentations:enter-comment-label")}
@@ -354,7 +350,8 @@ let RepLPA = (props) => {
"myrepresentations:add-files-label"
)}
ticketnumber={
props.props.caseReference
props.currentView.caseReference
.ticketnumber
}
hint={"sdsd"}
fileList={
@@ -401,9 +398,8 @@ let RepLPA = (props) => {
</div>
<div className="govuk-grid-row">
<div className="govuk-button-group">
{(typeof props.representationType != "undefined" ||
_.has(formObj, "values", "representationType")) &&
props.representationType == "Questionnaire" && (
{isTypeConfirmed &&
selectedRepresentationType == "Questionnaire" && (
<>
{showQuestionnaireSection <
questionnaireCount && (
@@ -443,14 +439,14 @@ let RepLPA = (props) => {
// top: 0,
// behavior: "smooth",
// });
setRepFileName(
(setRepFileName(
props.formObj[
"representationForm"
].values
),
setShowQuestionnaireSection(
showQuestionnaireSection - 1
);
));
}}
className="govuk-link"
>
@@ -503,9 +499,8 @@ let RepLPA = (props) => {
)}
</>
)}{" "}
{(typeof props.representationType != "undefined" ||
_.has(formObj, "values", "representationType")) &&
props.representationType != "Questionnaire" && (
{isTypeConfirmed &&
selectedRepresentationType != "Questionnaire" && (
<>
<button
type="submit"
@@ -513,7 +508,7 @@ let RepLPA = (props) => {
data-module="govuk-button"
disabled={shouldDisableButton({
props,
formObj,
formObj
})}
onClick={() => {
let valuesObj =
@@ -612,7 +607,7 @@ RepLPA = connect((state) => {
return {
procedureType,
representationType,
representationType
};
})(RepLPA);
@@ -625,7 +620,7 @@ const mapDispatchToProps = (dispatch) => ({
},
setCurrentReference: (currentReference) => {
dispatch(setCurrentReference(currentReference));
},
}
});
const mapStateToProps = (state, props) => {
@@ -641,8 +636,7 @@ const mapStateToProps = (state, props) => {
],
"firstname":
props.props.props.accountDetails.accountDetails.firstname,
"lastname":
props.props.props.accountDetails.accountDetails.lastname,
"lastname": props.props.props.accountDetails.accountDetails.lastname
}
);
return {
@@ -653,7 +647,7 @@ const mapStateToProps = (state, props) => {
currentView: state.currentView,
myRepresentations: state.myRepresentations,
watchedCases: state.watchedCases,
initialValues: initialValuesTemp,
initialValues: initialValuesTemp
};
};
@@ -664,6 +658,6 @@ export default connect(
reduxForm({
form: "representationForm",
enableReinitialize: true,
destroyOnUnmount: false,
destroyOnUnmount: false
})(RepLPA)
);
@@ -34,6 +34,7 @@ let RepLPACapacitySelection = (props) => {
casesObj={casesObj}
detailsObj={detailsObj}
caseReference={props.props.caseReference}
ticketnumber={props.props.ticketnumber}
searchResultsObj={props.props.searchResultsObj}
props={props.props}
/>{" "}
@@ -8,7 +8,7 @@ const RepLPADetails = (props) => {
const router = useRouter();
const { locale } = router;
const { currentType, casesObj, caseReference } = props;
const { currentType, casesObj, caseReference, ticketnumber } = props;
let setCaseDetailsObj = router.query.hasOwnProperty("state")
? router.query.state == "edit"
@@ -21,7 +21,7 @@ const RepLPADetails = (props) => {
var detailsObj = {};
detailsObj = jsonpath({
path: '$..[?(@ && @.pinswg_name=="' + caseReference + '")]',
path: '$..[?(@ && @.ticketnumber=="' + ticketnumber + '")]',
json: setCaseDetailsObj,
eval: true,
});
@@ -84,7 +84,10 @@ const RepLandOwner = (props) => {
label={t(
"myrepresentations:add-files-label"
)}
ticketnumber={props.props.caseReference}
ticketnumber={
props.currentView.caseReference
.ticketnumber
}
hint={"sdsd"}
fileList={[]}
setFilesForRepresentation={[]}
@@ -14,12 +14,12 @@ import {
RenderPickList,
RenderRadioList,
RenderRichMultiline,
RenderTextfield,
RenderTextfield
} from "../representationElements";
import {
setRepresentationCapacity,
setRepresentationSubmit,
setFilesForRepresentations,
setFilesForRepresentations
} from "../../../../store/currentView/action";
let Enforcement_Questionnaire = (props) => {
@@ -36,7 +36,7 @@ let Enforcement_Questionnaire = (props) => {
procedureTypeValue,
onBehalfOfLPA,
showQuestionnaireSection,
setFilesForRepresentations,
setFilesForRepresentations
} = props;
const { acceptedFiles, getRootProps, getInputProps } = useDropzone();
const files = acceptedFiles.map((file) => (
@@ -61,10 +61,10 @@ let Enforcement_Questionnaire = (props) => {
detailsObj = jsonpath({
path:
'$..[?(@ && @.pinswg_name=="' +
currentView.caseReference.currentReference +
currentView.caseReference.ticketnumber +
'")]',
json: setCaseDetailsObj,
eval: true,
eval: true
});
let setCaseResultssObj = router.query.hasOwnProperty("state")
@@ -84,10 +84,10 @@ let Enforcement_Questionnaire = (props) => {
resultsObj = jsonpath({
path:
'$..[?(@ && @.ticketnumber=="' +
currentView.caseReference.currentReference +
currentView.caseReference.ticketnumber +
'")]',
json: setCaseResultssObj,
eval: true,
eval: true
});
resultsObj = resultsObj[0];
@@ -118,7 +118,7 @@ let Enforcement_Questionnaire = (props) => {
<div className="govuk-form-group">
<div className="govuk-label">
2. {t("myrepresentations:enf-section-question-2")}:{" "}
{resultsObj.pinswg_lpareference}
{resultsObj.pinswg_lpareference || ""}
</div>
</div>
<div className="govuk-form-group">
@@ -155,7 +155,7 @@ let Enforcement_Questionnaire = (props) => {
optionsArr={[
"Written representations",
"Hearing",
"Inquiry",
"Inquiry"
]}
id="procedureType"
component={RenderPickList}
@@ -190,7 +190,7 @@ let Enforcement_Questionnaire = (props) => {
component={RenderRadioList}
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
type="text"
rows="5"
@@ -207,7 +207,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="enterNeighbouringLandToViewSite"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="enterNeighbouringLandToViewSite"
className="govuk-radios__input"
@@ -236,7 +236,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="enterNeighbouringLandAccessRequired"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="enterNeighbouringLandAccessRequired"
className="govuk-radios__input"
@@ -265,7 +265,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="healthAndSafetyIssues"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="healthAndSafetyIssues"
className="govuk-radios__input"
@@ -328,7 +328,7 @@ let Enforcement_Questionnaire = (props) => {
component={RenderRadioList}
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
type="text"
rows="5"
@@ -342,7 +342,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="feeExempt"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="feeExempt"
className="govuk-radios__input"
@@ -379,7 +379,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="previousGrantedCerts"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="previousGrantedCerts"
className="govuk-radios__input"
@@ -406,7 +406,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="stopNoticeServed"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="stopNoticeServed"
className="govuk-radios__input"
@@ -433,7 +433,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="relatedCases"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="relatedCases"
className="govuk-radios__input"
@@ -460,7 +460,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="previousEnforcement"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="previousEnforcement"
className="govuk-radios__input"
@@ -487,7 +487,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="subjectArticle4"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="subjectArticle4"
className="govuk-radios__input"
@@ -514,7 +514,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="previousPDRRestriction"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="previousPDRRestriction"
className="govuk-radios__input"
@@ -594,7 +594,7 @@ let Enforcement_Questionnaire = (props) => {
component={RenderRadioList}
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
type="text"
rows="5"
@@ -621,7 +621,7 @@ let Enforcement_Questionnaire = (props) => {
component={RenderRadioList}
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
type="text"
rows="5"
@@ -635,7 +635,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="ROW"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="ROW"
className="govuk-radios__input"
@@ -662,7 +662,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="conservationArea"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="conservationArea"
className="govuk-radios__input"
@@ -689,7 +689,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="affectListedBuilding"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="affectListedBuilding"
className="govuk-radios__input"
@@ -716,7 +716,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="TPO"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="TPO"
className="govuk-radios__input"
@@ -743,7 +743,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="natureConservationSite"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="natureConservationSite"
className="govuk-radios__input"
@@ -770,7 +770,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="protectedSpecies"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="protectedSpecies"
className="govuk-radios__input"
@@ -802,7 +802,7 @@ let Enforcement_Questionnaire = (props) => {
component={RenderRadioList}
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
type="text"
rows="5"
@@ -816,7 +816,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="floodingIssue"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="floodingIssue"
className="govuk-radios__input"
@@ -843,7 +843,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="governmentDepartmentComments"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="governmentDepartmentComments"
className="govuk-radios__input"
@@ -884,7 +884,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="developmentCarriedOutInAccordance"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="developmentCarriedOutInAccordance"
className="govuk-radios__input"
@@ -915,7 +915,7 @@ let Enforcement_Questionnaire = (props) => {
component={RenderRadioList}
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
type="text"
rows="5"
@@ -932,7 +932,7 @@ let Enforcement_Questionnaire = (props) => {
component={RenderRadioList}
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
type="text"
rows="5"
@@ -946,7 +946,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="developmentAnyOtherConditions"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="developmentAnyOtherConditions"
className="govuk-radios__input"
@@ -987,7 +987,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="copyOfEnforcementNotice"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="copyOfEnforcementNotice"
className="govuk-radios__input"
@@ -1014,7 +1014,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="enforcementNoticePlan"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="enforcementNoticePlan"
className="govuk-radios__input"
@@ -1041,7 +1041,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="listOfNotifiedOfEnforcement"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="listOfNotifiedOfEnforcement"
className="govuk-radios__input"
@@ -1068,7 +1068,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="copyOfLetterOfAppealNotification"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="copyOfLetterOfAppealNotification"
className="govuk-radios__input"
@@ -1095,7 +1095,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="lpaEIAOS"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="lpaEIAOS"
className="govuk-radios__input"
@@ -1125,7 +1125,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="detailsOfOtherAppeals"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="detailsOfOtherAppeals"
className="govuk-radios__input"
@@ -1152,7 +1152,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="detailsOfPreviousAppeals"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="detailsOfPreviousAppeals"
className="govuk-radios__input"
@@ -1179,7 +1179,7 @@ let Enforcement_Questionnaire = (props) => {
datafieldname="otherRelevantInformation"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="otherRelevantInformation"
className="govuk-radios__input"
@@ -1213,7 +1213,10 @@ let Enforcement_Questionnaire = (props) => {
name={"representationDocuments"}
id={"representationDocuments"}
label={t("myrepresentations:add-files-label")}
ticketnumber={props.props.props.caseReference}
ticketnumber={
props.props.currentView.caseReference
.ticketnumber
}
hint={"sdsd"}
fileList={
props.currentView.fileList
@@ -1309,7 +1312,7 @@ Enforcement_Questionnaire = connect((state) => {
return {
procedureTypeValue,
representationTypeValue,
representationTypeValue
};
})(Enforcement_Questionnaire);
@@ -1320,7 +1323,7 @@ const mapDispatchToProps = (dispatch) => {
setFilesForRepresentations: (fileList) => {
dispatch(setFilesForRepresentations(fileList));
},
}
};
};
@@ -1332,7 +1335,7 @@ const mapStateToProps = (state, props) => {
appealType: state.appealType,
currentView: state.currentView,
myRepresentations: state.myRepresentations,
initialValues: state.currentView.caseReference.repDetails,
initialValues: state.currentView.caseReference.repDetails
};
};
@@ -1343,6 +1346,6 @@ export default connect(
reduxForm({
form: "representationForm",
enableReinitialize: false,
destroyOnUnmount: false,
destroyOnUnmount: false
})(Enforcement_Questionnaire)
);
@@ -4,6 +4,7 @@ import { useRouter } from "next/router";
import { useDropzone } from "react-dropzone";
import { Field, reduxForm, formValueSelector } from "redux-form";
import { getFormCollectionByID } from "../../../../components/utils";
import {
FileUploadField,
RenderDatePicker,
@@ -12,12 +13,12 @@ import {
RenderPickList,
RenderRadioList,
RenderRichMultiline,
RenderTextfield,
RenderTextfield
} from "../representationElements";
import {
setRepresentationCapacity,
setRepresentationSubmit,
setFilesForRepresentations,
setFilesForRepresentations
} from "../../../../store/currentView/action";
import { connect } from "react-redux";
@@ -38,7 +39,7 @@ let S78_Questionnaire = (props) => {
procedureTypeValue,
onBehalfOfLPA,
showQuestionnaireSection,
setFilesForRepresentations,
setFilesForRepresentations
} = props;
const { acceptedFiles, getRootProps, getInputProps } = useDropzone();
const files = acceptedFiles.map((file) => (
@@ -62,11 +63,11 @@ let S78_Questionnaire = (props) => {
detailsObj = jsonpath({
path:
'$..[?(@ && @.pinswg_name=="' +
currentView.caseReference.currentReference +
'$..[?(@ && @.ticketnumber=="' +
currentView.caseReference.ticketnumber +
'")]',
json: setCaseDetailsObj,
eval: true,
eval: true
});
let setCaseResultssObj = router.query.hasOwnProperty("state")
@@ -92,10 +93,10 @@ let S78_Questionnaire = (props) => {
: "ticketnumber"
: "ticketnumber") +
'=="' +
currentView.caseReference.currentReference +
currentView.caseReference.ticketnumber +
'")]',
json: setCaseResultssObj,
eval: true,
eval: true
});
resultsObj = resultsObj[0];
@@ -136,7 +137,7 @@ let S78_Questionnaire = (props) => {
<div className="govuk-form-group">
<div className="govuk-label">
2. {t("myrepresentations:s78-section-question-2")}:{" "}
{resultsObj.pinswg_lpareference}
{resultsObj.pinswg_lpareference || ""}
</div>
</div>
<div className="govuk-form-group">
@@ -175,7 +176,7 @@ let S78_Questionnaire = (props) => {
optionsArr={[
"Written representations",
"Hearing",
"Inquiry",
"Inquiry"
]}
id="procedureType"
component={RenderPickList}
@@ -209,7 +210,7 @@ let S78_Questionnaire = (props) => {
component={RenderRadioList}
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
type="text"
rows="5"
@@ -224,7 +225,7 @@ let S78_Questionnaire = (props) => {
datafieldname="enterNeighbouringLandToViewSite"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="enterNeighbouringLandToViewSite"
className="govuk-radios__input"
@@ -253,7 +254,7 @@ let S78_Questionnaire = (props) => {
datafieldname="enterNeighbouringLandAccessRequired"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="enterNeighbouringLandAccessRequired"
className="govuk-radios__input"
@@ -282,7 +283,7 @@ let S78_Questionnaire = (props) => {
datafieldname="healthAndSafetyIssues"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="healthAndSafetyIssues"
className="govuk-radios__input"
@@ -405,7 +406,7 @@ let S78_Questionnaire = (props) => {
component={RenderRadioList}
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
type="text"
rows="5"
@@ -434,7 +435,7 @@ let S78_Questionnaire = (props) => {
component={RenderRadioList}
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
type="text"
rows="5"
@@ -450,7 +451,7 @@ let S78_Questionnaire = (props) => {
datafieldname="ROW"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="ROW"
className="govuk-radios__input"
@@ -477,7 +478,7 @@ let S78_Questionnaire = (props) => {
datafieldname="conservationArea"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="conservationArea"
className="govuk-radios__input"
@@ -504,7 +505,7 @@ let S78_Questionnaire = (props) => {
datafieldname="affectListedBuilding"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="affectListedBuilding"
className="govuk-radios__input"
@@ -531,7 +532,7 @@ let S78_Questionnaire = (props) => {
datafieldname="TPO"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="TPO"
className="govuk-radios__input"
@@ -558,7 +559,7 @@ let S78_Questionnaire = (props) => {
datafieldname="natureConservationSite"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="natureConservationSite"
className="govuk-radios__input"
@@ -585,7 +586,7 @@ let S78_Questionnaire = (props) => {
datafieldname="protectedSpecies"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="protectedSpecies"
className="govuk-radios__input"
@@ -616,7 +617,7 @@ let S78_Questionnaire = (props) => {
component={RenderRadioList}
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
type="text"
rows="5"
@@ -629,7 +630,7 @@ let S78_Questionnaire = (props) => {
datafieldname="governmentDepartmentComments"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="governmentDepartmentComments"
className="govuk-radios__input"
@@ -675,7 +676,7 @@ let S78_Questionnaire = (props) => {
component={RenderRadioList}
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
type="text"
rows="5"
@@ -691,7 +692,7 @@ let S78_Questionnaire = (props) => {
datafieldname="developmentCarriedOutInAccordance"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="developmentCarriedOutInAccordance"
className="govuk-radios__input"
@@ -722,7 +723,7 @@ let S78_Questionnaire = (props) => {
component={RenderRadioList}
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
type="text"
rows="5"
@@ -741,7 +742,7 @@ let S78_Questionnaire = (props) => {
component={RenderRadioList}
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
type="text"
rows="5"
@@ -757,7 +758,7 @@ let S78_Questionnaire = (props) => {
datafieldname="developmentAnyOtherConditions"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="developmentAnyOtherConditions"
className="govuk-radios__input"
@@ -797,7 +798,7 @@ let S78_Questionnaire = (props) => {
datafieldname="copyOfApplicantsCertificate"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="copyOfApplicantsCertificate"
className="govuk-radios__input"
@@ -825,7 +826,7 @@ let S78_Questionnaire = (props) => {
datafieldname="advertismentGiven"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="advertismentGiven"
className="govuk-radios__input"
@@ -853,7 +854,7 @@ let S78_Questionnaire = (props) => {
datafieldname="copyOfLetterOfAppealNotification"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="copyOfLetterOfAppealNotification"
className="govuk-radios__input"
@@ -880,7 +881,7 @@ let S78_Questionnaire = (props) => {
datafieldname="lpaEIAOS"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="lpaEIAOS"
className="govuk-radios__input"
@@ -907,7 +908,7 @@ let S78_Questionnaire = (props) => {
datafieldname="advertismentConsentDiscontinuanceNotice"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="advertismentConsentDiscontinuanceNotice"
className="govuk-radios__input"
@@ -934,7 +935,7 @@ let S78_Questionnaire = (props) => {
datafieldname="detailsOfOtherAppeals"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="detailsOfOtherAppeals"
className="govuk-radios__input"
@@ -961,7 +962,7 @@ let S78_Questionnaire = (props) => {
datafieldname="detailsOfPreviousAppeals"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="detailsOfPreviousAppeals"
className="govuk-radios__input"
@@ -988,7 +989,7 @@ let S78_Questionnaire = (props) => {
datafieldname="otherRelevantInformation"
options={[
t("myrepresentations:questionnaire-yes"),
t("myrepresentations:questionnaire-no"),
t("myrepresentations:questionnaire-no")
]}
id="otherRelevantInformation"
className="govuk-radios__input"
@@ -1022,7 +1023,10 @@ let S78_Questionnaire = (props) => {
name={"representationDocuments"}
id={"representationDocuments"}
label={t("myrepresentations:add-files-label")}
ticketnumber={props.props.props.caseReference}
ticketnumber={
props.props.currentView.caseReference
.ticketnumber
}
hint={"sdsd"}
fileList={
props.currentView.fileList
@@ -1117,7 +1121,7 @@ S78_Questionnaire = connect((state) => {
return {
procedureTypeValue,
representationTypeValue,
representationTypeValue
};
})(S78_Questionnaire);
@@ -1128,7 +1132,7 @@ const mapDispatchToProps = (dispatch) => {
setFilesForRepresentations: (fileList) => {
dispatch(setFilesForRepresentations(fileList));
},
}
};
};
@@ -1140,7 +1144,7 @@ const mapStateToProps = (state, props) => {
appealType: state.appealType,
currentView: state.currentView,
myRepresentations: state.myRepresentations,
initialValues: state.currentView.caseReference.repDetails,
initialValues: state.currentView.caseReference.repDetails
};
};
@@ -1151,6 +1155,6 @@ export default connect(
reduxForm({
form: "representationForm",
enableReinitialize: false,
destroyOnUnmount: false,
destroyOnUnmount: false
})(S78_Questionnaire)
);
@@ -0,0 +1,50 @@
import RepresentationTimetablePanel from "./RepresentationTimetablePanel";
const RepresentationFlowFormStage = ({
t,
appealIsConsultation,
currentReference,
whichControl,
appealIsLocalImpact,
renderDueDate,
renderStatementsDue,
renderMarineImpact,
renderConsultationClose,
renderFinalCommentsDue,
isQuestionnaire,
whichProgress
}) => {
return (
<>
<h1 className="govuk-heading-l">
{appealIsConsultation
? t("myrepresentations:make-a-consultation-on-label")
: t(
"myrepresentations:make-a-representation-on-label"
)}{" "}
{currentReference}{" "}
</h1>
<div id="rep-form">
<div className="govuk-grid-column-three-quarters wgForm">
{whichControl()}
</div>
<RepresentationTimetablePanel
t={t}
appealIsConsultation={appealIsConsultation}
appealIsLocalImpact={appealIsLocalImpact}
renderDueDate={renderDueDate}
renderStatementsDue={renderStatementsDue}
renderMarineImpact={renderMarineImpact}
renderConsultationClose={renderConsultationClose}
renderFinalCommentsDue={renderFinalCommentsDue}
isQuestionnaire={isQuestionnaire}
whichProgress={whichProgress}
/>
</div>
</>
);
};
export default RepresentationFlowFormStage;

Some files were not shown because too many files have changed in this diff Show More