Improve entries page and latest changes panel, units on events and timeline
All checks were successful
All checks were successful
This commit is contained in:
@@ -3,8 +3,10 @@ import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:mileograph_flutter/objects/objects.dart';
|
||||
import 'package:mileograph_flutter/services/data_service.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class LegCard extends StatelessWidget {
|
||||
class LegCard extends StatefulWidget {
|
||||
const LegCard({
|
||||
super.key,
|
||||
required this.leg,
|
||||
@@ -16,30 +18,106 @@ class LegCard extends StatelessWidget {
|
||||
final bool showEditButton;
|
||||
final bool showDate;
|
||||
|
||||
@override
|
||||
State<LegCard> createState() => _LegCardState();
|
||||
}
|
||||
|
||||
class _LegCardState extends State<LegCard> {
|
||||
bool _expanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final leg = widget.leg;
|
||||
final routeSegments = _parseRouteSegments(leg.route);
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return Card(
|
||||
child: ExpansionTile(
|
||||
onExpansionChanged: (v) => setState(() => _expanded = v),
|
||||
tilePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
leading: const Icon(Icons.train),
|
||||
title: Text('${leg.start} → ${leg.end}'),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showDate) Text(_formatDateTime(leg.beginTime)),
|
||||
if (leg.headcode.isNotEmpty)
|
||||
Text(
|
||||
'Headcode: ${leg.headcode}',
|
||||
style: textTheme.labelSmall,
|
||||
),
|
||||
if (leg.network.isNotEmpty)
|
||||
Text(
|
||||
leg.network,
|
||||
style: textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
title: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = constraints.maxWidth > 520;
|
||||
final routeText = Text('${leg.start} → ${leg.end}');
|
||||
final timeText =
|
||||
Text(_formatDateTime(leg.beginTime, includeDate: widget.showDate));
|
||||
if (!isWide) {
|
||||
return routeText;
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
timeText,
|
||||
const SizedBox(width: 6),
|
||||
const Text('·'),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(child: routeText),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
subtitle: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = constraints.maxWidth > 520;
|
||||
final timeWidget =
|
||||
Text(_formatDateTime(leg.beginTime, includeDate: widget.showDate));
|
||||
final tractionWrap = !_expanded && leg.locos.isNotEmpty
|
||||
? Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: leg.locos.map((loco) {
|
||||
final iconColor = loco.powering
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).hintColor;
|
||||
final label = '${loco.locoClass} ${loco.number}'.trim();
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.train, size: 14, color: iconColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label.isEmpty ? 'Loco ${loco.id}' : label,
|
||||
style: textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
)
|
||||
: null;
|
||||
|
||||
final children = <Widget>[];
|
||||
if (isWide) {
|
||||
if (tractionWrap != null) {
|
||||
children.add(tractionWrap);
|
||||
}
|
||||
} else {
|
||||
children.add(timeWidget);
|
||||
if (tractionWrap != null) {
|
||||
children.add(const SizedBox(height: 4));
|
||||
children.add(tractionWrap);
|
||||
}
|
||||
}
|
||||
if (leg.headcode.isNotEmpty) {
|
||||
children.add(
|
||||
Text(
|
||||
'Headcode: ${leg.headcode}',
|
||||
style: textTheme.labelSmall,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (leg.network.isNotEmpty) {
|
||||
children.add(
|
||||
Text(
|
||||
leg.network,
|
||||
style: textTheme.labelSmall,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -66,7 +144,7 @@ class LegCard extends StatelessWidget {
|
||||
],
|
||||
],
|
||||
),
|
||||
if (showEditButton) ...[
|
||||
if (widget.showEditButton) ...[
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
tooltip: 'Edit entry',
|
||||
@@ -76,6 +154,18 @@ class LegCard extends StatelessWidget {
|
||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||
onPressed: () => context.push('/legs/edit/${leg.id}'),
|
||||
),
|
||||
if (_expanded) ...[
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
tooltip: 'Delete entry',
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||
onPressed: () => _confirmDelete(context, leg.id),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -114,15 +204,52 @@ class LegCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _confirmDelete(BuildContext context, int legId) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Delete entry?'),
|
||||
content: const Text('Are you sure you want to delete this entry?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
|
||||
final data = context.read<DataService>();
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
try {
|
||||
await data.api.delete('/legs/delete?leg_id=$legId');
|
||||
await data.refreshLegs();
|
||||
messenger.showSnackBar(const SnackBar(content: Text('Entry deleted')));
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text('Failed to delete entry: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDate(DateTime? date) {
|
||||
if (date == null) return '';
|
||||
return '${date.year.toString().padLeft(4, '0')}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime date) {
|
||||
String _formatTime(DateTime date) {
|
||||
return '${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime date, {bool includeDate = true}) {
|
||||
final timeStr = _formatTime(date);
|
||||
if (!includeDate) return timeStr;
|
||||
final dateStr = _formatDate(date);
|
||||
final timeStr =
|
||||
'${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
||||
return '$dateStr · $timeStr';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user