Files
minstrel/flutter_client/lib/admin/admin_providers.dart
T
bvandeusen bd1bcab12b feat(flutter/admin): requests screen with approve/reject
AdminRequestsScreen renders rows from adminRequestsProvider, joining
requester user_id → username via adminUsersProvider for display
(falls back to UUID prefix when the users list hasn't loaded yet).
Approve fires immediately; Reject confirms via dialog. Both are
optimistic with rollback in the controller.

Adds AdminUsersController build-only (the read side) so the row join
works; mutation methods land in Slice 3 alongside the Users screen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 21:54:58 -04:00

109 lines
3.4 KiB
Dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../api/endpoints/admin_invites.dart';
import '../api/endpoints/admin_quarantine.dart';
import '../api/endpoints/admin_requests.dart';
import '../api/endpoints/admin_users.dart';
import '../library/library_providers.dart' show dioProvider;
import '../models/admin_request.dart';
import '../models/admin_user.dart';
final adminRequestsApiProvider = FutureProvider<AdminRequestsApi>((ref) async {
return AdminRequestsApi(await ref.watch(dioProvider.future));
});
final adminQuarantineApiProvider =
FutureProvider<AdminQuarantineApi>((ref) async {
return AdminQuarantineApi(await ref.watch(dioProvider.future));
});
final adminUsersApiProvider = FutureProvider<AdminUsersApi>((ref) async {
return AdminUsersApi(await ref.watch(dioProvider.future));
});
final adminInvitesApiProvider = FutureProvider<AdminInvitesApi>((ref) async {
return AdminInvitesApi(await ref.watch(dioProvider.future));
});
class AdminCounts {
const AdminCounts({
required this.requests,
required this.quarantine,
required this.users,
});
final int requests;
final int quarantine;
final int users;
}
/// Fans out to the three list endpoints in parallel and rolls them up
/// into `{requests, quarantine, users}` for the landing screen badges.
final adminCountsProvider = FutureProvider<AdminCounts>((ref) async {
final requestsApi = await ref.watch(adminRequestsApiProvider.future);
final quarantineApi = await ref.watch(adminQuarantineApiProvider.future);
final usersApi = await ref.watch(adminUsersApiProvider.future);
final results = await Future.wait([
requestsApi.list(),
quarantineApi.list(),
usersApi.list(),
]);
return AdminCounts(
requests: (results[0] as List).length,
quarantine: (results[1] as List).length,
users: (results[2] as List).length,
);
});
class AdminRequestsController extends AsyncNotifier<List<AdminRequest>> {
@override
Future<List<AdminRequest>> build() async {
final api = await ref.watch(adminRequestsApiProvider.future);
return api.list();
}
Future<void> approve(String id) async {
await _decide(id, (api) => api.approve(id));
}
Future<void> reject(String id) async {
await _decide(id, (api) => api.reject(id));
}
Future<void> _decide(
String id,
Future<void> Function(AdminRequestsApi api) action,
) async {
final api = await ref.read(adminRequestsApiProvider.future);
final current = state.value ?? const <AdminRequest>[];
state = AsyncData(current.where((r) => r.id != id).toList());
try {
await action(api);
// Refresh landing badge.
ref.invalidate(adminCountsProvider);
} catch (e, st) {
state = AsyncData(current);
Error.throwWithStackTrace(e, st);
}
}
}
final adminRequestsProvider =
AsyncNotifierProvider<AdminRequestsController, List<AdminRequest>>(
AdminRequestsController.new);
/// Read side defined here so the Requests screen can join requester
/// UUID → username for display. Mutation methods (setAdmin,
/// setAutoApprove, resetPassword, delete) are appended in Slice 3
/// alongside the Users screen.
class AdminUsersController extends AsyncNotifier<List<AdminUser>> {
@override
Future<List<AdminUser>> build() async {
final api = await ref.watch(adminUsersApiProvider.future);
return api.list();
}
}
final adminUsersProvider =
AsyncNotifierProvider<AdminUsersController, List<AdminUser>>(
AdminUsersController.new);