glp-companion/lib/features/medication/domain/entities/injection_site.dart

130 lines
3.8 KiB
Dart

/// Anatomical regions targeted by subcutaneous GLP-1/GIP injections.
enum InjectionBodyRegion {
abdomen('Abdomen'),
thigh('Thigh'),
upperArm('Upper Arm');
const InjectionBodyRegion(this.displayName);
final String displayName;
}
/// The 12-quadrant anatomical rotation matrix used to prevent
/// lipohypertrophy and bruising at injection sites.
///
/// Quadrants are grouped into three regions of four sites each:
/// * **Abdomen (4):** Right/Left x Upper/Lower
/// * **Thighs (4):** Right/Left x Anterior/Lateral
/// * **Upper arms (4):** Right/Left x Tricep/Lateral
///
/// Enum value names double as stable persistence identifiers (stored as text
/// in the drift schema), so renaming a value is a breaking migration - add
/// new quadrants instead of editing existing ones.
enum InjectionSite {
abdomenRightUpper(
'Right Upper Abdomen',
'Right Upper',
InjectionBodyRegion.abdomen,
),
abdomenRightLower(
'Right Lower Abdomen',
'Right Lower',
InjectionBodyRegion.abdomen,
),
abdomenLeftUpper(
'Left Upper Abdomen',
'Left Upper',
InjectionBodyRegion.abdomen,
),
abdomenLeftLower(
'Left Lower Abdomen',
'Left Lower',
InjectionBodyRegion.abdomen,
),
thighRightAnterior(
'Right Anterior Thigh',
'Right Anterior',
InjectionBodyRegion.thigh,
),
thighRightLateral(
'Right Lateral Thigh',
'Right Lateral',
InjectionBodyRegion.thigh,
),
thighLeftAnterior(
'Left Anterior Thigh',
'Left Anterior',
InjectionBodyRegion.thigh,
),
thighLeftLateral(
'Left Lateral Thigh',
'Left Lateral',
InjectionBodyRegion.thigh,
),
armRightTricep('Right Tricep', 'Right Tricep', InjectionBodyRegion.upperArm),
armRightLateral(
'Right Lateral Arm',
'Right Lateral',
InjectionBodyRegion.upperArm,
),
armLeftTricep('Left Tricep', 'Left Tricep', InjectionBodyRegion.upperArm),
armLeftLateral(
'Left Lateral Arm',
'Left Lateral',
InjectionBodyRegion.upperArm,
);
const InjectionSite(this.displayName, this.shortLabel, this.region);
/// Human-readable anatomical name (e.g. `Right Upper Abdomen`).
final String displayName;
/// Abbreviated label used inside compact pickers and chips.
final String shortLabel;
/// Region this quadrant belongs to.
final InjectionBodyRegion region;
/// Stable string persisted to (and parsed from) the database.
String get persistenceKey => name;
/// Parses a persisted identifier or display name back into a quadrant.
///
/// Returns `null` for absent or unrecognized values so legacy rows with an
/// unknown site never crash the presentation layer.
static InjectionSite? tryParse(String? raw) {
if (raw == null || raw.trim().isEmpty) return null;
final normalized = raw.trim();
for (final site in values) {
if (site.name == normalized || site.displayName == normalized) {
return site;
}
}
return null;
}
/// Rotation policy: returns the least-recently-used quadrant.
///
/// [lastUsedAt] maps a quadrant to the timestamp of its most recent use;
/// quadrants missing from the map (or mapped to `null`) count as never used
/// and therefore win over any previously used site. Ties - including ties
/// between never-used quadrants - resolve to the earliest entry in the
/// enum's declaration order, which keeps suggestions deterministic.
static InjectionSite leastRecentlyUsed(
Map<InjectionSite, DateTime?> lastUsedAt,
) {
final never = DateTime.fromMillisecondsSinceEpoch(0);
var best = values.first;
var bestTime = lastUsedAt[best] ?? never;
for (final site in values.skip(1)) {
final siteTime = lastUsedAt[site] ?? never;
// Strict comparison preserves declaration order on ties.
if (siteTime.isBefore(bestTime)) {
best = site;
bestTime = siteTime;
}
}
return best;
}
}