V3-10: trip splitting
TripRepository.splitTrip mirrors mergeTrips' transaction shape: splits at a segment boundary, moves the target segment and everything after it to a new trip, recomputes both trips' aggregates from scratch. startedAt/endedAt derive from each trip's actual remaining segments, not copied from the pre-split row. Trip detail gained a split action with a segment-boundary picker and a naming confirmation; a single-segment ride explains why it can't split instead of offering a dead control.
This commit is contained in:
@@ -100,6 +100,17 @@ class TripDetailScreen extends ConsumerWidget {
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
onPressed: () => _rename(context, ref, async.value!.trip),
|
||||
),
|
||||
if (async.valueOrNull != null)
|
||||
IconButton(
|
||||
key: const Key('split'),
|
||||
icon: const Icon(Icons.content_cut),
|
||||
tooltip: async.value!.segments.length < 2
|
||||
? 'Nothing to split -- this ride has only one segment'
|
||||
: 'Split this ride',
|
||||
// V3-10: a dead control that explains itself via SnackBar on tap, rather
|
||||
// than a disabled button with no explanation at all.
|
||||
onPressed: () => _split(context, ref, async.value!),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: switch (async) {
|
||||
@@ -185,6 +196,64 @@ class TripDetailScreen extends ConsumerWidget {
|
||||
ref.invalidate(tripDetailProvider(trip.id));
|
||||
}
|
||||
|
||||
/// V3-10: splits at a segment boundary only -- see `TripRepository.splitTrip`. A
|
||||
/// single-segment ride has no boundary to offer, so this explains why rather than
|
||||
/// silently doing nothing.
|
||||
Future<void> _split(BuildContext context, WidgetRef ref, TripDetail detail) async {
|
||||
if (detail.segments.length < 2) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('This ride has only one segment -- there is nothing to split.'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final atSegmentId = await showModalBottomSheet<int>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => SafeArea(
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||
child: Text('Split before...'),
|
||||
),
|
||||
// The first segment can never be a split point -- nothing would remain
|
||||
// before it. See TripRepository.splitTrip.
|
||||
for (final segment in detail.segments.skip(1))
|
||||
ListTile(
|
||||
key: Key('split-at-${segment.id}'),
|
||||
leading: const Icon(Icons.content_cut),
|
||||
title: Text(formatDateTime(segment.startedAt)),
|
||||
onTap: () => Navigator.of(context).pop(segment.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (atSegmentId == null || !context.mounted) return;
|
||||
|
||||
final segment = detail.segments.firstWhere((s) => s.id == atSegmentId);
|
||||
final firstLabel = formatDateTime(detail.trip.startedAt);
|
||||
final secondLabel = formatDateTime(segment.startedAt);
|
||||
final confirmed = await confirmDialog(
|
||||
context,
|
||||
title: 'Split this ride?',
|
||||
message: 'This ride will become two: "$firstLabel" and "$secondLabel". '
|
||||
'Distance and other totals are recomputed for each.',
|
||||
confirmLabel: 'Split',
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
final newTripId = await ref
|
||||
.read(tripRepositoryProvider)
|
||||
.splitTrip(detail.trip.id, atSegmentId);
|
||||
ref.invalidate(tripDetailProvider(detail.trip.id));
|
||||
if (newTripId != null) ref.invalidate(tripDetailProvider(newTripId));
|
||||
}
|
||||
|
||||
/// No picker in front of Start — V3-01's constraint — but a ride's activity is fully
|
||||
/// editable here after the fact, the same pattern as rename.
|
||||
Future<void> _editActivity(BuildContext context, WidgetRef ref, Trip trip) async {
|
||||
|
||||
Reference in New Issue
Block a user