Files
recipe-app/flutter/lib/features/admin/data/admin_repository.dart
T
Nils-Johan Gynther 7f15f8028b
Test Suite / test (24.15.0) (push) Has been cancelled
feat: update inventory queries to filter by userId for accurate recipe analysis
2026-05-06 10:32:35 +02:00

285 lines
9.9 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/api/api_client.dart';
import '../../../core/api/api_paths.dart';
import '../../../core/api/guarded_api_call.dart';
import '../../auth/data/auth_providers.dart';
import '../domain/admin_ai_categorize_result.dart';
import '../domain/admin_category_node.dart';
import '../domain/admin_product.dart';
import '../domain/ai_model_info.dart';
import '../domain/pending_product.dart';
import '../domain/receipt_alias.dart';
import '../domain/user_admin.dart';
final adminRepositoryProvider = Provider<AdminRepository>((ref) {
return AdminRepository(ref.watch(apiClientProvider), ref);
});
class AdminRepository {
final ApiClient _apiClient;
final Ref _ref;
AdminRepository(this._apiClient, this._ref);
// ── Interna helpers ────────────────────────────────────────────────────────
Future<String?> _token() => _ref.read(authStateProvider.future);
/// GET-anrop som returnerar en typad lista med [fromJson].
Future<List<T>> _getList<T>(
String path,
T Function(Map<String, dynamic>) fromJson, {
bool requiresAuth = true,
}) async {
final token = requiresAuth ? await _token() : null;
final data = await guardedApiCall(
_ref,
() => _apiClient.getJson(path, token: token),
);
return _parseList(data, fromJson);
}
/// POST-anrop som returnerar ett typad objekt med [fromJson].
Future<T> _post<T>(
String path, {
required Map<String, dynamic>? body,
required T Function(dynamic) parse,
bool requiresAuth = true,
}) async {
final token = requiresAuth ? await _token() : null;
final data = await guardedApiCall(
_ref,
() => _apiClient.postJson(path, body: body, token: token),
);
return parse(data);
}
/// PATCH-anrop som returnerar ett typad objekt med [fromJson].
Future<T> _patch<T>(
String path, {
required Map<String, dynamic> body,
required T Function(Map<String, dynamic>) parse,
}) async {
final token = await _token();
final data = await guardedApiCall(
_ref,
() => _apiClient.patchJson(path, body: body, token: token),
);
return parse(Map<String, dynamic>.from(data as Map));
}
/// Fire-and-forget PATCH.
Future<void> _patchVoid(String path, Map<String, dynamic> body) async {
final token = await _token();
await guardedApiCall(
_ref,
() => _apiClient.patchJson(path, body: body, token: token),
);
}
/// Fire-and-forget POST.
Future<void> _postVoid(String path, [Map<String, dynamic>? body]) async {
final token = await _token();
await guardedApiCall(
_ref,
() => _apiClient.postJson(path, body: body, token: token),
);
}
/// Fire-and-forget DELETE.
Future<void> _deleteVoid(String path) async {
final token = await _token();
await guardedApiCall(
_ref,
() => _apiClient.deleteJson(path, token: token),
);
}
/// Tolerant listparsning — accepterar ren lista eller wrapper ({items, data}).
static List<T> _parseList<T>(
dynamic data,
T Function(Map<String, dynamic>) fromJson,
) {
final List<dynamic> raw;
if (data is List<dynamic>) {
raw = data;
} else if (data is Map<String, dynamic>) {
raw = (data['items'] as List<dynamic>?) ??
(data['data'] as List<dynamic>?) ??
const [];
if (raw.isEmpty && data.isNotEmpty) {
debugPrint('[AdminRepository] Unexpected API wrapper shape: ${data.keys}');
}
} else {
raw = const [];
}
return raw
.whereType<Map>()
.map((e) => fromJson(Map<String, dynamic>.from(e)))
.toList();
}
// ── Användare ──────────────────────────────────────────────────────────────
Future<List<UserAdmin>> listUsers() =>
_getList(UserApiPaths.list, UserAdmin.fromJson);
Future<UserAdmin> setRole(int userId, String newRole) =>
_patch(UserApiPaths.setRole(userId),
body: {'role': newRole}, parse: UserAdmin.fromJson);
Future<UserAdmin> setPremium(int userId, {required bool isPremium}) =>
_patch(UserApiPaths.setPremium(userId),
body: {'isPremium': isPremium}, parse: UserAdmin.fromJson);
Future<UserAdmin> setRecipeSharing(int userId,
{required bool canShareRecipes}) =>
_patch(UserApiPaths.setRecipeSharing(userId),
body: {'canShareRecipes': canShareRecipes}, parse: UserAdmin.fromJson);
Future<void> updateEmail(int userId, String email) =>
_patchVoid(UserApiPaths.updateEmail(userId), {'email': email});
Future<UserAdmin> createUser({
required String username,
required String email,
required String password,
String role = 'user',
}) =>
_post(
UserApiPaths.list,
body: {
'username': username,
'email': email,
'password': password,
'role': role,
},
parse: (d) => UserAdmin.fromJson(d as Map<String, dynamic>),
);
Future<void> deleteUser(int userId) => _deleteVoid(UserApiPaths.delete(userId));
/// Returns `{ temporaryPassword, to, subject, body }`.
Future<Map<String, dynamic>> resetPassword(int userId) =>
_post<Map<String, dynamic>>(
UserApiPaths.resetPassword(userId),
body: null,
parse: (d) => d as Map<String, dynamic>,
);
// ── Produkter ──────────────────────────────────────────────────────────────
Future<List<AdminProduct>> listProducts() =>
_getList(ProductApiPaths.mine, AdminProduct.fromJson);
Future<List<AdminProduct>> listDeletedProducts() =>
_getList(ProductApiPaths.deleted, AdminProduct.fromJson);
Future<List<PendingProduct>> listPendingProducts() =>
_getList(ProductApiPaths.pending, PendingProduct.fromJson);
Future<void> setProductStatus(int productId, String status) =>
_patchVoid(ProductApiPaths.setStatus(productId), {'status': status});
Future<void> setProductCategory(int productId, {required int? categoryId}) =>
_patchVoid(ProductApiPaths.update(productId), {'categoryId': categoryId});
Future<void> removeProduct(int productId) =>
_deleteVoid(ProductApiPaths.remove(productId));
Future<void> restoreProduct(int productId) =>
_postVoid(ProductApiPaths.restore(productId));
/// Skapar en ny aktiv produkt (kräver admin). Returnerar `{id, name, categoryId?}`.
Future<Map<String, dynamic>> createProduct(String name, {int? categoryId}) =>
_post<Map<String, dynamic>>(
ProductApiPaths.list,
body: {
'name': name.trim(),
if (categoryId != null) 'categoryId': categoryId,
},
parse: (d) => d as Map<String, dynamic>,
);
Future<void> bulkSetCategory(List<int> ids, {required int? categoryId}) =>
_postVoid(ProductApiPaths.bulkUpdate, {'ids': ids, 'categoryId': categoryId});
Future<void> mergeProducts({
required int sourceProductId,
required int targetProductId,
}) =>
_postVoid(ProductApiPaths.merge, {
'sourceProductId': sourceProductId,
'targetProductId': targetProductId,
});
Future<Map<String, dynamic>> previewMerge({
required int sourceProductId,
required int targetProductId,
}) async {
final token = await _token();
final data = await guardedApiCall(
_ref,
() => _apiClient.getJson(
ProductApiPaths.mergePreview(sourceProductId, targetProductId),
token: token,
),
);
return Map<String, dynamic>.from(data as Map);
}
Future<List<AdminAiCategorizeResult>> aiCategorizeBulk({
List<int>? productIds,
}) async {
final token = await _token();
final data = await guardedApiCall(
_ref,
() => _apiClient.postJson(
ProductApiPaths.aiCategorizeBulk,
body: (productIds == null || productIds.isEmpty)
? null
: {'productIds': productIds},
token: token,
),
);
return _parseList(data, AdminAiCategorizeResult.fromJson)
.where((e) => e.productId > 0 && e.categoryId > 0)
.toList();
}
// ── Kategorier ─────────────────────────────────────────────────────────────
Future<List<AdminCategoryNode>> listCategoryTree() =>
_getList(
CategoryApiPaths.tree,
AdminCategoryNode.fromJson,
requiresAuth: false,
);
// ── AI-modeller ────────────────────────────────────────────────────────────
/// OBS: endpointen /ai/models kräver autentisering.
Future<List<AiModelInfo>> listAiModels() =>
_getList(AiApiPaths.models, AiModelInfo.fromJson);
// ── Kvittoalias (admin/global fallback) ───────────────────────────────────
Future<List<ReceiptAlias>> listReceiptAliases() =>
_getList(ReceiptAliasApiPaths.list, ReceiptAlias.fromJson);
Future<void> upsertReceiptAlias({
required String receiptName,
required int productId,
bool isGlobal = false,
}) =>
_postVoid(ReceiptAliasApiPaths.list, {
'receiptName': receiptName,
'productId': productId,
'isGlobal': isGlobal,
});
Future<void> removeReceiptAlias(int id) =>
_deleteVoid(ReceiptAliasApiPaths.remove(id));
}