Skip to content

Commit b5ce8b9

Browse files
authored
Add reserve pressure user input to dive planner (#67)
* UI tests for editable reserve pressure field (TDD red) * Implement requirements for UX of user input of reserve pressure in dive planning * Add tests for plan calculator to ensure that user entry of reserve is respected in the calculations * Update dive planner to respect reserve pressure entered by user (TDD green phase) * dart format . corrections * fix: use ref.read for pressureUnitProvider to prevent plan state reset ref.watch inside divePlanNotifierProvider caused the entire notifier to be recreated when the user changed their pressure unit in settings, destroying all in-progress plan segments, tanks, and edits. Changed to ref.read since the pressure unit is only needed to compute the initial default reserve pressure value, not to reactively track changes. Fixes CODE_REVIEW.md issue #1 (HIGH). * feat: validate reserve pressure input with localized errors * fix: wrap reserve pressure input in Expanded to prevent row overflow on narrow screens * feat: reset to default reserve pressure when field cleared, with localized info message * fix: add Semantics label to reserve pressure TextField for screen readers * fix: display exact user-entered reserve pressure in warning messages * refactor: extract magic number 50 to DivePlanState.kDefaultReservePressureBar constant * style: dart format corrections * fix: use max tank pressure instead of sum for reserve validation to match calculator logic * Add pull request description for editable reserve pressure feature * fix: enforce digits-only input on reserve pressure field * Remove PUSH_TEMPLATE.md * Add tests for missing coverage flagged by codecov bot
1 parent 7dd57eb commit b5ce8b9

30 files changed

Lines changed: 1086 additions & 23 deletions

lib/features/dive_planner/data/services/plan_calculator_service.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,13 @@ class PlanCalculatorService {
5555
/// [segments] - The dive plan segments in order.
5656
/// [tanks] - Available tanks with starting pressures and volumes.
5757
/// [sacRate] - Surface Air Consumption in L/min.
58+
/// [reservePressure] - Reserve pressure threshold in bar.
5859
/// [initialTissueState] - Tissue state from previous dive (for repetitive).
5960
PlanResult calculatePlan({
6061
required List<PlanSegment> segments,
6162
required List<DiveTank> tanks,
6263
required double sacRate,
64+
double reservePressure = DivePlanState.kDefaultReservePressureBar,
6365
List<TissueCompartment>? initialTissueState,
6466
}) {
6567
if (segments.isEmpty) {
@@ -301,15 +303,16 @@ class PlanCalculatorService {
301303
value: remainingPressure.toDouble(),
302304
),
303305
);
304-
} else if (remainingPressure != null && remainingPressure < 50) {
306+
} else if (remainingPressure != null &&
307+
remainingPressure < reservePressure) {
305308
warnings.add(
306309
PlanWarning(
307310
type: PlanWarningType.gasLow,
308311
severity: PlanWarningSeverity.alert,
309312
message:
310-
'Tank ${tank.name ?? tank.gasMix.name} below 50 bar reserve',
313+
'Tank ${tank.name ?? tank.gasMix.name} below ${reservePressure.toStringAsFixed(0)} bar reserve',
311314
value: remainingPressure.toDouble(),
312-
threshold: 50,
315+
threshold: reservePressure,
313316
),
314317
);
315318
}
@@ -325,7 +328,8 @@ class PlanCalculatorService {
325328
remainingPressure: remainingPressure,
326329
percentUsed: percentUsed,
327330
reserveViolation:
328-
remainingPressure != null && remainingPressure < 50,
331+
remainingPressure != null &&
332+
remainingPressure < reservePressure,
329333
),
330334
);
331335
}

lib/features/dive_planner/domain/entities/plan_result.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,9 @@ class PlanResult extends Equatable {
461461

462462
/// State of a dive plan being edited.
463463
class DivePlanState extends Equatable {
464+
/// Default reserve pressure in bar.
465+
static const double kDefaultReservePressureBar = 50;
466+
464467
/// Unique ID for this plan.
465468
final String id;
466469

@@ -494,6 +497,9 @@ class DivePlanState extends Equatable {
494497
/// Altitude above sea level in meters (for altitude diving).
495498
final double? altitude;
496499

500+
/// Reserve pressure in bar.
501+
final double reservePressure;
502+
497503
/// Notes for the plan.
498504
final String notes;
499505

@@ -518,6 +524,7 @@ class DivePlanState extends Equatable {
518524
this.initialTissueState,
519525
this.siteId,
520526
this.altitude,
527+
this.reservePressure = kDefaultReservePressureBar,
521528
this.notes = '',
522529
this.isDirty = false,
523530
required this.createdAt,
@@ -564,6 +571,7 @@ class DivePlanState extends Equatable {
564571
List<TissueCompartment>? initialTissueState,
565572
String? siteId,
566573
double? altitude,
574+
double? reservePressure,
567575
String? notes,
568576
bool? isDirty,
569577
DateTime? createdAt,
@@ -589,6 +597,7 @@ class DivePlanState extends Equatable {
589597
: (initialTissueState ?? this.initialTissueState),
590598
siteId: clearSiteId ? null : (siteId ?? this.siteId),
591599
altitude: clearAltitude ? null : (altitude ?? this.altitude),
600+
reservePressure: reservePressure ?? this.reservePressure,
592601
notes: notes ?? this.notes,
593602
isDirty: isDirty ?? this.isDirty,
594603
createdAt: createdAt ?? this.createdAt,
@@ -609,6 +618,7 @@ class DivePlanState extends Equatable {
609618
initialTissueState,
610619
siteId,
611620
altitude,
621+
reservePressure,
612622
notes,
613623
isDirty,
614624
createdAt,

lib/features/dive_planner/presentation/providers/dive_planner_providers.dart

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:uuid/uuid.dart';
22

33
import 'package:submersion/core/constants/enums.dart';
4+
import 'package:submersion/core/constants/units.dart';
45
import 'package:submersion/core/providers/provider.dart';
56
import 'package:submersion/features/dive_log/domain/entities/dive.dart';
67
import 'package:submersion/features/settings/presentation/providers/settings_providers.dart';
@@ -38,16 +39,24 @@ final planCalculatorServiceProvider = Provider<PlanCalculatorService>((ref) {
3839
/// StateNotifier for managing dive plan editing state.
3940
class DivePlanNotifier extends StateNotifier<DivePlanState> {
4041
final PlanCalculatorService _calculator;
42+
final double _defaultReservePressure;
4143

42-
DivePlanNotifier(this._calculator) : super(_createInitialState());
44+
DivePlanNotifier(
45+
this._calculator, {
46+
double reservePressure = DivePlanState.kDefaultReservePressureBar,
47+
}) : _defaultReservePressure = reservePressure,
48+
super(_createInitialState(reservePressure: reservePressure));
4349

44-
static DivePlanState _createInitialState() {
50+
static DivePlanState _createInitialState({
51+
double reservePressure = DivePlanState.kDefaultReservePressureBar,
52+
}) {
4553
final now = DateTime.now();
4654
return DivePlanState(
4755
id: _uuid.v4(),
4856
name: 'New Dive Plan',
4957
segments: [],
5058
tanks: [_createDefaultTank()],
59+
reservePressure: reservePressure,
5160
createdAt: now,
5261
updatedAt: now,
5362
);
@@ -72,7 +81,7 @@ class DivePlanNotifier extends StateNotifier<DivePlanState> {
7281

7382
/// Reset to a new empty plan.
7483
void newPlan() {
75-
state = _createInitialState();
84+
state = _createInitialState(reservePressure: _defaultReservePressure);
7685
}
7786

7887
/// Load an existing plan for editing.
@@ -282,6 +291,15 @@ class DivePlanNotifier extends StateNotifier<DivePlanState> {
282291
);
283292
}
284293

294+
/// Update reserve pressure in bar.
295+
void updateReservePressure(double reservePressure) {
296+
state = state.copyWith(
297+
reservePressure: reservePressure,
298+
isDirty: true,
299+
updatedAt: DateTime.now(),
300+
);
301+
}
302+
285303
// --------------------------------------------------------------------------
286304
// Repetitive Dive Support
287305
// --------------------------------------------------------------------------
@@ -365,7 +383,12 @@ class DivePlanNotifier extends StateNotifier<DivePlanState> {
365383
final divePlanNotifierProvider =
366384
StateNotifierProvider<DivePlanNotifier, DivePlanState>((ref) {
367385
final calculator = ref.watch(planCalculatorServiceProvider);
368-
return DivePlanNotifier(calculator);
386+
final pressureUnit = ref.read(pressureUnitProvider);
387+
// Default reserve: 50 bar for metric, 500 psi (~34.47 bar) for imperial
388+
final reservePressure = pressureUnit == PressureUnit.psi
389+
? PressureUnit.psi.convert(500, PressureUnit.bar)
390+
: DivePlanState.kDefaultReservePressureBar;
391+
return DivePlanNotifier(calculator, reservePressure: reservePressure);
369392
});
370393

371394
// ============================================================================
@@ -387,6 +410,7 @@ final planResultsProvider = Provider<PlanResult>((ref) {
387410
segments: state.segments,
388411
tanks: state.tanks,
389412
sacRate: state.sacRate,
413+
reservePressure: state.reservePressure,
390414
initialTissueState: state.initialTissueState,
391415
);
392416
});

lib/features/dive_planner/presentation/widgets/gas_results_panel.dart

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,13 @@ class GasResultsPanel extends ConsumerWidget {
6464
)
6565
else
6666
...results.gasConsumptions.map(
67-
(consumption) =>
68-
_GasConsumptionCard(consumption: consumption, units: units),
67+
(consumption) => _GasConsumptionCard(
68+
consumption: consumption,
69+
reservePressure: ref
70+
.watch(divePlanNotifierProvider)
71+
.reservePressure,
72+
units: units,
73+
),
6974
),
7075
],
7176
),
@@ -76,9 +81,14 @@ class GasResultsPanel extends ConsumerWidget {
7681

7782
class _GasConsumptionCard extends StatelessWidget {
7883
final GasConsumption consumption;
84+
final double reservePressure;
7985
final UnitFormatter units;
8086

81-
const _GasConsumptionCard({required this.consumption, required this.units});
87+
const _GasConsumptionCard({
88+
required this.consumption,
89+
required this.reservePressure,
90+
required this.units,
91+
});
8292

8393
@override
8494
Widget build(BuildContext context) {
@@ -239,9 +249,7 @@ class _GasConsumptionCard extends StatelessWidget {
239249
const SizedBox(width: 4),
240250
Text(
241251
context.l10n.divePlanner_warning_belowMinReserve(
242-
units.formatPressure(
243-
consumption.minGasReserve?.toDouble(),
244-
),
252+
units.formatPressure(reservePressure),
245253
),
246254
style: theme.textTheme.bodySmall?.copyWith(
247255
color: theme.colorScheme.error,

0 commit comments

Comments
 (0)