add ability for non admins to add new traction, pending approval. Various QoL updates
All checks were successful
Release / meta (push) Successful in 6s
Release / linux-build (push) Successful in 57s
Release / web-build (push) Successful in 1m14s
Release / android-build (push) Successful in 5m33s
Release / release-master (push) Successful in 18s
Release / release-dev (push) Successful in 20s
All checks were successful
Release / meta (push) Successful in 6s
Release / linux-build (push) Successful in 57s
Release / web-build (push) Successful in 1m14s
Release / android-build (push) Successful in 5m33s
Release / release-master (push) Successful in 18s
Release / release-dev (push) Successful in 20s
This commit is contained in:
@@ -5,6 +5,7 @@ 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/authservice.dart';
|
||||
import 'package:mileograph_flutter/services/data_service.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -29,6 +30,7 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
final List<_EventDraft> _draftEvents = [];
|
||||
bool _isSaving = false;
|
||||
bool _isDeleting = false;
|
||||
bool _isModerating = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -59,8 +61,12 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
|
||||
Future<void> _load() {
|
||||
final data = context.read<DataService>();
|
||||
final auth = context.read<AuthService>();
|
||||
data.fetchEventFields();
|
||||
return data.fetchLocoTimeline(widget.locoId);
|
||||
return data.fetchLocoTimeline(
|
||||
widget.locoId,
|
||||
includeAllPending: auth.isElevated,
|
||||
);
|
||||
}
|
||||
|
||||
void _addDraftEvent() {
|
||||
@@ -151,8 +157,18 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
|
||||
Future<void> _deleteEntry(LocoAttrVersion entry) async {
|
||||
if (_isDeleting) return;
|
||||
final isPending = entry.isPending;
|
||||
final blockId = entry.versionId;
|
||||
if (blockId == null) {
|
||||
final pendingEventId = entry.sourceEventId;
|
||||
if (isPending && pendingEventId == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Cannot delete: pending timeline block has no event ID.'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!isPending && blockId == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Cannot delete: timeline block has no ID.')),
|
||||
);
|
||||
@@ -193,13 +209,23 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
_isDeleting = true;
|
||||
});
|
||||
try {
|
||||
await data.deleteTimelineBlock(
|
||||
blockId: blockId,
|
||||
);
|
||||
if (isPending && pendingEventId != null) {
|
||||
await data.deletePendingEvent(eventId: pendingEventId);
|
||||
} else if (blockId != null) {
|
||||
await data.deleteTimelineBlock(
|
||||
blockId: blockId,
|
||||
);
|
||||
}
|
||||
await _load();
|
||||
if (mounted) {
|
||||
messenger.showSnackBar(
|
||||
const SnackBar(content: Text('Timeline block deleted')),
|
||||
SnackBar(
|
||||
content: Text(
|
||||
isPending
|
||||
? 'Pending timeline block deleted'
|
||||
: 'Timeline block deleted',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -217,6 +243,79 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _moderatePendingEntry(
|
||||
LocoAttrVersion entry,
|
||||
_PendingModerationAction action,
|
||||
) async {
|
||||
if (_isModerating) return;
|
||||
final eventId = entry.sourceEventId;
|
||||
if (eventId == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Cannot moderate: pending timeline block has no event ID.'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final data = context.read<DataService>();
|
||||
final approve = action == _PendingModerationAction.approve;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final verb = approve ? 'approve' : 'reject';
|
||||
final ok = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('${approve ? 'Approve' : 'Reject'} pending event?'),
|
||||
content: Text(
|
||||
'Are you sure you want to $verb this pending timeline block?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(approve ? 'Approve' : 'Reject'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (ok != true || !mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isModerating = true;
|
||||
});
|
||||
try {
|
||||
if (approve) {
|
||||
await data.approvePendingEvent(eventId: eventId);
|
||||
} else {
|
||||
await data.rejectPendingEvent(eventId: eventId);
|
||||
}
|
||||
await _load();
|
||||
if (mounted) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Pending timeline block ${approve ? 'approved' : 'rejected'}.',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('Failed to $verb pending timeline block: $e')),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isModerating = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _removeDraftAt(int index) {
|
||||
if (index < 0 || index >= _draftEvents.length) return;
|
||||
final draft = _draftEvents.removeAt(index);
|
||||
@@ -248,6 +347,9 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
_isSaving = true;
|
||||
});
|
||||
try {
|
||||
final existingPending =
|
||||
await data.fetchUserPendingEvents(widget.locoId);
|
||||
final clearedEventIds = <int>{};
|
||||
final invalid = <String>[];
|
||||
for (final draft in _draftEvents) {
|
||||
final dateStr = draft.dateController.text.trim();
|
||||
@@ -274,6 +376,13 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
invalid.add('Add at least one value');
|
||||
continue;
|
||||
}
|
||||
await _clearDuplicatePending(
|
||||
existingPending,
|
||||
clearedEventIds,
|
||||
values.keys,
|
||||
dateStr,
|
||||
data,
|
||||
);
|
||||
await data.createLocoEvent(
|
||||
locoId: widget.locoId,
|
||||
eventDate: dateStr,
|
||||
@@ -312,6 +421,42 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _clearDuplicatePending(
|
||||
List<LocoAttrVersion> existingPending,
|
||||
Set<int> clearedEventIds,
|
||||
Iterable<String> attrs,
|
||||
String dateStr,
|
||||
DataService data,
|
||||
) async {
|
||||
final trimmedDate = dateStr.trim().toLowerCase();
|
||||
final attrSet = attrs.map((e) => e.toLowerCase()).toSet();
|
||||
for (final pending in existingPending) {
|
||||
final attrMatch = attrSet.contains(pending.attrCode.toLowerCase());
|
||||
if (!attrMatch) continue;
|
||||
final matchesDate = _dateMatchesPending(trimmedDate, pending);
|
||||
if (!matchesDate) continue;
|
||||
final eventId = pending.sourceEventId;
|
||||
if (eventId == null || clearedEventIds.contains(eventId)) continue;
|
||||
await data.deletePendingEvent(eventId: eventId);
|
||||
clearedEventIds.add(eventId);
|
||||
}
|
||||
}
|
||||
|
||||
bool _dateMatchesPending(String draftDateLower, LocoAttrVersion pending) {
|
||||
final masked = pending.maskedValidFrom?.trim().toLowerCase();
|
||||
if (masked != null && masked.isNotEmpty && masked == draftDateLower) {
|
||||
return true;
|
||||
}
|
||||
final draftDate = DateTime.tryParse(draftDateLower);
|
||||
final pendingDate = pending.validFrom;
|
||||
if (draftDate != null && pendingDate != null) {
|
||||
return draftDate.year == pendingDate.year &&
|
||||
draftDate.month == pendingDate.month &&
|
||||
draftDate.day == pendingDate.day;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _isValidDateString(String input) {
|
||||
final trimmed = input.trim();
|
||||
final regex = RegExp(r'^\d{4}-(\d{2}|xx|XX)-(\d{2}|xx|XX)$');
|
||||
@@ -412,6 +557,8 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
data.eventFields,
|
||||
),
|
||||
onDeleteEntry: _deleteEntry,
|
||||
onModeratePending: _moderatePendingEntry,
|
||||
pendingActionsBusy: _isModerating,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_EventEditor(
|
||||
|
||||
Reference in New Issue
Block a user