Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
9ea0bb2
add firebase_storage and file_picker deps
nuilewis Jun 24, 2026
0443e13
add hla and user role enums
nuilewis Jun 24, 2026
f9e5437
add role field to user profile
nuilewis Jun 24, 2026
7fa92bf
add hla data models
nuilewis Jun 24, 2026
eb41335
add hla firestore and storage services
nuilewis Jun 24, 2026
f0f8e61
add hla repository
nuilewis Jun 24, 2026
265af9c
add hla riverpod notifiers
nuilewis Jun 24, 2026
2addd7c
add hla reusable components
nuilewis Jun 24, 2026
a44628a
add hla patient screens
nuilewis Jun 24, 2026
487421d
add hla admin screens
nuilewis Jun 24, 2026
50b47de
add hla feature barrel
nuilewis Jun 24, 2026
b74e3bb
register hla routes
nuilewis Jun 24, 2026
19eb2c9
regenerate objectbox and provider code
nuilewis Jun 24, 2026
b8c54b0
add hla entry card to home
nuilewis Jun 24, 2026
3d2d8d8
add hla entry points to profile
nuilewis Jun 24, 2026
6e9f807
add hla security rules and indexes
nuilewis Jun 24, 2026
2c3edb0
add hla unit tests
nuilewis Jun 24, 2026
4cd11a4
scaffold hla admin web app
nuilewis Jun 24, 2026
a390f16
add admin app entry point and firebase config
nuilewis Jun 24, 2026
63ec6d7
add admin core utilities
nuilewis Jun 24, 2026
5313e23
add web-adapted hla models
nuilewis Jun 24, 2026
bdbb9be
add admin auth and firestore services
nuilewis Jun 24, 2026
c0224ea
add admin repository
nuilewis Jun 24, 2026
3f9f878
add admin riverpod providers
nuilewis Jun 24, 2026
67887b0
add admin shared widgets
nuilewis Jun 24, 2026
eefe604
add admin auth-gate screens
nuilewis Jun 24, 2026
64a55ba
add admin dashboard shell
nuilewis Jun 24, 2026
0c65700
add admin requests queue and detail
nuilewis Jun 24, 2026
e582274
add admin slots management
nuilewis Jun 24, 2026
ebdd5ff
add admin unit tests
nuilewis Jun 24, 2026
7a8ecfc
add admin setup readme
nuilewis Jun 24, 2026
95e5183
feat: add sign in with email
nuilewis Jun 25, 2026
435d105
chore [config]: properly setup firebase for web, and firebase auth
nuilewis Jun 29, 2026
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
45 changes: 45 additions & 0 deletions admin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/

# Symbolication related
app.*.symbols

# Obfuscation related
app.*.map.json

# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
30 changes: 30 additions & 0 deletions admin/.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.

version:
revision: "e1fd963c6f6922bd32afde2e9698a363cd0406d2"
channel: "stable"

project_type: app

# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
base_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
- platform: web
create_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2
base_revision: e1fd963c6f6922bd32afde2e9698a363cd0406d2

# User provided section

# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
94 changes: 94 additions & 0 deletions admin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Circle · HLA Admin (Flutter Web)

A standalone Flutter **web** dashboard for Circle staff to manage the HLA typing
feature. It is a sibling app in this monorepo (`admin/`) and talks to the **same
Firebase project** (`circle-60af7`) as the mobile app — the same Firestore
collections (`hla_test_requests`, `hla_appointment_slots`, `hla_admins`) and the
same Storage path (`hla_results/`).

It is deliberately **not** coupled to the mobile app's code: the mobile HLA
models are ObjectBox entities and the app pulls in mobile-only plugins that don't
build for web. This app re-implements the small HLA data layer in pure Dart, with
serialization that matches the mobile app exactly so the same documents round-trip.

## What staff can do

- **Requests queue** — see every request in real time, filter by status, open one.
- **Advance status** — move a request through the journey with an optional note
(appended to the patient-visible timeline).
- **Upload results** — attach a PDF/image to a request (stored in Firebase
Storage) plus an optional summary; sets status to *Results available*.
- **Appointment slots** — create slots, activate/deactivate them.

## Architecture

```
lib/
core/ enums (status/relation), theme, formatting, AdminException
models/ HlaTestRequest, HlaSampleSubject, HlaStatusEvent, HlaAppointmentSlot
services/ AuthService (Google popup), HlaAdminService (Firestore + Storage)
repositories/HlaAdminRepository (status events, result upload, slot release)
providers/ Riverpod (no codegen): auth + admin gate, requests/slots streams, mutations
screens/ app_root (auth gate) -> sign_in / not_authorized / dashboard
dashboard -> requests (master/detail) + slots
widgets/ status chip, status timeline
```

State: `flutter_riverpod` (no build_runner). Errors flow through `AsyncValue` and
surface as snackbars.

## One-time setup

The committed `lib/firebase_options.dart` has **placeholder** `apiKey`/`appId` —
a Web app must be registered on the Firebase project first.

1. **Install deps**
```bash
cd admin
flutter pub get
```

2. **Add web Firebase config** (from `admin/`):
```bash
flutterfire configure --project=circle-60af7 --platforms=web
```
This rewrites `lib/firebase_options.dart` with the real web credentials.
(Or register a Web app in the Firebase console and paste `apiKey` + `appId`.)

3. **Enable sign-in providers**: Firebase console -> Authentication -> Sign-in
method -> enable **Google** and **Email/Password** (the dashboard offers
both). Under Authentication -> Settings -> Authorized domains, add `localhost`
(for local dev) and your eventual hosting domain. Email/password admins are
created in Authentication -> Users.

4. **Deploy the security rules + indexes** (from the repo root — these are shared
with the mobile feature and live in `../firestore.rules`, `../storage.rules`):
```bash
firebase deploy --only firestore:rules,firestore:indexes,storage
```

5. **Provision admins**: the dashboard gate (and the security rules) require an
`hla_admins/{uid}` document. For each staff member, after they sign in once
(to create their Firebase Auth uid), add an empty document at
`hla_admins/{their-uid}` in Firestore.

## Run / build

```bash
cd admin
flutter run -d chrome # local dev
flutter build web # production bundle -> build/web
flutter test # unit tests
```

To host it: `firebase deploy --only hosting` after configuring a Hosting target
that serves `admin/build/web` (not yet wired in `firebase.json`).

## Notes

- The admin gate is **authoritative via `hla_admins`**, not the user's profile
`role` field — a non-admin who signs in sees an "access denied" screen and the
rules reject any write.
- Result uploads use `putData` (web has bytes, not file paths).
- This app resolves to newer Firebase/Riverpod majors than the mobile app; that's
fine since it's an independent package.
28 changes: 28 additions & 0 deletions admin/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.

# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml

linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule

# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
14 changes: 14 additions & 0 deletions admin/firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"flutter": {
"platforms": {
"dart": {
"lib/firebase_options.dart": {
"projectId": "circle-60af7",
"configurations": {
"web": "1:497391679746:web:7a3f50c4e6f710119d2a1b"
}
}
}
}
}
}
72 changes: 72 additions & 0 deletions admin/lib/core/enums.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import 'package:flutter/material.dart';

/// The stages of an HLA typing request's journey. Mirrors the mobile app's
/// `HlaTestStatus` exactly — the enum **names** are the values persisted in
/// Firestore, so they must stay in sync across both apps.
///
/// Declaration order is the canonical forward order; [cancelled] sits outside
/// the sequence.
enum HlaTestStatus {
requested('Sample requested', Icons.note_add_outlined),
bookingConfirmed('Appointment booked', Icons.event_available_outlined),
collected('Sample collected', Icons.science_outlined),
sentForTesting('Sample sent for testing', Icons.inventory_2_outlined),
inTransit('Sample in transit', Icons.local_shipping_outlined),
tested('Sample tested', Icons.bolt_outlined),
resultsProcessing('Results processing', Icons.hourglass_top_outlined),
resultsAvailable('Results available', Icons.task_outlined),
resultsSent('Results sent', Icons.check_circle_outline),
cancelled('Cancelled', Icons.cancel_outlined);

const HlaTestStatus(this.label, this.icon);

final String label;
final IconData icon;
}

extension HlaTestStatusX on HlaTestStatus {
/// Position in the forward sequence; -1 for [cancelled].
int get order =>
this == HlaTestStatus.cancelled ? -1 : HlaTestStatus.values.indexOf(this);

bool get isCancelledState => this == HlaTestStatus.cancelled;

bool get isTerminal =>
this == HlaTestStatus.cancelled || this == HlaTestStatus.resultsSent;

/// Next status in the forward sequence, or null at/after the end.
HlaTestStatus? get next {
if (isTerminal) return null;
final int nextIndex = order + 1;
// resultsSent is the last forward value; cancelled is excluded.
if (nextIndex >= HlaTestStatus.values.length - 1) {
return HlaTestStatus.resultsSent;
}
return HlaTestStatus.values[nextIndex];
}

bool get isResultsReady =>
!isCancelledState && order >= HlaTestStatus.resultsAvailable.order;
}

/// Relationship of a sample subject to the requesting patient. Names match the
/// mobile app's `HlaSubjectRelation`.
enum HlaSubjectRelation {
self('Self'),
sibling('Sibling'),
parent('Parent'),
child('Child'),
relative('Relative'),
potentialDonor('Potential donor'),
other('Other');

const HlaSubjectRelation(this.label);
final String label;

static HlaSubjectRelation fromName(String? name) {
return HlaSubjectRelation.values.firstWhere(
(r) => r.name == name,
orElse: () => HlaSubjectRelation.other,
);
}
}
13 changes: 13 additions & 0 deletions admin/lib/core/exceptions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// A simple, user-facing error surfaced by services/repositories and rendered
/// by the UI (via `AsyncValue.error`). Keeps the web app free of the mobile
/// app's fpdart `Either`/`Failure` machinery — Riverpod's `AsyncValue.guard`
/// handles the functional error flow here.
class AdminException implements Exception {
AdminException(this.message, {this.cause});

final String message;
final Object? cause;

@override
String toString() => 'AdminException: $message';
}
7 changes: 7 additions & 0 deletions admin/lib/core/format.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:intl/intl.dart';

/// Formats a (UTC) timestamp in the viewer's local time, e.g. "Jun 24, 2026 · 3:30 PM".
String formatDateTime(DateTime dt) =>
DateFormat('MMM d, y · h:mm a').format(dt.toLocal());

String formatDate(DateTime dt) => DateFormat('MMM d, y').format(dt.toLocal());
36 changes: 36 additions & 0 deletions admin/lib/core/theme.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';

/// Brand purple seed, matching the mobile app's `AppColours.purpleSeed`.
const Color kBrandSeed = Color(0xFF7B4FFF);

ThemeData buildAdminTheme() {
final ColorScheme scheme = ColorScheme.fromSeed(
seedColor: kBrandSeed,
brightness: Brightness.light,
);
return ThemeData(
useMaterial3: true,
colorScheme: scheme,
scaffoldBackgroundColor: scheme.surface,
visualDensity: VisualDensity.comfortable,
cardTheme: CardThemeData(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: scheme.outlineVariant),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
isDense: true,
),
);
}

/// Shared spacing tokens.
const double kGap4 = 4;
const double kGap8 = 8;
const double kGap12 = 12;
const double kGap16 = 16;
const double kGap24 = 24;
const double kGap32 = 32;
29 changes: 29 additions & 0 deletions admin/lib/firebase_options.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;

/// Firebase configuration for the HLA admin **web** app.
///
/// ⚠️ The `apiKey` and `appId` below are PLACEHOLDERS. A Web app must be
/// registered on the `circle-60af7` Firebase project to obtain them. The
/// quickest way is to run, from this `admin/` directory:
///
/// flutterfire configure --project=circle-60af7 --platforms=web
///
/// which regenerates this file with the real web credentials. Alternatively,
/// register a Web app in the Firebase console (Project settings → Your apps →
/// Add app → Web) and paste its `apiKey` and `appId` here.
///
/// The non-secret identifiers (projectId, messagingSenderId, storageBucket)
/// are shared with the mobile app and already filled in.

const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyBdJoJ6xP2VH2fuAS4oUtQ1qQ2xDoIn5o8',
appId: '1:497391679746:web:7a3f50c4e6f710119d2a1b',
messagingSenderId: '497391679746',
projectId: 'circle-60af7',
authDomain: 'circle-60af7.firebaseapp.com',
storageBucket: 'circle-60af7.firebasestorage.app',
measurementId: 'G-3KQHB7RSVC',
);

class DefaultFirebaseOptions {
}
Loading
Loading