feat(notifications): category list with tap-to-filter navigation

This commit is contained in:
2026-04-22 20:29:49 +02:00
parent 6ff11b8c1e
commit 1961be3805

View File

@@ -5,6 +5,7 @@ import 'package:control_panel/src/features/notifications/presentation/widgets/no
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:legacy_ui/legacy_ui.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_shared/sf_shared.dart';
@@ -19,13 +20,14 @@ class NotificationsScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final primaryColor = context.sfColors.legacyPrimary;
final selectedType = ref.watch(notificationsFilterProvider);
final feedAsync = ref.watch(notificationsFeedProvider);
ref.listen(notificationsFeedProvider, (prev, next) async {
if (next.hasError && prev?.hasError != true) {
await showErrorDialog(context, I18n.alertsLoadError);
}
});
if (selectedType != null) {
return _FilteredNotificationsScreen(
type: selectedType,
onBack: () =>
ref.read(notificationsFilterProvider.notifier).select(null),
);
}
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
@@ -53,81 +55,176 @@ class NotificationsScreen extends ConsumerWidget {
),
),
),
body: Column(
children: [
_FilterChips(
selectedType: selectedType,
onSelected: ref.read(notificationsFilterProvider.notifier).select,
),
Expanded(
child: feedAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => Center(
child: Text(context.translate(I18n.alertsLoadError)),
body: SingleChildScrollView(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 14, big: 12),
vertical: SizeUtils.getByScreen(small: 12, big: 10),
),
child: Column(
children: _notificationTypes.map((entry) {
return Padding(
padding: EdgeInsets.only(
bottom: SizeUtils.getByScreen(small: 8, big: 7),
),
data: (feed) => _NotificationsList(
notifications: feed.notifications,
isLoadingMore: feed.isLoadingMore,
child: SectionButton(
onPressed: () => ref
.read(notificationsFilterProvider.notifier)
.select(entry.type),
icon: Icon(
entry.icon,
size: SizeUtils.getByScreen(small: 34, big: 38),
color: primaryColor,
),
iconPadding: SizeUtils.getByScreen(small: 14, big: 12),
body: Text(
context.translate(entry.labelKey),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 16, big: 17),
fontWeight: FontWeight.w500,
),
),
),
),
),
],
);
}).toList(),
),
),
);
}
}
class _FilterChips extends StatelessWidget {
final String? selectedType;
final ValueChanged<String?> onSelected;
class _NotificationType {
final String type;
final String labelKey;
final IconData icon;
const _FilterChips({required this.selectedType, required this.onSelected});
const _NotificationType({
required this.type,
required this.labelKey,
required this.icon,
});
}
const _notificationTypes = [
_NotificationType(
type: 'sos',
labelKey: I18n.alertTypeSos,
icon: Icons.sos,
),
_NotificationType(
type: 'falldown',
labelKey: I18n.alertTypeFalldown,
icon: Icons.personal_injury,
),
_NotificationType(
type: 'lowBattery',
labelKey: I18n.alertTypeLowBattery,
icon: Icons.battery_alert,
),
_NotificationType(
type: 'disconnect',
labelKey: I18n.alertTypeDisconnect,
icon: Icons.link_off,
),
_NotificationType(
type: 'geofenceIn',
labelKey: I18n.alertTypeGeofenceIn,
icon: Icons.location_on,
),
_NotificationType(
type: 'geofenceOut',
labelKey: I18n.alertTypeGeofenceOut,
icon: Icons.location_off,
),
_NotificationType(
type: 'braceletRemoved',
labelKey: I18n.alertTypeBraceletRemoved,
icon: Icons.watch_off,
),
_NotificationType(
type: 'abnormalHeartRate',
labelKey: I18n.alertTypeAbnormalHeartRate,
icon: Icons.heart_broken,
),
_NotificationType(
type: 'standstill',
labelKey: I18n.alertTypeStandstill,
icon: Icons.accessibility_new,
),
_NotificationType(
type: 'movement',
labelKey: I18n.alertTypeMovement,
icon: Icons.directions_walk,
),
];
class _FilteredNotificationsScreen extends ConsumerWidget {
final String type;
final VoidCallback onBack;
const _FilteredNotificationsScreen({
required this.type,
required this.onBack,
});
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final primaryColor = context.sfColors.legacyPrimary;
final feedAsync = ref.watch(notificationsFeedProvider);
final filters = [
(null, context.translate(I18n.alertsFilterAll)),
('sos', 'SOS'),
('falldown', context.translate(I18n.alertTypeFalldown)),
('lowBattery', context.translate(I18n.alertTypeLowBattery)),
('disconnect', context.translate(I18n.alertTypeDisconnect)),
('geofenceIn', context.translate(I18n.alertTypeGeofenceIn)),
('geofenceOut', context.translate(I18n.alertTypeGeofenceOut)),
];
final typeEntry = _notificationTypes
.where((e) => e.type == type)
.firstOrNull;
final title = typeEntry != null
? context.translate(typeEntry.labelKey)
: type;
return SizedBox(
height: SizeUtils.getByScreen<double>(small: 44, big: 40),
child: ListView.separated(
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
vertical: SizeUtils.getByScreen(small: 4, big: 3),
),
itemCount: filters.length,
separatorBuilder: (_, __) => SizedBox(
width: SizeUtils.getByScreen(small: 6, big: 5),
),
itemBuilder: (context, index) {
final (type, label) = filters[index];
final isSelected = type == selectedType;
ref.listen(notificationsFeedProvider, (prev, next) async {
if (next.hasError && prev?.hasError != true) {
await showErrorDialog(context, I18n.alertsLoadError);
}
});
return ChoiceChip(
label: Text(label),
selected: isSelected,
selectedColor: primaryColor,
labelStyle: TextStyle(
fontSize: SizeUtils.getByScreen(small: 12, big: 11),
fontWeight: FontWeight.w600,
color: isSelected ? Colors.white : primaryColor,
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, _) {
if (!didPop) onBack();
},
child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.surface,
surfaceTintColor: Colors.transparent,
elevation: 0,
centerTitle: true,
automaticallyImplyLeading: false,
leading: IconButton(
onPressed: onBack,
icon: Icon(
Icons.adaptive.arrow_back,
color: primaryColor,
size: SizeUtils.getByScreen(small: 32, big: 28),
),
side: BorderSide(color: primaryColor),
onSelected: (_) => onSelected(type),
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
},
),
title: Text(
title.toUpperCase(),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 20, big: 19),
fontWeight: FontWeight.w500,
letterSpacing: 0,
color: primaryColor,
),
),
),
body: feedAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (_, __) => Center(
child: Text(context.translate(I18n.alertsLoadError)),
),
data: (feed) => _NotificationsList(
notifications: feed.notifications,
isLoadingMore: feed.isLoadingMore,
),
),
),
);
}