add redate functionality to set the earliest date of entries
All checks were successful
Release / meta (push) Successful in 3s
Release / linux-build (push) Successful in 56s
Release / web-build (push) Successful in 1m43s
Release / android-build (push) Successful in 5m32s
Release / release-master (push) Successful in 7s
Release / release-dev (push) Successful in 12s
All checks were successful
Release / meta (push) Successful in 3s
Release / linux-build (push) Successful in 56s
Release / web-build (push) Successful in 1m43s
Release / android-build (push) Successful in 5m32s
Release / release-master (push) Successful in 7s
Release / release-dev (push) Successful in 12s
This commit is contained in:
@@ -13,6 +13,8 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
part 'loco_timeline/timeline_grid.dart';
|
part 'loco_timeline/timeline_grid.dart';
|
||||||
part 'loco_timeline/event_editor.dart';
|
part 'loco_timeline/event_editor.dart';
|
||||||
|
|
||||||
|
enum _TimelineMenuAction { redate }
|
||||||
|
|
||||||
class LocoTimelinePage extends StatefulWidget {
|
class LocoTimelinePage extends StatefulWidget {
|
||||||
const LocoTimelinePage({
|
const LocoTimelinePage({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -537,6 +539,239 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
return _draftEvents.every(_draftIsValid);
|
return _draftEvents.every(_draftIsValid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleTimelineMenuAction(
|
||||||
|
_TimelineMenuAction action,
|
||||||
|
List<LocoAttrVersion> timeline,
|
||||||
|
) async {
|
||||||
|
switch (action) {
|
||||||
|
case _TimelineMenuAction.redate:
|
||||||
|
await _startMassRedate(timeline);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startMassRedate(List<LocoAttrVersion> timeline) async {
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
final eventDate = await _showRedateDateDialog();
|
||||||
|
if (!mounted || eventDate == null) return;
|
||||||
|
|
||||||
|
final earliestByAttr = _earliestEntriesByAttr(timeline);
|
||||||
|
if (earliestByAttr.isEmpty) {
|
||||||
|
messenger.showSnackBar(
|
||||||
|
const SnackBar(content: Text('No timeline values available to redate.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await _showMassRedateProgressDialog(
|
||||||
|
eventDate: eventDate,
|
||||||
|
entries: earliestByAttr,
|
||||||
|
);
|
||||||
|
if (!mounted || result == null) return;
|
||||||
|
|
||||||
|
await _load();
|
||||||
|
if (!mounted) return;
|
||||||
|
final successCount = result.total - result.failures;
|
||||||
|
final message = result.failures == 0
|
||||||
|
? 'Redated $successCount attributes.'
|
||||||
|
: 'Redated $successCount of ${result.total} attributes (${result.failures} failed).';
|
||||||
|
messenger.showSnackBar(SnackBar(content: Text(message)));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LocoAttrVersion> _earliestEntriesByAttr(List<LocoAttrVersion> timeline) {
|
||||||
|
final byAttr = <String, LocoAttrVersion>{};
|
||||||
|
for (final entry in timeline) {
|
||||||
|
if (entry.isPending) continue;
|
||||||
|
final key = entry.attrCode.trim().toLowerCase();
|
||||||
|
if (key.isEmpty) continue;
|
||||||
|
final current = byAttr[key];
|
||||||
|
if (current == null || LocoAttrVersion.compareByStart(entry, current) < 0) {
|
||||||
|
byAttr[key] = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final entries = byAttr.values.toList()
|
||||||
|
..sort((a, b) => a.attrCode.compareTo(b.attrCode));
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _showRedateDateDialog() async {
|
||||||
|
final controller = TextEditingController(
|
||||||
|
text: DateFormat('yyyy-MM-dd').format(DateTime.now()),
|
||||||
|
);
|
||||||
|
String? validationError;
|
||||||
|
final result = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) {
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (dialogContext, setDialogState) {
|
||||||
|
Future<void> pickDate() async {
|
||||||
|
final initial = DateTime.tryParse(controller.text.trim()) ??
|
||||||
|
DateTime.now();
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: dialogContext,
|
||||||
|
initialDate: initial,
|
||||||
|
firstDate: DateTime(1800),
|
||||||
|
lastDate: DateTime(2500),
|
||||||
|
);
|
||||||
|
if (picked == null) return;
|
||||||
|
final nextValue = DateFormat('yyyy-MM-dd').format(picked);
|
||||||
|
setDialogState(() {
|
||||||
|
controller.text = nextValue;
|
||||||
|
validationError = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Redate timeline attributes'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Enter the target date. The earliest value of each attribute will be submitted at this date.',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Date',
|
||||||
|
hintText: 'YYYY-MM-DD',
|
||||||
|
errorText: validationError,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
tooltip: 'Pick date',
|
||||||
|
icon: const Icon(Icons.calendar_month),
|
||||||
|
onPressed: pickDate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onSubmitted: (_) {
|
||||||
|
final value = controller.text.trim();
|
||||||
|
if (!_isValidDateString(value)) {
|
||||||
|
setDialogState(() {
|
||||||
|
validationError = 'Use format YYYY-MM-DD.';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Navigator.of(dialogContext).pop(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
final value = controller.text.trim();
|
||||||
|
if (!_isValidDateString(value)) {
|
||||||
|
setDialogState(() {
|
||||||
|
validationError = 'Use format YYYY-MM-DD.';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Navigator.of(dialogContext).pop(value);
|
||||||
|
},
|
||||||
|
child: const Text('Run'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
controller.dispose();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<_MassRedateResult?> _showMassRedateProgressDialog({
|
||||||
|
required String eventDate,
|
||||||
|
required List<LocoAttrVersion> entries,
|
||||||
|
}) async {
|
||||||
|
final data = context.read<DataService>();
|
||||||
|
var started = false;
|
||||||
|
var completed = 0;
|
||||||
|
var failures = 0;
|
||||||
|
var currentAttrLabel = '';
|
||||||
|
|
||||||
|
return showDialog<_MassRedateResult>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (dialogContext) {
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (dialogContext, setDialogState) {
|
||||||
|
Future<void> run() async {
|
||||||
|
for (final entry in entries) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setDialogState(() {
|
||||||
|
currentAttrLabel = _formatAttrLabel(entry.attrCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
final value = _valueForEntry(entry);
|
||||||
|
final isBlank = value is String && value.trim().isEmpty;
|
||||||
|
if (value == null || isBlank) {
|
||||||
|
failures += 1;
|
||||||
|
setDialogState(() {
|
||||||
|
completed += 1;
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await data.createLocoEvent(
|
||||||
|
locoId: widget.locoId,
|
||||||
|
eventDate: eventDate,
|
||||||
|
values: {entry.attrCode: value},
|
||||||
|
details: '',
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
failures += 1;
|
||||||
|
}
|
||||||
|
if (!mounted || !dialogContext.mounted) return;
|
||||||
|
setDialogState(() {
|
||||||
|
completed += 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted || !dialogContext.mounted) return;
|
||||||
|
Navigator.of(dialogContext).pop(
|
||||||
|
_MassRedateResult(total: entries.length, failures: failures),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!started) {
|
||||||
|
started = true;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final total = entries.length;
|
||||||
|
final progress = total == 0 ? 0.0 : completed / total;
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Redating attributes'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
LinearProgressIndicator(value: progress),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text('$completed / $total complete'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
currentAttrLabel.isEmpty
|
||||||
|
? 'Preparing requests...'
|
||||||
|
: 'Current: $currentAttrLabel',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final data = context.watch<DataService>();
|
final data = context.watch<DataService>();
|
||||||
@@ -554,6 +789,20 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
onPressed: () => Navigator.of(context).maybePop(),
|
onPressed: () => Navigator.of(context).maybePop(),
|
||||||
),
|
),
|
||||||
title: Text('Timeline · ${widget.locoLabel}'),
|
title: Text('Timeline · ${widget.locoLabel}'),
|
||||||
|
actions: [
|
||||||
|
PopupMenuButton<_TimelineMenuAction>(
|
||||||
|
tooltip: 'Timeline actions',
|
||||||
|
icon: const Icon(Icons.more_vert),
|
||||||
|
onSelected: (action) =>
|
||||||
|
_handleTimelineMenuAction(action, visibleTimeline),
|
||||||
|
itemBuilder: (context) => const [
|
||||||
|
PopupMenuItem<_TimelineMenuAction>(
|
||||||
|
value: _TimelineMenuAction.redate,
|
||||||
|
child: Text('Redate'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: RefreshIndicator(
|
body: RefreshIndicator(
|
||||||
onRefresh: _load,
|
onRefresh: _load,
|
||||||
@@ -684,3 +933,13 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _MassRedateResult {
|
||||||
|
const _MassRedateResult({
|
||||||
|
required this.total,
|
||||||
|
required this.failures,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int total;
|
||||||
|
final int failures;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
# 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
|
# 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.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.8.2+19
|
version: 0.8.3+20
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
|
|||||||
Reference in New Issue
Block a user