706 lines
21 KiB
Dart
706 lines
21 KiB
Dart
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 ProfilePage extends StatefulWidget {
|
|
const ProfilePage({super.key});
|
|
|
|
@override
|
|
State<ProfilePage> createState() => _ProfilePageState();
|
|
}
|
|
|
|
class _ProfilePageState extends State<ProfilePage> {
|
|
bool _initialised = false;
|
|
final Map<String, bool> _groupExpanded = {};
|
|
bool _loadingAwards = false;
|
|
bool _loadingClassProgress = false;
|
|
bool _loadingLocoProgress = false;
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
if (_initialised) return;
|
|
_initialised = true;
|
|
_refreshAwards();
|
|
}
|
|
|
|
Future<void> _refreshAwards() {
|
|
_loadingAwards = false;
|
|
_loadingClassProgress = false;
|
|
_loadingLocoProgress = false;
|
|
final data = context.read<DataService>();
|
|
return Future.wait([
|
|
data.fetchBadgeAwards(limit: 20, badgeCode: 'class_clearance'),
|
|
data.fetchClassClearanceProgress(),
|
|
data.fetchLocoClearanceProgress(),
|
|
]);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final data = context.watch<DataService>();
|
|
final awards = data.badgeAwards;
|
|
final loading = data.isBadgeAwardsLoading;
|
|
final classProgress = data.classClearanceProgress;
|
|
final classProgressLoading =
|
|
data.isClassClearanceProgressLoading || _loadingClassProgress;
|
|
final locoProgress = data.locoClearanceProgress;
|
|
final locoProgressLoading =
|
|
data.isLocoClearanceProgressLoading || _loadingLocoProgress;
|
|
final hasAnyData =
|
|
awards.isNotEmpty || classProgress.isNotEmpty || locoProgress.isNotEmpty;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Badges'),
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () {
|
|
final navigator = Navigator.of(context);
|
|
if (navigator.canPop()) {
|
|
navigator.pop();
|
|
} else {
|
|
context.go('/');
|
|
}
|
|
},
|
|
),
|
|
),
|
|
body: RefreshIndicator(
|
|
onRefresh: _refreshAwards,
|
|
child: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
if ((loading || classProgressLoading || locoProgressLoading) &&
|
|
!hasAnyData)
|
|
const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 24.0),
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
)
|
|
else if (!hasAnyData)
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 12.0),
|
|
child: Text('No badges awarded yet.'),
|
|
)
|
|
else
|
|
..._buildGroupedAwards(
|
|
context,
|
|
awards,
|
|
classProgress,
|
|
locoProgress,
|
|
classProgressLoading,
|
|
locoProgressLoading,
|
|
data.classClearanceHasMore,
|
|
data.locoClearanceHasMore,
|
|
data.badgeAwardsHasMore,
|
|
loading,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
List<Widget> _buildGroupedAwards(
|
|
BuildContext context,
|
|
List<BadgeAward> awards,
|
|
List<ClassClearanceProgress> classProgress,
|
|
List<LocoClearanceProgress> locoProgress,
|
|
bool classProgressLoading,
|
|
bool locoProgressLoading,
|
|
bool classProgressHasMore,
|
|
bool locoProgressHasMore,
|
|
bool badgeAwardsHasMore,
|
|
bool badgeAwardsLoading,
|
|
) {
|
|
final grouped = _groupAwards(awards);
|
|
if ((classProgress.isNotEmpty || classProgressLoading) &&
|
|
!grouped.containsKey('class_clearance')) {
|
|
grouped['class_clearance'] = [];
|
|
}
|
|
if ((locoProgress.isNotEmpty || locoProgressLoading) &&
|
|
!grouped.containsKey('loco_clearance')) {
|
|
grouped['loco_clearance'] = [];
|
|
}
|
|
final codes = _orderedBadgeCodes(grouped.keys.toList());
|
|
|
|
return codes.map((code) {
|
|
final items = grouped[code]!;
|
|
final expanded = _groupExpanded[code] ?? true;
|
|
final title = _formatBadgeName(code);
|
|
final isClass = code == 'class_clearance';
|
|
final isLoco = code == 'loco_clearance';
|
|
final classItems = isClass ? classProgress : <ClassClearanceProgress>[];
|
|
final locoItems = isLoco ? locoProgress : <LocoClearanceProgress>[];
|
|
final awardCount = isLoco
|
|
? locoItems.where((item) => item.awardedTiers.isNotEmpty).length
|
|
: items.length;
|
|
final isLoadingSection = isClass
|
|
? (classProgressLoading || badgeAwardsLoading || _loadingAwards)
|
|
: (isLoco ? locoProgressLoading : false);
|
|
|
|
final children = <Widget>[];
|
|
|
|
if (isClass && items.isNotEmpty) {
|
|
children.add(_buildSubheading(context, 'Awarded'));
|
|
children.addAll(
|
|
items.map(
|
|
(award) => Padding(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
|
|
child: _buildAwardCard(context, award, compact: true),
|
|
),
|
|
),
|
|
);
|
|
if (badgeAwardsHasMore || badgeAwardsLoading || _loadingAwards) {
|
|
children.add(
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4.0, bottom: 8.0),
|
|
child: _buildLoadMoreButton(
|
|
context,
|
|
badgeAwardsLoading || _loadingAwards,
|
|
() => _loadMoreAwards(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} else if (!isClass && !isLoco && items.isNotEmpty) {
|
|
children.add(_buildSubheading(context, 'Awarded'));
|
|
children.addAll(
|
|
items.map(
|
|
(award) => Padding(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
|
|
child: _buildAwardCard(context, award, compact: true),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (isClass) {
|
|
children.addAll(
|
|
_buildClassProgressSection(
|
|
context,
|
|
classItems,
|
|
classProgressLoading,
|
|
classProgressHasMore,
|
|
),
|
|
);
|
|
}
|
|
if (isLoco) {
|
|
children.addAll(
|
|
_buildLocoProgressSection(
|
|
context,
|
|
locoItems,
|
|
locoProgressLoading,
|
|
locoProgressHasMore,
|
|
showHeading: false,
|
|
),
|
|
);
|
|
}
|
|
|
|
if (children.isEmpty && !isLoadingSection) {
|
|
children.add(
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 8.0),
|
|
child: Text('No awards'),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(vertical: 6.0),
|
|
child: ExpansionTile(
|
|
key: ValueKey(code),
|
|
tilePadding: const EdgeInsets.symmetric(horizontal: 12.0),
|
|
title: Row(
|
|
children: [
|
|
Expanded(child: Text(title)),
|
|
if (isLoadingSection) ...[
|
|
const SizedBox(width: 8),
|
|
const SizedBox(
|
|
height: 18,
|
|
width: 18,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
),
|
|
],
|
|
const SizedBox(width: 8),
|
|
_buildCountChip(context, awardCount),
|
|
],
|
|
),
|
|
initiallyExpanded: expanded,
|
|
onExpansionChanged: (isOpen) {
|
|
setState(() => _groupExpanded[code] = isOpen);
|
|
},
|
|
children: children,
|
|
),
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
Map<String, List<BadgeAward>> _groupAwards(List<BadgeAward> awards) {
|
|
final Map<String, List<BadgeAward>> grouped = {};
|
|
for (final award in awards) {
|
|
final code = award.badgeCode.toLowerCase();
|
|
grouped.putIfAbsent(code, () => []).add(award);
|
|
}
|
|
return grouped;
|
|
}
|
|
|
|
Widget _buildAwardCard(
|
|
BuildContext context,
|
|
BadgeAward award, {
|
|
bool compact = false,
|
|
}) {
|
|
final badgeName = _formatBadgeName(award.badgeCode);
|
|
final tier = award.badgeTier.isNotEmpty
|
|
? award.badgeTier[0].toUpperCase() + award.badgeTier.substring(1)
|
|
: '';
|
|
final tierIcon = _buildTierIcon(award.badgeTier);
|
|
final scope = _scopeToShow(award);
|
|
|
|
final content = Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
if (tierIcon != null) ...[
|
|
tierIcon,
|
|
const SizedBox(width: 8),
|
|
],
|
|
Expanded(
|
|
child: Text(
|
|
'$badgeName • $tier',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
if (award.awardedAt != null)
|
|
Text(
|
|
_formatAwardDate(award.awardedAt!),
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
],
|
|
),
|
|
if (scope != null && scope.isNotEmpty) ...[
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
scope,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
],
|
|
if (award.loco != null) ...[
|
|
const SizedBox(height: 6),
|
|
_buildLocoInfo(context, award.loco!),
|
|
],
|
|
],
|
|
);
|
|
|
|
if (compact) {
|
|
return content;
|
|
}
|
|
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(10.0),
|
|
child: content,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLocoInfo(BuildContext context, LocoSummary loco) {
|
|
final lines = <String>[];
|
|
final classNum = [
|
|
if (loco.locoClass.isNotEmpty) loco.locoClass,
|
|
if (loco.number.isNotEmpty) loco.number,
|
|
].join(' ');
|
|
if (classNum.isNotEmpty) lines.add(classNum);
|
|
if ((loco.name ?? '').isNotEmpty) lines.add(loco.name!);
|
|
if ((loco.livery ?? '').isNotEmpty) lines.add(loco.livery!);
|
|
if ((loco.location ?? '').isNotEmpty) lines.add(loco.location!);
|
|
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Icon(Icons.train, size: 20),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: lines.map((line) {
|
|
return Text(
|
|
line,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
String _formatBadgeName(String code) {
|
|
if (code.isEmpty) return 'Badge';
|
|
const known = {
|
|
'class_clearance': 'Class Clearance',
|
|
'loco_clearance': 'Loco Clearance',
|
|
};
|
|
final lower = code.toLowerCase();
|
|
if (known.containsKey(lower)) return known[lower]!;
|
|
final parts = code.split(RegExp(r'[_\\s]+')).where((p) => p.isNotEmpty);
|
|
return parts
|
|
.map((p) => p[0].toUpperCase() + p.substring(1).toLowerCase())
|
|
.join(' ');
|
|
}
|
|
|
|
List<String> _orderedBadgeCodes(List<String> codes) {
|
|
final lowerCodes = codes.map((c) => c.toLowerCase()).toSet();
|
|
final ordered = <String>[];
|
|
for (final code in ['loco_clearance', 'class_clearance']) {
|
|
if (lowerCodes.remove(code)) ordered.add(code);
|
|
}
|
|
final remaining = lowerCodes.toList()
|
|
..sort((a, b) => _formatBadgeName(a).compareTo(_formatBadgeName(b)));
|
|
ordered.addAll(remaining);
|
|
return ordered;
|
|
}
|
|
|
|
Widget _buildSubheading(BuildContext context, String label) {
|
|
return Padding(
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
|
child: Text(
|
|
label,
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.labelMedium
|
|
?.copyWith(fontWeight: FontWeight.w700),
|
|
),
|
|
);
|
|
}
|
|
|
|
List<Widget> _buildClassProgressSection(
|
|
BuildContext context,
|
|
List<ClassClearanceProgress> progress,
|
|
bool isLoading,
|
|
bool hasMore,
|
|
) {
|
|
if (progress.isEmpty && !isLoading && !hasMore) return const [];
|
|
return [
|
|
_buildSubheading(context, 'In Progress'),
|
|
...progress.map(
|
|
(item) => Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0),
|
|
child: _buildClassProgressCard(context, item),
|
|
),
|
|
),
|
|
if (hasMore || isLoading)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4.0, bottom: 8.0),
|
|
child: _buildLoadMoreButton(
|
|
context,
|
|
isLoading,
|
|
() => _loadMoreClassProgress(),
|
|
),
|
|
),
|
|
if (progress.isNotEmpty) const SizedBox(height: 4),
|
|
];
|
|
}
|
|
|
|
List<Widget> _buildLocoProgressSection(
|
|
BuildContext context,
|
|
List<LocoClearanceProgress> progress,
|
|
bool isLoading,
|
|
bool hasMore,
|
|
{bool showHeading = true}
|
|
) {
|
|
if (progress.isEmpty && !isLoading && !hasMore) return const [];
|
|
return [
|
|
if (showHeading) _buildSubheading(context, 'In Progress'),
|
|
if (progress.isEmpty && isLoading)
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
|
child: _buildLoadingIndicator(),
|
|
),
|
|
...progress.map(
|
|
(item) => Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6.0),
|
|
child: _buildLocoProgressCard(context, item),
|
|
),
|
|
),
|
|
if (hasMore || isLoading)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4.0, bottom: 8.0),
|
|
child: _buildLoadMoreButton(
|
|
context,
|
|
isLoading,
|
|
() => _loadMoreLocoProgress(),
|
|
),
|
|
),
|
|
if (progress.isNotEmpty) const SizedBox(height: 4),
|
|
];
|
|
}
|
|
|
|
Widget _buildClassProgressCard(
|
|
BuildContext context,
|
|
ClassClearanceProgress progress,
|
|
) {
|
|
final pct = progress.percentComplete.clamp(0, 100);
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(10.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
progress.className,
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
),
|
|
Text(
|
|
'${pct.toStringAsFixed(0)}%',
|
|
style: Theme.of(context).textTheme.labelMedium,
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
LinearProgressIndicator(
|
|
value: progress.total == 0 ? 0 : pct / 100,
|
|
minHeight: 6,
|
|
),
|
|
if (progress.total > 0)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 2.0),
|
|
child: Text(
|
|
'${progress.completed}/${progress.total}',
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.labelSmall
|
|
?.copyWith(color: Theme.of(context).hintColor),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLocoProgressCard(
|
|
BuildContext context,
|
|
LocoClearanceProgress progress,
|
|
) {
|
|
final tierIcons = progress.awardedTiers
|
|
.map((tier) => _buildTierIcon(tier, size: 18))
|
|
.whereType<Widget>()
|
|
.toList();
|
|
final reachedTopTier = progress.nextTier.isEmpty;
|
|
final pct = progress.percent.clamp(0, 100);
|
|
final nextTier = progress.nextTier.isNotEmpty
|
|
? progress.nextTier[0].toUpperCase() + progress.nextTier.substring(1)
|
|
: 'Next';
|
|
final loco = progress.loco;
|
|
final title = [
|
|
if (loco.number.isNotEmpty) loco.number,
|
|
if (loco.locoClass.isNotEmpty) loco.locoClass,
|
|
].join(' • ');
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(10.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
title.isNotEmpty ? title : 'Loco',
|
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
if (tierIcons.isNotEmpty)
|
|
Row(
|
|
children: tierIcons
|
|
.expand((icon) sync* {
|
|
yield icon;
|
|
yield const SizedBox(width: 4);
|
|
})
|
|
.toList()
|
|
..removeLast(),
|
|
),
|
|
],
|
|
),
|
|
if ((loco.name ?? '').isNotEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 2.0),
|
|
child: Text(
|
|
loco.name ?? '',
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
),
|
|
if (!reachedTopTier) ...[
|
|
const SizedBox(height: 6),
|
|
LinearProgressIndicator(
|
|
value: progress.required == 0 ? 0 : pct / 100,
|
|
minHeight: 6,
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4.0),
|
|
child: Text(
|
|
'${pct.toStringAsFixed(0)}% to $nextTier award',
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLoadMoreButton(
|
|
BuildContext context,
|
|
bool isLoading,
|
|
Future<void> Function() onPressed,
|
|
) {
|
|
return Align(
|
|
alignment: Alignment.center,
|
|
child: OutlinedButton.icon(
|
|
onPressed: isLoading
|
|
? null
|
|
: () {
|
|
onPressed();
|
|
},
|
|
icon: isLoading
|
|
? const SizedBox(
|
|
height: 18,
|
|
width: 18,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Icon(Icons.expand_more),
|
|
label: Text(isLoading ? 'Loading...' : 'Load more'),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLoadingIndicator() {
|
|
return const Center(
|
|
child: SizedBox(
|
|
height: 24,
|
|
width: 24,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCountChip(BuildContext context, int count) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(999),
|
|
),
|
|
child: Text(
|
|
'$count',
|
|
style: Theme.of(context)
|
|
.textTheme
|
|
.labelMedium
|
|
?.copyWith(fontWeight: FontWeight.w700),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _loadMoreClassProgress() {
|
|
final data = context.read<DataService>();
|
|
if (data.isClassClearanceProgressLoading || _loadingClassProgress) {
|
|
return Future.value();
|
|
}
|
|
setState(() => _loadingClassProgress = true);
|
|
return data
|
|
.fetchClassClearanceProgress(
|
|
offset: data.classClearanceProgress.length,
|
|
append: true,
|
|
)
|
|
.whenComplete(() {
|
|
if (mounted) setState(() => _loadingClassProgress = false);
|
|
});
|
|
}
|
|
|
|
Future<void> _loadMoreLocoProgress() {
|
|
final data = context.read<DataService>();
|
|
if (data.isLocoClearanceProgressLoading || _loadingLocoProgress) {
|
|
return Future.value();
|
|
}
|
|
setState(() => _loadingLocoProgress = true);
|
|
return data
|
|
.fetchLocoClearanceProgress(
|
|
offset: data.locoClearanceProgress.length,
|
|
append: true,
|
|
)
|
|
.whenComplete(() {
|
|
if (mounted) setState(() => _loadingLocoProgress = false);
|
|
});
|
|
}
|
|
|
|
Future<void> _loadMoreAwards() {
|
|
final data = context.read<DataService>();
|
|
if (data.isBadgeAwardsLoading || _loadingAwards) return Future.value();
|
|
setState(() => _loadingAwards = true);
|
|
return data
|
|
.fetchBadgeAwards(
|
|
offset: data.badgeAwards.length,
|
|
append: true,
|
|
badgeCode: 'class_clearance',
|
|
limit: 20,
|
|
)
|
|
.whenComplete(() {
|
|
if (mounted) setState(() => _loadingAwards = false);
|
|
});
|
|
}
|
|
|
|
String _formatAwardDate(DateTime date) {
|
|
final y = date.year.toString().padLeft(4, '0');
|
|
final m = date.month.toString().padLeft(2, '0');
|
|
final d = date.day.toString().padLeft(2, '0');
|
|
return '$y-$m-$d';
|
|
}
|
|
|
|
Widget? _buildTierIcon(String tier, {double size = 24}) {
|
|
final lower = tier.toLowerCase();
|
|
Color? color;
|
|
switch (lower) {
|
|
case 'bronze':
|
|
color = const Color(0xFFCD7F32);
|
|
break;
|
|
case 'silver':
|
|
color = const Color(0xFFC0C0C0);
|
|
break;
|
|
case 'gold':
|
|
color = const Color(0xFFFFD700);
|
|
break;
|
|
}
|
|
if (color == null) return null;
|
|
return Icon(Icons.emoji_events, color: color, size: size);
|
|
}
|
|
|
|
String? _scopeToShow(BadgeAward award) {
|
|
final scope = award.scopeValue?.trim() ?? '';
|
|
if (scope.isEmpty) return null;
|
|
final code = award.badgeCode.toLowerCase();
|
|
if (code == 'loco_clearance') {
|
|
// Hide numeric loco IDs; loco details are shown separately.
|
|
if (int.tryParse(scope) != null) return null;
|
|
}
|
|
return scope;
|
|
}
|
|
}
|