From 6b5d12f3a1461a265494740b8086005e71d504f9 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 8 May 2026 21:53:03 -0400 Subject: [PATCH] feat(flutter/admin): landing screen with section cards + counts ListView of three AdminSectionCard tiles (Requests / Quarantine / Users) showing live counts from adminCountsProvider. Pull-to-refresh re-fans-out to the three list endpoints. Admin/Quarantine/Users sub-routes will land in subsequent commits. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../lib/admin/admin_landing_screen.dart | 66 +++++++++++++++++++ .../lib/admin/widgets/admin_section_card.dart | 53 +++++++++++++++ .../test/admin/admin_landing_screen_test.dart | 46 +++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 flutter_client/lib/admin/admin_landing_screen.dart create mode 100644 flutter_client/lib/admin/widgets/admin_section_card.dart create mode 100644 flutter_client/test/admin/admin_landing_screen_test.dart diff --git a/flutter_client/lib/admin/admin_landing_screen.dart b/flutter_client/lib/admin/admin_landing_screen.dart new file mode 100644 index 00000000..3849c77f --- /dev/null +++ b/flutter_client/lib/admin/admin_landing_screen.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../shared/widgets/main_app_bar_actions.dart'; +import '../theme/theme_extension.dart'; +import 'admin_providers.dart'; +import 'widgets/admin_section_card.dart'; + +class AdminLandingScreen extends ConsumerWidget { + const AdminLandingScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final fs = Theme.of(context).extension()!; + final counts = ref.watch(adminCountsProvider); + return Scaffold( + backgroundColor: fs.obsidian, + appBar: AppBar( + backgroundColor: fs.obsidian, + elevation: 0, + title: Text('Admin', style: TextStyle(color: fs.parchment)), + actions: const [MainAppBarActions(currentRoute: '/admin')], + ), + body: SafeArea( + child: counts.when( + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, _) => Center( + child: Text('$e', style: TextStyle(color: fs.error)), + ), + data: (c) => RefreshIndicator( + onRefresh: () async => ref.refresh(adminCountsProvider.future), + child: ListView( + children: [ + AdminSectionCard( + key: const Key('admin_card_requests'), + icon: Icons.inbox, + title: 'Requests', + subtitle: 'Approve or reject Lidarr requests', + count: c.requests, + onTap: () => context.push('/admin/requests'), + ), + AdminSectionCard( + key: const Key('admin_card_quarantine'), + icon: Icons.warning_amber, + title: 'Quarantine', + subtitle: 'Resolve scan failures', + count: c.quarantine, + onTap: () => context.push('/admin/quarantine'), + ), + AdminSectionCard( + key: const Key('admin_card_users'), + icon: Icons.people, + title: 'Users', + subtitle: 'Manage accounts and invites', + count: c.users, + onTap: () => context.push('/admin/users'), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/flutter_client/lib/admin/widgets/admin_section_card.dart b/flutter_client/lib/admin/widgets/admin_section_card.dart new file mode 100644 index 00000000..919441de --- /dev/null +++ b/flutter_client/lib/admin/widgets/admin_section_card.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +import '../../theme/theme_extension.dart'; + +class AdminSectionCard extends StatelessWidget { + const AdminSectionCard({ + super.key, + required this.icon, + required this.title, + required this.subtitle, + required this.count, + required this.onTap, + }); + + final IconData icon; + final String title; + final String subtitle; + final int count; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final fs = Theme.of(context).extension()!; + return Card( + color: fs.iron, + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: ListTile( + leading: Icon(icon, color: fs.parchment), + title: Text( + title, + style: TextStyle( + color: fs.parchment, + fontFamily: 'Fraunces', + fontSize: 18, + ), + ), + subtitle: Text(subtitle, style: TextStyle(color: fs.ash)), + trailing: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: fs.bronze, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '$count', + style: TextStyle(color: fs.obsidian, fontWeight: FontWeight.w500), + ), + ), + onTap: onTap, + ), + ); + } +} diff --git a/flutter_client/test/admin/admin_landing_screen_test.dart b/flutter_client/test/admin/admin_landing_screen_test.dart new file mode 100644 index 00000000..95eca570 --- /dev/null +++ b/flutter_client/test/admin/admin_landing_screen_test.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:minstrel/admin/admin_landing_screen.dart'; +import 'package:minstrel/admin/admin_providers.dart'; +import 'package:minstrel/auth/auth_provider.dart'; +import 'package:minstrel/models/user.dart'; +import 'package:minstrel/theme/theme_data.dart'; + +class _StubAuth extends AuthController { + @override + Future build() async => + const User(id: 'u1', username: 'admin', isAdmin: true); +} + +void main() { + testWidgets('renders three section cards with counts from provider', + (t) async { + await t.pumpWidget( + ProviderScope( + overrides: [ + authControllerProvider.overrideWith(() => _StubAuth()), + adminCountsProvider.overrideWith( + (_) async => const AdminCounts( + requests: 3, + quarantine: 1, + users: 5, + ), + ), + ], + child: MaterialApp( + theme: buildThemeData(), + home: const AdminLandingScreen(), + ), + ), + ); + await t.pumpAndSettle(); + expect(find.byKey(const Key('admin_card_requests')), findsOneWidget); + expect(find.byKey(const Key('admin_card_quarantine')), findsOneWidget); + expect(find.byKey(const Key('admin_card_users')), findsOneWidget); + expect(find.text('3'), findsOneWidget); + expect(find.text('1'), findsOneWidget); + expect(find.text('5'), findsOneWidget); + }); +}