feat: enhance admin and profile repositories with token handling; update dropdown initial values in various screens
This commit is contained in:
Binary file not shown.
@@ -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 {
|
||||||
_ref,
|
final token = await _token();
|
||||||
() => _apiClient.deleteJson(UserApiPaths.delete(userId)),
|
return guardedApiCall(
|
||||||
);
|
_ref,
|
||||||
|
() => _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 *',
|
||||||
|
|||||||
Reference in New Issue
Block a user