130 lines
3.8 KiB
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;
|
|
}
|
|
} |