critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

The generated .xlsx file opens without errors in Microsoft Excel (Windows), LibreOffice Calc, and Numbers (macOS)
Column structure, headers, and data values in the Excel output exactly match the CSV output from task-009 — both formats are kept in sync by sharing the BufdirReportRow data class
Column headers in row 1 are bold and have a light background fill for visual distinction
Numeric columns (hours, participant count) are formatted as numbers in Excel (not text), enabling Excel SUM/AVERAGE functions to work without manual reformatting
Date columns use Excel date format (YYYY-MM-DD or Norwegian locale equivalent) rather than raw ISO strings
The generated .xlsx is uploaded to a Supabase Storage private bucket and a signed download URL with 15-minute expiry is returned to the requesting admin
Signed URL generation uses the Supabase Storage API — no manual JWT signing in application code
The export_history table record for Excel exports includes: export_type='excel', storage_path, signed_url_expiry, file_size_bytes, row_count, admin_id, created_at
If Supabase Storage upload fails, the error is propagated to the caller with a specific `ExportStorageException`; no partial export history record is written
The export history for each admin user is queryable and displays last 50 exports, including both CSV and Excel entries sorted by created_at descending
File naming convention follows: `bufdir_export_{org_slug}_{YYYYMMDD_HHMMSS}.xlsx`

Technical Requirements

frameworks
Flutter
Dart
Riverpod
apis
Supabase Storage (upload, signed URL generation)
Dart `excel` package (v2+) for .xlsx generation
OrgHierarchyService.getSubtreeIds()
AdminExportService (CSV path from task-009)
data models
BufdirReportRow
ExportHistory
Activity
OrgNode
performance requirements
Excel file generation for 10 000 rows completes in under 45 seconds
Generated .xlsx file size for 10 000 rows does not exceed 10MB
Supabase Storage upload uses streaming upload to avoid holding the full file in memory
security requirements
Excel files are stored in a private Supabase Storage bucket with no public access policy
Signed URLs expire after exactly 15 minutes and are single-use where the storage provider supports it
Admin can only retrieve download URLs for their own export history entries — RLS on export_history table enforces this
File names do not include user IDs or PII — only org slug and timestamp

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Share `BufdirReportRow` between both the CSV serializer (task-009) and Excel generator — this is the single source of truth for column order and data mapping. Define an `ExportFormat` enum (`csv`, `excel`) and route through a single `AdminExportService.generateExport(format, orgNodeId, dateRange)` method that delegates to either serializer, keeping the data querying logic non-duplicated. For the Dart `excel` package: create one sheet named 'Bufdir Report', set column widths proportional to content length, apply `CellStyle(bold: true, backgroundColorHex: 'FFE3E8F0')` to header cells. Use `Excel.createExcel()` and encode with `.encode()` to get bytes, then pass bytes directly to Supabase Storage's upload stream — do not write to a temporary file on disk in production.

For the storage path, use `exports/{admin_id}/{filename}` to make RLS policies straightforward. After upload, call `createSignedUrl` with 900-second expiry and include the URL in both the API response and the export_history record.

Testing Requirements

Unit tests: mock the Dart `excel` package interface and Supabase Storage client; assert that the Excel sheet is created with correct column headers in row 1 with bold styling; assert that numeric and date columns use the correct Excel cell types (not string); assert that storage upload is called with the correct bucket name and file path pattern; assert that a signed URL is requested with 900-second (15-minute) expiry. Integration tests: generate a real .xlsx file from 100 seeded activity rows, parse it back with the `excel` package, and assert column values match the original data. Upload to Supabase local Storage and assert a signed URL is returned. Cross-format consistency test: generate both CSV and Excel for identical input data and assert that all row values are equivalent after parsing.

Error path test: mock a Storage upload failure and assert ExportStorageException is thrown and no export_history record is written.

Component
Admin Export Service
service medium
Epic Risks (4)
medium impact high prob technical

OrgHierarchyNavigator rendering NHF's full 1,400-chapter tree in a single widget may cause Flutter frame-rate drops below 60 fps on mid-range devices, making the navigator unusable for NHF national admins.

Mitigation & Contingency

Mitigation: Implement lazy expansion: only load immediate children on node expand rather than the full tree upfront. Use virtual scrolling for long sibling lists. Test with a synthetic 1,400-node dataset on a low-end Android device during development.

Contingency: If lazy expansion is insufficient, replace the tree widget with a paginated drill-down navigator (select level → select child) that avoids rendering more than 50 nodes at a time.

medium impact medium prob dependency

Bufdir may update their required export column structure or file format during or after development. If the AdminExportService hardcodes the current Bufdir schema, any format change requires a code release rather than a config update.

Mitigation & Contingency

Mitigation: Drive the Bufdir column mapping from a configuration repository rather than hardcoded constants. Abstract column definitions into a named schema config so that format changes require only a config update and re-deployment without service logic changes.

Contingency: If Bufdir format changes post-launch, release a config update within one sprint. If the change is structural (new required sections), scope a targeted service update and communicate timeline to partner organisations.

high impact medium prob integration

Role transition side-effects in UserManagementService (e.g., certification expiry removing mentor from chapter listing, pause triggering coordinator notification) may interact with external services like HLF's website sync. Incomplete side-effect handling could leave the system in an inconsistent state.

Mitigation & Contingency

Mitigation: Model side-effects as explicit domain events published after the primary state change is persisted. Implement event handlers as idempotent operations so re-processing is safe. Write integration tests that assert all side-effects fire correctly for each role transition type.

Contingency: If a side-effect fails after the primary change is persisted, log the failure with full context and trigger a manual reconciliation alert to the on-call team. Provide an admin-accessible re-trigger action for failed side-effects.

medium impact medium prob scope

If AdminStatisticsService cache TTL is set too long, org_admin may see significantly stale KPI values (e.g., a mentor newly paused an hour ago still appears as active), undermining trust in the dashboard.

Mitigation & Contingency

Mitigation: Default cache TTL to 5 minutes with a manual refresh action on the dashboard. Implement cache invalidation triggered by UserManagementService write operations that affect counted entities.

Contingency: If staleness causes org admin complaints post-launch, reduce TTL to 60 seconds and introduce a real-time Supabase subscription for high-impact counters (paused mentors, expiring certifications).