add transfer all button for admins
All checks were successful
Release / meta (push) Successful in 6s
Release / linux-build (push) Successful in 1m4s
Release / web-build (push) Successful in 2m47s
Release / android-build (push) Successful in 11m45s
Release / release-master (push) Successful in 26s
Release / release-dev (push) Successful in 30s
All checks were successful
Release / meta (push) Successful in 6s
Release / linux-build (push) Successful in 1m4s
Release / web-build (push) Successful in 2m47s
Release / android-build (push) Successful in 11m45s
Release / release-master (push) Successful in 26s
Release / release-dev (push) Successful in 30s
This commit is contained in:
@@ -130,11 +130,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
int decimals = 1,
|
||||
bool includeUnit = true,
|
||||
}) {
|
||||
return units.format(
|
||||
miles,
|
||||
decimals: decimals,
|
||||
includeUnit: includeUnit,
|
||||
);
|
||||
return units.format(miles, decimals: decimals, includeUnit: includeUnit);
|
||||
}
|
||||
|
||||
String _manualMileageLabel(DistanceUnit unit) {
|
||||
@@ -191,15 +187,13 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
}
|
||||
|
||||
double _milesFromInputWithUnit(DistanceUnit unit) {
|
||||
return DistanceFormatter(unit)
|
||||
.parseInputMiles(_mileageController.text.trim()) ??
|
||||
return DistanceFormatter(
|
||||
unit,
|
||||
).parseInputMiles(_mileageController.text.trim()) ??
|
||||
0;
|
||||
}
|
||||
|
||||
List<UserSummary> _friendsFromFriendships(
|
||||
DataService data,
|
||||
String? selfId,
|
||||
) {
|
||||
List<UserSummary> _friendsFromFriendships(DataService data, String? selfId) {
|
||||
final friends = <UserSummary>[];
|
||||
for (final f in data.friendships) {
|
||||
final other = _friendFromFriendship(f, selfId);
|
||||
@@ -300,9 +294,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
if (!mounted) return;
|
||||
final baseFriends = _friendsFromFriendships(data, auth.userId);
|
||||
final initialSelectedIds = {..._shareUserIds};
|
||||
final initialSelectedUsers = {
|
||||
for (final u in _shareUsers) u.userId: u,
|
||||
};
|
||||
final initialSelectedUsers = {for (final u in _shareUsers) u.userId: u};
|
||||
|
||||
final result = await showModalBottomSheet<List<UserSummary>>(
|
||||
context: context,
|
||||
@@ -334,8 +326,10 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
searchError = null;
|
||||
});
|
||||
try {
|
||||
final results =
|
||||
await data.searchUsers(trimmed, friendsOnly: true);
|
||||
final results = await data.searchUsers(
|
||||
trimmed,
|
||||
friendsOnly: true,
|
||||
);
|
||||
setModalState(() {
|
||||
searchResults = results;
|
||||
});
|
||||
@@ -414,8 +408,9 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
itemCount: list.length,
|
||||
itemBuilder: (_, index) {
|
||||
final user = list[index];
|
||||
final isSelected =
|
||||
selectedIds.contains(user.userId);
|
||||
final isSelected = selectedIds.contains(
|
||||
user.userId,
|
||||
);
|
||||
return CheckboxListTile(
|
||||
value: isSelected,
|
||||
title: Text(user.displayName),
|
||||
@@ -481,8 +476,9 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
if (previousUnit == currentUnit) return;
|
||||
|
||||
final miles = _milesFromInputWithUnit(previousUnit);
|
||||
final nextText = DistanceFormatter(currentUnit)
|
||||
.format(miles, decimals: 2, includeUnit: false);
|
||||
final nextText = DistanceFormatter(
|
||||
currentUnit,
|
||||
).format(miles, decimals: 2, includeUnit: false);
|
||||
_mileageController.text = nextText;
|
||||
_lastDistanceUnit = currentUnit;
|
||||
}
|
||||
@@ -842,8 +838,9 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
final destination = json['leg_destination'] as String? ?? '';
|
||||
final hasEndTime = endTime != null || endDelay != 0;
|
||||
final originTime = DateTime.tryParse(json['leg_origin_time'] ?? '');
|
||||
final destinationTime =
|
||||
DateTime.tryParse(json['leg_destination_time'] ?? '');
|
||||
final destinationTime = DateTime.tryParse(
|
||||
json['leg_destination_time'] ?? '',
|
||||
);
|
||||
final hasOriginTime = originTime != null;
|
||||
final hasDestinationTime = destinationTime != null;
|
||||
|
||||
@@ -860,8 +857,9 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
_selectedOriginDate = originTime ?? beginTime;
|
||||
_selectedOriginTime = TimeOfDay.fromDateTime(originTime ?? beginTime);
|
||||
_selectedDestinationDate = destinationTime ?? endTime ?? beginTime;
|
||||
_selectedDestinationTime =
|
||||
TimeOfDay.fromDateTime(destinationTime ?? endTime ?? beginTime);
|
||||
_selectedDestinationTime = TimeOfDay.fromDateTime(
|
||||
destinationTime ?? endTime ?? beginTime,
|
||||
);
|
||||
_hasOriginTime = hasOriginTime;
|
||||
_hasDestinationTime = hasDestinationTime;
|
||||
_useManualMileage = useManual;
|
||||
@@ -927,18 +925,20 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
);
|
||||
final tractionItems = _buildTractionFromApi(
|
||||
entry.locos
|
||||
.map((l) => {
|
||||
"loco_id": l.id,
|
||||
"type": l.type,
|
||||
"number": l.number,
|
||||
"class": l.locoClass,
|
||||
"name": l.name,
|
||||
"operator": l.operator,
|
||||
"notes": l.notes,
|
||||
"evn": l.evn,
|
||||
"alloc_pos": l.allocPos,
|
||||
"alloc_powering": l.powering ? 1 : 0,
|
||||
})
|
||||
.map(
|
||||
(l) => {
|
||||
"loco_id": l.id,
|
||||
"type": l.type,
|
||||
"number": l.number,
|
||||
"class": l.locoClass,
|
||||
"name": l.name,
|
||||
"operator": l.operator,
|
||||
"notes": l.notes,
|
||||
"evn": l.evn,
|
||||
"alloc_pos": l.allocPos,
|
||||
"alloc_powering": l.powering ? 1 : 0,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
final beginDelay = entry.beginDelayMinutes ?? 0;
|
||||
@@ -963,8 +963,9 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
_selectedOriginDate = originTime ?? beginTime;
|
||||
_selectedOriginTime = TimeOfDay.fromDateTime(originTime ?? beginTime);
|
||||
_selectedDestinationDate = destinationTime ?? endTime ?? beginTime;
|
||||
_selectedDestinationTime =
|
||||
TimeOfDay.fromDateTime(destinationTime ?? endTime ?? beginTime);
|
||||
_selectedDestinationTime = TimeOfDay.fromDateTime(
|
||||
destinationTime ?? endTime ?? beginTime,
|
||||
);
|
||||
_hasOriginTime = hasOriginTime;
|
||||
_hasDestinationTime = hasDestinationTime;
|
||||
_useManualMileage = useManual;
|
||||
@@ -980,12 +981,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
_endDelayController.text = endDelay.toString();
|
||||
_mileageController.text = mileageVal == 0
|
||||
? ''
|
||||
: _formatDistance(
|
||||
units,
|
||||
mileageVal,
|
||||
decimals: 2,
|
||||
includeUnit: false,
|
||||
);
|
||||
: _formatDistance(units, mileageVal, decimals: 2, includeUnit: false);
|
||||
_tractionItems
|
||||
..clear()
|
||||
..addAll(tractionItems);
|
||||
@@ -1187,10 +1183,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
),
|
||||
],
|
||||
if (!matchValue) ...[
|
||||
_stationField(
|
||||
label: label,
|
||||
controller: controller,
|
||||
),
|
||||
_stationField(label: label, controller: controller),
|
||||
CheckboxListTile(
|
||||
value: hasTime,
|
||||
onChanged: onTimeChanged,
|
||||
@@ -1237,8 +1230,9 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
minimumSize: const Size(0, 36),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
onPressed:
|
||||
_isEditing || _activeLegShare != null ? null : _openDrafts,
|
||||
onPressed: _isEditing || _activeLegShare != null
|
||||
? null
|
||||
: _openDrafts,
|
||||
icon: const Icon(Icons.list_alt, size: 16),
|
||||
label: const Text('Drafts'),
|
||||
),
|
||||
@@ -1246,26 +1240,27 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: const Size(0, 36),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
onPressed: _isEditing ||
|
||||
_savingDraft ||
|
||||
_submitting ||
|
||||
_activeLegShare != null
|
||||
? null
|
||||
: _saveDraftManually,
|
||||
icon: _savingDraft
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
minimumSize: const Size(0, 36),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
onPressed:
|
||||
_isEditing ||
|
||||
_savingDraft ||
|
||||
_submitting ||
|
||||
_activeLegShare != null
|
||||
? null
|
||||
: _saveDraftManually,
|
||||
icon: _savingDraft
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.save_alt, size: 16),
|
||||
label: Text(_savingDraft ? 'Saving...' : 'Save to drafts'),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: const Size(0, 36),
|
||||
@@ -1276,17 +1271,17 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
: () => _resetFormState(clearDraft: true),
|
||||
icon: const Icon(Icons.clear, size: 16),
|
||||
label: const Text('Clear form'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_activeLegShare != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildSharedBanner(),
|
||||
],
|
||||
if (_activeLegShare != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildSharedBanner(),
|
||||
],
|
||||
_buildTripSelector(context),
|
||||
const SizedBox(height: 8),
|
||||
if (_activeLegShare == null) _buildShareSection(context),
|
||||
_dateTimeGroup(
|
||||
_dateTimeGroup(
|
||||
context,
|
||||
title: 'Departure time',
|
||||
onDateTap: _pickDate,
|
||||
@@ -1393,8 +1388,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
onTimeChanged: _submitting ? null : _toggleDestinationTime,
|
||||
matchLabel: 'Match entry end',
|
||||
matchValue: _matchDestinationToEntry,
|
||||
onMatchChanged:
|
||||
_submitting ? null : _toggleMatchDestination,
|
||||
onMatchChanged: _submitting ? null : _toggleMatchDestination,
|
||||
pickerBuilder: () => _dateTimeGroupSimple(
|
||||
context,
|
||||
title: 'Destination arrival',
|
||||
@@ -1428,7 +1422,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
final tractionPanel = _section('Traction', [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ElevatedButton.icon(
|
||||
child: FilledButton.icon(
|
||||
onPressed: _openTractionPicker,
|
||||
icon: const Icon(Icons.search),
|
||||
label: const Text('Search traction'),
|
||||
@@ -1440,13 +1434,13 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
final routeStart = _routeResult?.calculatedRoute.isNotEmpty == true
|
||||
? _routeResult!.calculatedRoute.first
|
||||
: (_routeResult?.inputRoute.isNotEmpty == true
|
||||
? _routeResult!.inputRoute.first
|
||||
: _startController.text.trim());
|
||||
? _routeResult!.inputRoute.first
|
||||
: _startController.text.trim());
|
||||
final routeEnd = _routeResult?.calculatedRoute.isNotEmpty == true
|
||||
? _routeResult!.calculatedRoute.last
|
||||
: (_routeResult?.inputRoute.isNotEmpty == true
|
||||
? _routeResult!.inputRoute.last
|
||||
: _endController.text.trim());
|
||||
? _routeResult!.inputRoute.last
|
||||
: _endController.text.trim());
|
||||
|
||||
final mileagePanel = _section(
|
||||
'Your Journey',
|
||||
@@ -1456,12 +1450,12 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
FilledButton.icon(
|
||||
onPressed: _openCalculator,
|
||||
icon: const Icon(Icons.calculate, size: 18),
|
||||
label: const Text('Open mileage calculator'),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
TextButton.icon(
|
||||
onPressed: _reverseRouteAndEndpoints,
|
||||
icon: const Icon(Icons.swap_horiz),
|
||||
label: const Text('Reverse route'),
|
||||
@@ -1493,8 +1487,8 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: mileageLabel,
|
||||
helperText: currentDistanceUnit ==
|
||||
DistanceUnit.milesChains
|
||||
helperText:
|
||||
currentDistanceUnit == DistanceUnit.milesChains
|
||||
? 'Enter as miles.chains (e.g., 12.40 for 12m 40c)'
|
||||
: null,
|
||||
border: const OutlineInputBorder(),
|
||||
@@ -1571,7 +1565,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton.icon(
|
||||
FilledButton.icon(
|
||||
onPressed: _submitting ? null : _submit,
|
||||
icon: _submitting
|
||||
? const SizedBox(
|
||||
@@ -1783,52 +1777,52 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
},
|
||||
fieldViewBuilder:
|
||||
(context, textEditingController, focusNode, onFieldSubmitted) {
|
||||
if (textEditingController.text != controller.text) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
if (textEditingController.text != controller.text) {
|
||||
textEditingController.value = controller.value;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
if (textEditingController.text != controller.text) {
|
||||
textEditingController.value = controller.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return TextFormField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: _loadingStations
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(4.0),
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.search),
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
onChanged: (_) {
|
||||
controller.value = textEditingController.value;
|
||||
_saveDraft();
|
||||
return TextFormField(
|
||||
controller: textEditingController,
|
||||
focusNode: focusNode,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: _loadingStations
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(4.0),
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.search),
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
onChanged: (_) {
|
||||
controller.value = textEditingController.value;
|
||||
_saveDraft();
|
||||
},
|
||||
onFieldSubmitted: (_) {
|
||||
final matches = _matchStations(
|
||||
textEditingController.text,
|
||||
stationNames,
|
||||
).toList();
|
||||
if (matches.isNotEmpty) {
|
||||
final top = matches.first;
|
||||
controller.text = top;
|
||||
textEditingController.text = top;
|
||||
_saveDraft();
|
||||
}
|
||||
focusNode.unfocus();
|
||||
},
|
||||
);
|
||||
},
|
||||
onFieldSubmitted: (_) {
|
||||
final matches = _matchStations(
|
||||
textEditingController.text,
|
||||
stationNames,
|
||||
).toList();
|
||||
if (matches.isNotEmpty) {
|
||||
final top = matches.first;
|
||||
controller.text = top;
|
||||
textEditingController.text = top;
|
||||
_saveDraft();
|
||||
}
|
||||
focusNode.unfocus();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1842,7 +1836,11 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
return best;
|
||||
}
|
||||
|
||||
void _insertCandidate(List<String> best, String candidate, {required int max}) {
|
||||
void _insertCandidate(
|
||||
List<String> best,
|
||||
String candidate, {
|
||||
required int max,
|
||||
}) {
|
||||
final existingIndex = best.indexOf(candidate);
|
||||
if (existingIndex >= 0) return;
|
||||
|
||||
@@ -2016,7 +2014,6 @@ class _NewEntryPageState extends State<NewEntryPage> {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class _UpperCaseTextFormatter extends TextInputFormatter {
|
||||
|
||||
@@ -15,6 +15,7 @@ class TractionPage extends StatefulWidget {
|
||||
this.replacementPendingLocoId,
|
||||
this.transferFromLabel,
|
||||
this.transferFromLocoId,
|
||||
this.transferAllAllocations = false,
|
||||
this.onSelect,
|
||||
this.selectedKeys = const {},
|
||||
});
|
||||
@@ -24,6 +25,7 @@ class TractionPage extends StatefulWidget {
|
||||
final int? replacementPendingLocoId;
|
||||
final String? transferFromLabel;
|
||||
final int? transferFromLocoId;
|
||||
final bool transferAllAllocations;
|
||||
final ValueChanged<LocoSummary>? onSelect;
|
||||
final Set<String> selectedKeys;
|
||||
|
||||
@@ -39,6 +41,12 @@ class _TractionPageState extends State<TractionPage> {
|
||||
bool _mileageFirst = true;
|
||||
bool _initialised = false;
|
||||
int? get _transferFromLocoId => widget.transferFromLocoId;
|
||||
bool get _transferAllAllocations {
|
||||
if (widget.transferAllAllocations) return true;
|
||||
final param =
|
||||
GoRouterState.of(context).uri.queryParameters['transferAll'];
|
||||
return param?.toLowerCase() == 'true' || param == '1';
|
||||
}
|
||||
bool _showAdvancedFilters = false;
|
||||
String? _selectedClass;
|
||||
late Set<String> _selectedKeys;
|
||||
@@ -1315,13 +1323,19 @@ class _TractionPageState extends State<TractionPage> {
|
||||
context: navContext,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Text('Transfer allocations?'),
|
||||
title: Text(
|
||||
_transferAllAllocations
|
||||
? 'Transfer all allocations?'
|
||||
: 'Transfer allocations?',
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Transfer all allocations from $fromLabel to $toLabel?',
|
||||
_transferAllAllocations
|
||||
? 'Transfer all user allocations from $fromLabel to $toLabel?'
|
||||
: 'Transfer all allocations from $fromLabel to $toLabel?',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
@@ -1351,10 +1365,23 @@ class _TractionPageState extends State<TractionPage> {
|
||||
if (!navContext.mounted) return;
|
||||
try {
|
||||
final data = navContext.read<DataService>();
|
||||
await data.transferAllocations(fromLocoId: fromId, toLocoId: target.id);
|
||||
if (_transferAllAllocations) {
|
||||
await data.transferAllAllocations(
|
||||
fromLocoId: fromId,
|
||||
toLocoId: target.id,
|
||||
);
|
||||
} else {
|
||||
await data.transferAllocations(fromLocoId: fromId, toLocoId: target.id);
|
||||
}
|
||||
if (navContext.mounted) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(content: Text('Allocations transferred')),
|
||||
SnackBar(
|
||||
content: Text(
|
||||
_transferAllAllocations
|
||||
? 'All allocations transferred'
|
||||
: 'Allocations transferred',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await _refreshTraction(preservePosition: true);
|
||||
@@ -1426,7 +1453,9 @@ class _TractionPageState extends State<TractionPage> {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Transferring allocations from $label. Select a loco to transfer to.',
|
||||
_transferAllAllocations
|
||||
? 'Transferring all allocations from $label. Select a loco to transfer to.'
|
||||
: 'Transferring allocations from $label. Select a loco to transfer to.',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -724,10 +724,12 @@ Future<void> showTractionDetails(
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
final transferLabel = '${loco.locoClass} ${loco.number}'.trim();
|
||||
if (hasMileageOrTrips)
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
final transferLabel =
|
||||
'${loco.locoClass} ${loco.number}'.trim();
|
||||
navContext.push(
|
||||
Uri(
|
||||
path: '/traction',
|
||||
@@ -735,18 +737,20 @@ Future<void> showTractionDetails(
|
||||
'selection': 'single',
|
||||
'transferFromLocoId': loco.id.toString(),
|
||||
'transferFromLabel': transferLabel,
|
||||
'transferAll': '0',
|
||||
},
|
||||
).toString(),
|
||||
extra: {
|
||||
'selection': 'single',
|
||||
'transferFromLocoId': loco.id,
|
||||
'transferFromLabel': transferLabel,
|
||||
'transferAll': false,
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.swap_horiz),
|
||||
label: const Text('Transfer allocations'),
|
||||
),
|
||||
icon: const Icon(Icons.swap_horiz),
|
||||
label: const Text('Transfer allocations'),
|
||||
),
|
||||
if (auth.isElevated || canDeleteAsOwner) ...[
|
||||
const SizedBox(height: 8),
|
||||
ExpansionTile(
|
||||
@@ -757,6 +761,34 @@ Future<void> showTractionDetails(
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
children: [
|
||||
if (auth.isElevated) ...[
|
||||
FilledButton.tonal(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
final transferLabel =
|
||||
'${loco.locoClass} ${loco.number}'.trim();
|
||||
navContext.push(
|
||||
Uri(
|
||||
path: '/traction',
|
||||
queryParameters: {
|
||||
'selection': 'single',
|
||||
'transferFromLocoId': loco.id.toString(),
|
||||
'transferFromLabel': transferLabel,
|
||||
'transferAll': 'true',
|
||||
},
|
||||
).toString(),
|
||||
extra: {
|
||||
'selection': 'single',
|
||||
'transferFromLocoId': loco.id,
|
||||
'transferFromLabel': transferLabel,
|
||||
'transferAll': true,
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text('Transfer all allocations'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
FilledButton.tonal(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor:
|
||||
|
||||
@@ -482,6 +482,23 @@ extension DataServiceTraction on DataService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> transferAllAllocations({
|
||||
required int fromLocoId,
|
||||
required int toLocoId,
|
||||
}) async {
|
||||
try {
|
||||
await api.post('/loco/alloc/transfer?transferAll=true', {
|
||||
'from_loco_id': fromLocoId,
|
||||
'to_loco_id': toLocoId,
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'Failed to transfer all allocations $fromLocoId -> $toLocoId: $e',
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> adminDeleteLoco({required int locoId}) async {
|
||||
try {
|
||||
await api.delete('/loco/admin/delete/$locoId');
|
||||
|
||||
@@ -200,6 +200,22 @@ class _MyAppState extends State<MyApp> {
|
||||
(state.extra is Map
|
||||
? (state.extra as Map)['transferFromLabel']?.toString()
|
||||
: null);
|
||||
bool transferAllAllocations = false;
|
||||
final transferAllParam =
|
||||
state.uri.queryParameters['transferAll'] ??
|
||||
(state.extra is Map
|
||||
? (state.extra as Map)['transferAll']?.toString()
|
||||
: null);
|
||||
if (transferAllParam != null) {
|
||||
transferAllAllocations = transferAllParam.toLowerCase() == 'true' ||
|
||||
transferAllParam == '1';
|
||||
}
|
||||
if (!transferAllAllocations && state.extra is Map) {
|
||||
final raw = (state.extra as Map)['transferAll'];
|
||||
if (raw is bool) {
|
||||
transferAllAllocations = raw;
|
||||
}
|
||||
}
|
||||
final selectionMode =
|
||||
(selectionParam != null && selectionParam.isNotEmpty) ||
|
||||
replacementPendingLocoId != null ||
|
||||
@@ -215,6 +231,7 @@ class _MyAppState extends State<MyApp> {
|
||||
replacementPendingLocoId: replacementPendingLocoId,
|
||||
transferFromLabel: transferFromLabel,
|
||||
transferFromLocoId: transferFromLocoId,
|
||||
transferAllAllocations: transferAllAllocations,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 0.7.3+11
|
||||
version: 0.7.4+12
|
||||
|
||||
environment:
|
||||
sdk: ^3.8.1
|
||||
|
||||
Reference in New Issue
Block a user