feat: enhance admin and profile repositories with token handling; update dropdown initial values in various screens

This commit is contained in:
Nils-Johan Gynther
2026-04-23 21:34:08 +02:00
parent 111d196403
commit b589f7415d
15 changed files with 63 additions and 36 deletions
@@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/api/api_client.dart'; import '../../../core/api/api_client.dart';
import '../../../core/api/api_paths.dart'; import '../../../core/api/api_paths.dart';
import '../../../core/api/guarded_api_call.dart'; import '../../../core/api/guarded_api_call.dart';
import '../../auth/data/auth_providers.dart';
import '../domain/user_admin.dart'; import '../domain/user_admin.dart';
final adminRepositoryProvider = Provider<AdminRepository>((ref) { final adminRepositoryProvider = Provider<AdminRepository>((ref) {
@@ -14,26 +15,31 @@ class AdminRepository {
AdminRepository(this._apiClient, this._ref); AdminRepository(this._apiClient, this._ref);
Future<String?> _token() => _ref.read(authStateProvider.future);
Future<List<UserAdmin>> listUsers() async { Future<List<UserAdmin>> listUsers() async {
final token = await _token();
final data = await guardedApiCall( final data = await guardedApiCall(
_ref, _ref,
() => _apiClient.getJson(UserApiPaths.list), () => _apiClient.getJson(UserApiPaths.list, token: token),
); );
return (data as List<dynamic>).map((e) => UserAdmin.fromJson(e as Map<String, dynamic>)).toList(); return (data as List<dynamic>).map((e) => UserAdmin.fromJson(e as Map<String, dynamic>)).toList();
} }
Future<UserAdmin> setRole(int userId, String newRole) async { Future<UserAdmin> setRole(int userId, String newRole) async {
final token = await _token();
final data = await guardedApiCall( final data = await guardedApiCall(
_ref, _ref,
() => _apiClient.patchJson(UserApiPaths.setRole(userId), body: {'role': newRole}), () => _apiClient.patchJson(UserApiPaths.setRole(userId), body: {'role': newRole}, token: token),
); );
return UserAdmin.fromJson(data); return UserAdmin.fromJson(data);
} }
Future<UserAdmin> setPremium(int userId, {required bool isPremium}) async { Future<UserAdmin> setPremium(int userId, {required bool isPremium}) async {
final token = await _token();
final data = await guardedApiCall( final data = await guardedApiCall(
_ref, _ref,
() => _apiClient.patchJson(UserApiPaths.setPremium(userId), body: {'isPremium': isPremium}), () => _apiClient.patchJson(UserApiPaths.setPremium(userId), body: {'isPremium': isPremium}, token: token),
); );
return UserAdmin.fromJson(data); return UserAdmin.fromJson(data);
} }
@@ -44,6 +50,7 @@ class AdminRepository {
required String password, required String password,
String role = 'user', String role = 'user',
}) async { }) async {
final token = await _token();
final data = await guardedApiCall( final data = await guardedApiCall(
_ref, _ref,
() => _apiClient.postJson(UserApiPaths.list, body: { () => _apiClient.postJson(UserApiPaths.list, body: {
@@ -51,21 +58,25 @@ class AdminRepository {
'email': email, 'email': email,
'password': password, 'password': password,
'role': role, 'role': role,
}), }, token: token),
); );
return UserAdmin.fromJson(data as Map<String, dynamic>); return UserAdmin.fromJson(data as Map<String, dynamic>);
} }
Future<void> deleteUser(int userId) => guardedApiCall( Future<void> deleteUser(int userId) async {
final token = await _token();
return guardedApiCall(
_ref, _ref,
() => _apiClient.deleteJson(UserApiPaths.delete(userId)), () => _apiClient.deleteJson(UserApiPaths.delete(userId), token: token),
); );
}
/// Returns `{ temporaryPassword, to, subject, body }`. /// Returns `{ temporaryPassword, to, subject, body }`.
Future<Map<String, dynamic>> resetPassword(int userId) async { Future<Map<String, dynamic>> resetPassword(int userId) async {
final token = await _token();
final result = await guardedApiCall<dynamic>( final result = await guardedApiCall<dynamic>(
_ref, _ref,
() => _apiClient.postJson(UserApiPaths.resetPassword(userId)), () => _apiClient.postJson(UserApiPaths.resetPassword(userId), token: token),
); );
return (result as Map<String, dynamic>); return (result as Map<String, dynamic>);
} }
@@ -379,7 +379,7 @@ class _CreateUserDialogState extends State<_CreateUserDialog> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: _role, initialValue: _role,
decoration: const InputDecoration(labelText: 'Roll'), decoration: const InputDecoration(labelText: 'Roll'),
items: const [ items: const [
DropdownMenuItem(value: 'user', child: Text('Användare')), DropdownMenuItem(value: 'user', child: Text('Användare')),
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart'; import '../../../core/api/api_error_mapper.dart';
import '../../../core/l10n/l10n.dart'; import '../../../core/l10n/l10n.dart';
@@ -19,6 +19,10 @@ class _StringNotifier extends Notifier<String> {
final String _initial; final String _initial;
@override @override
String build() => _initial; String build() => _initial;
void setValue(String value) {
state = value;
}
} }
final inventoryLocationFilterProvider = final inventoryLocationFilterProvider =
@@ -196,7 +196,7 @@ class _CreateInventoryScreenState
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: DropdownButtonFormField<String>( child: DropdownButtonFormField<String>(
value: _unitController.text.trim().isEmpty initialValue: _unitController.text.trim().isEmpty
? null ? null
: _unitController.text.trim(), : _unitController.text.trim(),
isExpanded: true, isExpanded: true,
@@ -224,7 +224,7 @@ class _CreateInventoryScreenState
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: _locationController.text.trim().isEmpty initialValue: _locationController.text.trim().isEmpty
? null ? null
: _locationController.text.trim(), : _locationController.text.trim(),
isExpanded: true, isExpanded: true,
@@ -175,7 +175,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: DropdownButtonFormField<String>( child: DropdownButtonFormField<String>(
value: _unitController.text.trim().isEmpty initialValue: _unitController.text.trim().isEmpty
? null ? null
: _unitController.text.trim(), : _unitController.text.trim(),
isExpanded: true, isExpanded: true,
@@ -204,7 +204,7 @@ class _InventoryEditScreenState extends ConsumerState<InventoryEditScreen> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: _locationController.text.trim().isEmpty initialValue: _locationController.text.trim().isEmpty
? null ? null
: _locationController.text.trim(), : _locationController.text.trim(),
isExpanded: true, isExpanded: true,
@@ -4,9 +4,7 @@ import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart'; import '../../../core/api/api_error_mapper.dart';
import '../../../core/ui/async_state_views.dart'; import '../../../core/ui/async_state_views.dart';
import '../../auth/data/auth_providers.dart';
import '../data/inventory_providers.dart'; import '../data/inventory_providers.dart';
import '../domain/inventory_item.dart';
import 'swipeable_inventory_tile.dart'; import 'swipeable_inventory_tile.dart';
class InventoryScreen extends ConsumerWidget { class InventoryScreen extends ConsumerWidget {
@@ -53,14 +51,14 @@ class InventoryScreen extends ConsumerWidget {
selected: location == option, selected: location == option,
onSelected: (_) => ref onSelected: (_) => ref
.read(inventoryLocationFilterProvider.notifier) .read(inventoryLocationFilterProvider.notifier)
.state = option, .setValue(option),
), ),
) )
.toList(), .toList(),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: sort, initialValue: sort,
isExpanded: true, isExpanded: true,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Sortering', labelText: 'Sortering',
@@ -75,8 +73,9 @@ class InventoryScreen extends ConsumerWidget {
) )
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
ref.read(inventorySortFilterProvider.notifier).state = ref
value ?? ''; .read(inventorySortFilterProvider.notifier)
.setValue(value ?? '');
}, },
), ),
], ],
@@ -16,6 +16,18 @@ class _IntNotifier extends Notifier<int> {
final int _initial; final int _initial;
@override @override
int build() => _initial; int build() => _initial;
void increment() {
state = state + 1;
}
void decrement() {
state = state - 1;
}
void reset() {
state = 0;
}
} }
final mealPlanWeekOffsetProvider = final mealPlanWeekOffsetProvider =
@@ -19,7 +19,7 @@ class MealPlanRepository {
message: 'Ogiltigt svar från servern.', message: 'Ogiltigt svar från servern.',
); );
} }
return (data as List) return data
.map((item) => MealPlanEntry.fromJson(item as Map<String, dynamic>)) .map((item) => MealPlanEntry.fromJson(item as Map<String, dynamic>))
.toList(); .toList();
} on ApiException { } on ApiException {
@@ -41,7 +41,7 @@ class MealPlanRepository {
message: 'Ogiltigt svar från servern.', message: 'Ogiltigt svar från servern.',
); );
} }
return (data as List) return data
.map((item) => ShoppingItem.fromJson(item as Map<String, dynamic>)) .map((item) => ShoppingItem.fromJson(item as Map<String, dynamic>))
.toList(); .toList();
} on ApiException { } on ApiException {
@@ -63,7 +63,7 @@ class MealPlanRepository {
message: 'Ogiltigt svar från servern.', message: 'Ogiltigt svar från servern.',
); );
} }
return (data as List) return data
.map((item) => InventoryCompareItem.fromJson(item as Map<String, dynamic>)) .map((item) => InventoryCompareItem.fromJson(item as Map<String, dynamic>))
.toList(); .toList();
} on ApiException { } on ApiException {
@@ -114,19 +114,19 @@ class _MealPlanScreenState extends ConsumerState<MealPlanScreen> {
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: [ children: [
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () => ref.read(mealPlanWeekOffsetProvider.notifier).state--, onPressed: () => ref.read(mealPlanWeekOffsetProvider.notifier).decrement(),
icon: const Icon(Icons.chevron_left), icon: const Icon(Icons.chevron_left),
label: Text(l10n.mealPlanWeekPrevious), label: Text(l10n.mealPlanWeekPrevious),
), ),
Chip(label: Text(weekLabel)), Chip(label: Text(weekLabel)),
OutlinedButton.icon( OutlinedButton.icon(
onPressed: () => ref.read(mealPlanWeekOffsetProvider.notifier).state++, onPressed: () => ref.read(mealPlanWeekOffsetProvider.notifier).increment(),
icon: const Icon(Icons.chevron_right), icon: const Icon(Icons.chevron_right),
label: Text(l10n.mealPlanWeekNext), label: Text(l10n.mealPlanWeekNext),
), ),
if (ref.watch(mealPlanWeekOffsetProvider) != 0) if (ref.watch(mealPlanWeekOffsetProvider) != 0)
TextButton( TextButton(
onPressed: () => ref.read(mealPlanWeekOffsetProvider.notifier).state = 0, onPressed: () => ref.read(mealPlanWeekOffsetProvider.notifier).reset(),
child: Text(l10n.mealPlanWeekCurrent), child: Text(l10n.mealPlanWeekCurrent),
), ),
], ],
@@ -203,7 +203,7 @@ class _DayCard extends StatelessWidget {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: selectedValue, initialValue: selectedValue,
isExpanded: true, isExpanded: true,
decoration: InputDecoration( decoration: InputDecoration(
labelText: l10n.mealPlanSelectRecipe, labelText: l10n.mealPlanSelectRecipe,
@@ -248,7 +248,7 @@ class _DayCard extends StatelessWidget {
SizedBox( SizedBox(
width: 220, width: 220,
child: DropdownButtonFormField<int>( child: DropdownButtonFormField<int>(
value: currentServings, initialValue: currentServings,
decoration: InputDecoration( decoration: InputDecoration(
labelText: l10n.mealPlanServingsLabel, labelText: l10n.mealPlanServingsLabel,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
@@ -51,7 +51,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: selectedUnit, initialValue: selectedUnit,
isExpanded: true, isExpanded: true,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Enhet', labelText: 'Enhet',
@@ -72,7 +72,7 @@ class _PantryScreenState extends ConsumerState<PantryScreen> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: selectedLocation, initialValue: selectedLocation,
isExpanded: true, isExpanded: true,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Plats (valfri)', labelText: 'Plats (valfri)',
@@ -16,9 +16,10 @@ class ProfileRepository {
ProfileRepository(this._apiClient, this._ref); ProfileRepository(this._apiClient, this._ref);
Future<UserProfile> getMe() async { Future<UserProfile> getMe() async {
final token = await _ref.read(authStateProvider.future);
final data = await guardedApiCall( final data = await guardedApiCall(
_ref, _ref,
() => _apiClient.getJson(UserApiPaths.me), () => _apiClient.getJson(UserApiPaths.me, token: token),
); );
return UserProfile.fromJson(data); return UserProfile.fromJson(data);
} }
@@ -28,6 +29,7 @@ class ProfileRepository {
String? firstName, String? firstName,
String? lastName, String? lastName,
}) async { }) async {
final token = await _ref.read(authStateProvider.future);
final body = <String, dynamic>{ final body = <String, dynamic>{
if (email != null) 'email': email, if (email != null) 'email': email,
if (firstName != null) 'firstName': firstName, if (firstName != null) 'firstName': firstName,
@@ -35,7 +37,7 @@ class ProfileRepository {
}; };
final data = await guardedApiCall( final data = await guardedApiCall(
_ref, _ref,
() => _apiClient.patchJson(UserApiPaths.me, body: body), () => _apiClient.patchJson(UserApiPaths.me, body: body, token: token),
); );
return UserProfile.fromJson(data); return UserProfile.fromJson(data);
} }
@@ -17,7 +17,7 @@ class RecipeRepository {
throw const ApiException( throw const ApiException(
type: ApiErrorType.unknown, message: 'Ogiltigt svar från servern.'); type: ApiErrorType.unknown, message: 'Ogiltigt svar från servern.');
} }
return (data as List) return data
.map((e) => Recipe.fromJson(e as Map<String, dynamic>)) .map((e) => Recipe.fromJson(e as Map<String, dynamic>))
.toList(); .toList();
} on ApiException { } on ApiException {
@@ -399,7 +399,7 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
], ],
), ),
DropdownButtonFormField<int>( DropdownButtonFormField<int>(
value: ingredient.productId, initialValue: ingredient.productId,
isExpanded: true, isExpanded: true,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Produkt *', labelText: 'Produkt *',
@@ -461,7 +461,7 @@ class _RecipeEditScreenState extends ConsumerState<RecipeEditScreen> {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: DropdownButtonFormField<String>( child: DropdownButtonFormField<String>(
value: ingredient.unit.trim().isEmpty ? null : ingredient.unit, initialValue: ingredient.unit.trim().isEmpty ? null : ingredient.unit,
isExpanded: true, isExpanded: true,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Enhet *', labelText: 'Enhet *',