Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/assets/css/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@use "@hmcts/web-core/src/assets/css/button-as-link.scss";
@use "@hmcts/admin-pages/src/assets/css/index.scss" as admin;
@use "@hmcts/system-admin-pages/src/assets/css/dashboard.scss";
@use "@hmcts/verified-pages/src/assets/css/index.scss" as verified;
@use "@hmcts/verified-pages/src/assets/css/verified-pages.scss" as verified;

.govuk-service-navigation__navigation-end {
margin-left: auto !important;
Expand Down
13 changes: 12 additions & 1 deletion apps/web/vite.build.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import { assets as adminPagesAssets } from "@hmcts/admin-pages/config";
import { assets as systemAdminAssets } from "@hmcts/system-admin-pages/config";
import { assets as verifiedPagesAssets } from "@hmcts/verified-pages/config";
import { createBaseViteConfig } from "@hmcts/web-core";
import { assets as webCoreAssets } from "@hmcts/web-core/config";
import { defineConfig, mergeConfig } from "vite";
Expand All @@ -9,10 +11,19 @@ import { viteStaticCopy } from "vite-plugin-static-copy";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const baseConfig = createBaseViteConfig([path.join(__dirname, "src", "assets"), webCoreAssets, systemAdminAssets]);
const baseConfig = createBaseViteConfig([path.join(__dirname, "src", "assets"), webCoreAssets, systemAdminAssets, verifiedPagesAssets, adminPagesAssets]);

export default defineConfig(
mergeConfig(baseConfig, {
build: {
rollupOptions: {
input: {
...baseConfig.build?.rollupOptions?.input,
web_css: path.join(__dirname, "src/assets/css/index.scss"),
web_js: path.join(__dirname, "src/assets/js/index.ts")
}
}
},
plugins: [
viteStaticCopy({
targets: [
Expand Down
312 changes: 312 additions & 0 deletions docs/tickets/VIBE-306/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
# VIBE-306: Technical Implementation Plan

## Overview
This ticket implements bulk unsubscribe functionality for verified media users, allowing them to select multiple subscriptions and remove them in a single operation. The implementation includes a 3-page workflow with tabbed views, checkbox selection, confirmation, and success messaging.

## Summary
Verified media users can efficiently manage their subscriptions by selecting multiple subscriptions across different categories (all, by case, by court/tribunal) and removing them in bulk. The feature includes tab-based filtering, select-all functionality, confirmation workflow, and prevents accidental deletion through a two-step confirmation process.

## Architecture

### Database Requirements
- Use existing subscription tables (location subscriptions, case subscriptions, list type subscriptions)
- Delete operations must be transactional to ensure all selected subscriptions are removed atomically
- Soft delete vs hard delete consideration (recommendation: hard delete for immediate effect)

### Subscription Types
1. **Subscriptions by case** - case name, party name, reference number subscriptions
2. **Subscriptions by court or tribunal** - location-based subscriptions
3. **All subscriptions** - combined view of both types

## Module Structure

Extend existing subscription module or create: `libs/bulk-unsubscribe`

```
libs/bulk-unsubscribe/
├── package.json
├── tsconfig.json
└── src/
├── index.ts # Business logic exports
├── config.ts # Module configuration
├── pages/
│ ├── bulk-unsubscribe.ts
│ ├── bulk-unsubscribe.njk
│ ├── confirm-bulk-unsubscribe.ts
│ ├── confirm-bulk-unsubscribe.njk
│ ├── bulk-unsubscribe-success.ts
│ └── bulk-unsubscribe-success.njk
├── services/
│ └── bulk-unsubscribe-service.ts
├── assets/
│ └── js/
│ └── select-all.ts # Client-side select all logic
└── locales/
├── en.ts
└── cy.ts
```

## Implementation Tasks

### 1. Database Service

**bulk-unsubscribe-service.ts:**
- `getAllSubscriptionsByUserId(userId)` - Get all subscriptions for display
- `getCaseSubscriptionsByUserId(userId)` - Get case subscriptions only
- `getCourtSubscriptionsByUserId(userId)` - Get court/tribunal subscriptions only
- `deleteSubscriptionsByIds(subscriptionIds, userId)` - Bulk delete with transaction
- `validateSubscriptionOwnership(subscriptionIds, userId)` - Security check
- `getSubscriptionDetailsForConfirmation(subscriptionIds)` - Get details for confirmation page

### 2. Page Controllers and Templates

**bulk-unsubscribe (Page 1):**
- GET:
- Query user's subscriptions based on selected tab (default: All)
- Query parameter: `view` (all|case|court)
- Display tabbed interface
- Render tables with checkboxes
- Show empty state if no subscriptions in selected view
- POST:
- Validate at least one subscription selected
- Store selected subscription IDs in session
- Redirect to confirm-bulk-unsubscribe
- Tab switching:
- Use query parameters to switch views
- Preserve selections across tab switches (store in session)
- Select all functionality:
- Client-side JavaScript to check/uncheck all boxes
- Header checkbox controls all row checkboxes

**confirm-bulk-unsubscribe (Page 2):**
- GET:
- Retrieve selected subscription IDs from session
- Query subscription details for display
- Render confirmation table
- Show Yes/No radio buttons
- POST:
- Validate radio selection (Yes or No)
- If No: Redirect to your-email-subscriptions
- If Yes: Delete subscriptions using bulk-unsubscribe-service
- Clear session data
- Redirect to bulk-unsubscribe-success
- Transaction handling:
- Use database transaction to ensure all-or-nothing deletion
- Log deletion for audit purposes

**bulk-unsubscribe-success (Page 3):**
- GET: Display success banner
- Show navigation links:
- Add a new email subscription
- Manage your current email subscriptions
- Find a court or tribunal
- Clear session data if not already cleared
- POST/Redirect/GET pattern to prevent duplicate deletions

### 3. Tab Implementation

**Tabbed Views:**
- Implement GOV.UK Tabs component
- Three tabs:
1. All subscriptions - Combined view
2. Subscriptions by case - Case subscriptions only
3. Subscriptions by court or tribunal - Location subscriptions only
- Tab state managed via query parameter: `/bulk-unsubscribe?view=case`
- Preserve selections when switching tabs (store in session)

**Empty States:**
- If no subscriptions in selected tab, show empty state message
- Hide table when empty
- Message: "You do not have any subscriptions in this category."

### 4. Table Structures

**Subscriptions by case table:**
- Columns: Select (checkbox), Case name, Party name, Reference number, Date added
- Each row has unique checkbox with subscription ID as value

**Subscriptions by court or tribunal table:**
- Columns: Select (checkbox), Court or tribunal name, Date added
- Each row has unique checkbox with subscription ID as value

**All subscriptions view:**
- Display both tables sequentially
- Case subscriptions table first
- Court/tribunal subscriptions table second
- Separate "Select all" for each table

### 5. Select All Functionality

**Client-side JavaScript (select-all.ts):**
```typescript
// Pseudocode
function initSelectAll() {
const selectAllCheckboxes = document.querySelectorAll('.select-all-checkbox');

selectAllCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const tableId = e.target.dataset.table;
const rowCheckboxes = document.querySelectorAll(`#${tableId} .row-checkbox`);

rowCheckboxes.forEach(row => {
row.checked = e.target.checked;
});
});
});

// Update select-all if all rows manually checked
updateSelectAllState();
}
```

### 6. Session Management
- Store selected subscription IDs across pages
- Session data structure:
```typescript
{
bulk_unsubscribe: {
selected_ids: [123, 456, 789],
view: 'all' | 'case' | 'court'
}
}
```
- Clear session after successful deletion or cancellation

### 7. Validation

**Page 1 Validation:**
- At least one checkbox must be selected
- Error: "At least one subscription must be selected"
- Display GOV.UK error summary with anchor link

**Page 2 Validation:**
- One radio must be selected (Yes or No)
- Error: "An option must be selected."
- Display GOV.UK error summary

### 8. Security Considerations
- Validate subscription ownership before deletion
- Use CSRF tokens on POST requests
- Ensure authenticated user owns all selected subscriptions
- Use database transactions for atomic deletions
- Log all bulk deletions for audit trail

### 9. Locales
Create en.ts and cy.ts with content for:
- Page titles
- Tab labels
- Table column headings
- Button labels
- Radio options
- Empty state messages
- Error messages
- Success banner text
- Navigation links

### 10. Accessibility Implementation
- Ensure all tabs support keyboard navigation
- ARIA roles for tabs: `role="tablist"`, `role="tab"`, `role="tabpanel"`
- Checkboxes must have associated labels
- Select all checkbox clearly labeled
- Error summaries with anchor links to fields
- Success banner with `role="status"` or `role="alert"`
- Tables use semantic markup with proper header scope
- Screen reader announcements for tab changes
- Visible focus indicators on all interactive elements

### 11. Styling
- Use GOV.UK Design System components:
- Tabs
- Checkboxes
- Radios
- Tables
- Button (green)
- Error summary
- Success banner
- Responsive design for mobile/tablet
- Ensure checkbox alignment in tables
- Visual indication for selected rows (optional)

### 12. Integration
- Link from "Your email subscriptions" page
- Add "Bulk unsubscribe" button/link next to "Add email subscription"
- Ensure authentication middleware protects all pages
- Add authorization check for verified media user role
- Register module in apps/web/src/app.ts

### 13. Testing

**Unit Tests (Vitest):**
- bulk-unsubscribe-service.test.ts
- Get all subscriptions
- Get case subscriptions
- Get court subscriptions
- Delete subscriptions in transaction
- Validate subscription ownership
- Handle errors gracefully
- Verify audit logging

**E2E Tests (Playwright):**
- Create single journey test: "Verified user can bulk unsubscribe @nightly"
- Navigate from email subscriptions to bulk unsubscribe
- Test "All subscriptions" tab view
- Test "Subscriptions by case" tab view
- Test "Subscriptions by court or tribunal" tab view
- Test validation error when no checkbox selected
- Select multiple subscriptions
- Test select all functionality
- Test tab switching preserves selections
- Proceed to confirmation page
- Verify selected subscriptions displayed
- Test validation error when no radio selected
- Test "No" returns to email subscriptions
- Select "Yes" and confirm deletion
- Verify success page
- Verify subscriptions deleted from database
- Test empty state when no subscriptions
- Test back navigation
- Test Welsh translation at key points
- Test accessibility inline
- Test keyboard navigation

### 14. Documentation
- Update README if needed
- Document bulk unsubscribe workflow
- Add comments for transaction logic
- Document audit logging

## Dependencies
- @hmcts/postgres - Database access via Prisma
- @hmcts/auth - Authentication/authorization
- GOV.UK Frontend - UI components (Tabs, Tables, Checkboxes, Radios)
- express-session - Session management
- VIBE-300 - Subscription by case name and case reference (pre-requisite)

## Migration Requirements
- No new database tables required
- Use existing subscription tables
- Ensure cascading deletes configured correctly (if using foreign keys)

## Risk Considerations
- Accidental bulk deletion mitigated by two-step confirmation
- Transaction failure handling critical for data integrity
- Session timeout could lose selections (inform user to complete quickly)
- Large number of subscriptions may cause performance issues (pagination consideration)
- Concurrent deletion attempts need proper locking
- Audit trail essential for compliance and user support

## Definition of Done
- All 3 pages implemented with Welsh translations
- Tabbed interface functional with correct filtering
- Select all functionality working
- Bulk deletion service with transaction support
- Validation working on both pages
- Empty state handling correct
- Session management functional
- Audit logging implemented
- All pages meet WCAG 2.2 AA standards
- E2E journey test passes (including Welsh and accessibility)
- Unit tests achieve >80% coverage on service
- Code reviewed and approved
- Integration with email subscriptions page complete
- Security validation in place
Loading
Loading