add timeline edit/delete
All checks were successful
Release / meta (push) Successful in 8s
Release / linux-build (push) Successful in 6m36s
Release / android-build (push) Successful in 17m20s
Release / release-dev (push) Successful in 37s
Release / release-master (push) Successful in 35s

This commit is contained in:
2025-12-17 12:17:41 +00:00
parent 80be797322
commit fa9773bcd1
9 changed files with 454 additions and 38 deletions

View File

@@ -1,6 +1,8 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:mileograph_flutter/objects/objects.dart';
import 'package:mileograph_flutter/services/data_service.dart';
@@ -26,6 +28,7 @@ class LocoTimelinePage extends StatefulWidget {
class _LocoTimelinePageState extends State<LocoTimelinePage> {
final List<_EventDraft> _draftEvents = [];
bool _isSaving = false;
bool _isDeleting = false;
@override
void initState() {
@@ -51,6 +54,143 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
});
}
String? _eventDateForEntry(LocoAttrVersion entry) {
final masked = entry.maskedValidFrom?.trim();
if (masked != null && masked.isNotEmpty) return masked;
final from = entry.validFrom ?? entry.txnFrom;
if (from == null) return null;
return DateFormat('yyyy-MM-dd').format(from);
}
EventField? _fieldForAttr(String attrCode, List<EventField> fields) {
final normalized = attrCode.trim().toLowerCase();
for (final field in fields) {
if (field.name.trim().toLowerCase() == normalized) return field;
}
return null;
}
dynamic _valueForEntry(LocoAttrVersion entry) {
if (entry.valueInt != null) return entry.valueInt;
if (entry.valueBool != null) return entry.valueBool;
if (entry.valueEnum != null && entry.valueEnum!.isNotEmpty) {
return entry.valueEnum;
}
if (entry.valueStr != null && entry.valueStr!.isNotEmpty) {
return entry.valueStr;
}
if (entry.valueDate != null) {
return DateFormat('yyyy-MM-dd').format(entry.valueDate!);
}
if (entry.valueNorm != null && entry.valueNorm.toString().isNotEmpty) {
return entry.valueNorm;
}
final label = entry.valueLabel;
return label == '' ? '' : label;
}
void _prefillDraftFromEntry(LocoAttrVersion entry, List<EventField> fields) {
final dateStr = _eventDateForEntry(entry);
if (dateStr == null || dateStr.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Cannot edit: timeline block date unknown.')),
);
return;
}
final field = _fieldForAttr(entry.attrCode, fields);
if (field == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Cannot edit: no event field found for ${_formatAttrLabel(entry.attrCode)}.',
),
),
);
return;
}
final draft = _EventDraft();
draft.dateController.text = dateStr;
draft.detailsController.text = '';
draft.details = '';
draft.fields.add(
_FieldEntry(field: field)
..value = _valueForEntry(entry),
);
setState(() {
_draftEvents.add(draft);
});
}
Future<void> _deleteEntry(LocoAttrVersion entry) async {
if (_isDeleting) return;
final blockId = entry.versionId;
if (blockId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Cannot delete: timeline block has no ID.')),
);
return;
}
final data = context.read<DataService>();
final messenger = ScaffoldMessenger.of(context);
final dateStr = _eventDateForEntry(entry);
final ok = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Delete timeline block?'),
content: Text(
dateStr == null || dateStr.isEmpty
? 'This will delete the selected block for ${_formatAttrLabel(entry.attrCode)}.'
: 'This will delete the block for ${_formatAttrLabel(entry.attrCode)} starting at $dateStr.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Delete'),
),
],
);
},
);
if (ok != true) return;
if (!mounted) return;
setState(() {
_isDeleting = true;
});
try {
await data.deleteTimelineBlock(
blockId: blockId,
);
await _load();
if (mounted) {
messenger.showSnackBar(
const SnackBar(content: Text('Timeline block deleted')),
);
}
} catch (e) {
if (mounted) {
messenger.showSnackBar(
SnackBar(content: Text('Failed to delete timeline block: $e')),
);
}
} finally {
if (mounted) {
setState(() {
_isDeleting = false;
});
}
}
}
void _removeDraftAt(int index) {
if (index < 0 || index >= _draftEvents.length) return;
final draft = _draftEvents.removeAt(index);
@@ -241,6 +381,11 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
children: [
_TimelineGrid(
entries: timeline,
onEditEntry: (entry) => _prefillDraftFromEntry(
entry,
data.eventFields,
),
onDeleteEntry: _deleteEntry,
),
const SizedBox(height: 16),
_EventEditor(