feat: add meal planning feature with API integration
- Introduced MealPlanApiPaths for handling meal plan related API endpoints. - Added MealPlanScreen for displaying and managing meal plans. - Implemented MealPlanRepository for fetching and updating meal plan data. - Created data models: MealPlanEntry, MealPlanRecipe, InventoryCompareItem, ShoppingItem, and MealPlanDashboard. - Integrated meal plan functionality into the app router and UI. - Updated localization files for meal plan related strings in English and Swedish. - Added state management for meal plan using Riverpod.
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/api/api_providers.dart';
|
||||
import '../../../core/api/guarded_api_call.dart';
|
||||
import '../../auth/data/auth_providers.dart';
|
||||
import '../domain/meal_plan_dashboard.dart';
|
||||
import '../domain/meal_plan_week.dart';
|
||||
import 'meal_plan_repository.dart';
|
||||
|
||||
final mealPlanRepositoryProvider = Provider<MealPlanRepository>((ref) {
|
||||
return MealPlanRepository(ref.watch(apiClientProvider));
|
||||
});
|
||||
|
||||
final mealPlanWeekOffsetProvider = StateProvider<int>((ref) => 0);
|
||||
|
||||
final mealPlanWeekProvider = Provider<MealPlanWeek>((ref) {
|
||||
final offset = ref.watch(mealPlanWeekOffsetProvider);
|
||||
return MealPlanWeek.fromOffset(offset);
|
||||
});
|
||||
|
||||
final mealPlanDashboardProvider = FutureProvider<MealPlanDashboard>((ref) async {
|
||||
final week = ref.watch(mealPlanWeekProvider);
|
||||
final token = await ref.watch(authStateProvider.future);
|
||||
|
||||
return guardedApiCall(ref, () async {
|
||||
final repository = ref.read(mealPlanRepositoryProvider);
|
||||
final entries = await repository.fetchEntries(week.fromIso, week.toIso, token: token);
|
||||
final shoppingItems = await repository.fetchShoppingList(week.fromIso, week.toIso, token: token);
|
||||
final inventoryCompareItems = await repository.fetchInventoryCompare(
|
||||
week.fromIso,
|
||||
week.toIso,
|
||||
token: token,
|
||||
);
|
||||
|
||||
return MealPlanDashboard(
|
||||
entries: entries,
|
||||
shoppingItems: shoppingItems,
|
||||
inventoryCompareItems: inventoryCompareItems,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import '../../../core/api/api_client.dart';
|
||||
import '../../../core/api/api_exception.dart';
|
||||
import '../../../core/api/api_paths.dart';
|
||||
import '../domain/inventory_compare_item.dart';
|
||||
import '../domain/meal_plan_entry.dart';
|
||||
import '../domain/shopping_item.dart';
|
||||
|
||||
class MealPlanRepository {
|
||||
final ApiClient _api;
|
||||
|
||||
const MealPlanRepository(this._api);
|
||||
|
||||
Future<List<MealPlanEntry>> fetchEntries(String from, String to, {String? token}) async {
|
||||
try {
|
||||
final data = await _api.getJson(MealPlanApiPaths.listByRange(from, to), token: token);
|
||||
if (data is! List) {
|
||||
throw const ApiException(
|
||||
type: ApiErrorType.unknown,
|
||||
message: 'Ogiltigt svar från servern.',
|
||||
);
|
||||
}
|
||||
return data
|
||||
.map((item) => MealPlanEntry.fromJson(item as Map<String, dynamic>))
|
||||
.toList();
|
||||
} on ApiException {
|
||||
rethrow;
|
||||
} catch (_) {
|
||||
throw const ApiException(
|
||||
type: ApiErrorType.network,
|
||||
message: 'Kunde inte hämta matsedeln.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<ShoppingItem>> fetchShoppingList(String from, String to, {String? token}) async {
|
||||
try {
|
||||
final data = await _api.getJson(MealPlanApiPaths.shoppingList(from, to), token: token);
|
||||
if (data is! List) {
|
||||
throw const ApiException(
|
||||
type: ApiErrorType.unknown,
|
||||
message: 'Ogiltigt svar från servern.',
|
||||
);
|
||||
}
|
||||
return data
|
||||
.map((item) => ShoppingItem.fromJson(item as Map<String, dynamic>))
|
||||
.toList();
|
||||
} on ApiException {
|
||||
rethrow;
|
||||
} catch (_) {
|
||||
throw const ApiException(
|
||||
type: ApiErrorType.network,
|
||||
message: 'Kunde inte hämta inköpslistan.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<InventoryCompareItem>> fetchInventoryCompare(String from, String to, {String? token}) async {
|
||||
try {
|
||||
final data = await _api.getJson(MealPlanApiPaths.inventoryCompare(from, to), token: token);
|
||||
if (data is! List) {
|
||||
throw const ApiException(
|
||||
type: ApiErrorType.unknown,
|
||||
message: 'Ogiltigt svar från servern.',
|
||||
);
|
||||
}
|
||||
return data
|
||||
.map((item) => InventoryCompareItem.fromJson(item as Map<String, dynamic>))
|
||||
.toList();
|
||||
} on ApiException {
|
||||
rethrow;
|
||||
} catch (_) {
|
||||
throw const ApiException(
|
||||
type: ApiErrorType.network,
|
||||
message: 'Kunde inte hämta lagerjämförelsen.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<MealPlanEntry> upsert({
|
||||
required String date,
|
||||
required int recipeId,
|
||||
int? servings,
|
||||
String? token,
|
||||
}) async {
|
||||
try {
|
||||
final data = await _api.postJson(
|
||||
MealPlanApiPaths.list,
|
||||
body: {
|
||||
'date': date,
|
||||
'recipeId': recipeId,
|
||||
'servings': servings,
|
||||
},
|
||||
token: token,
|
||||
);
|
||||
if (data is! Map<String, dynamic>) {
|
||||
throw const ApiException(
|
||||
type: ApiErrorType.unknown,
|
||||
message: 'Ogiltigt svar från servern.',
|
||||
);
|
||||
}
|
||||
return MealPlanEntry.fromJson(data);
|
||||
} on ApiException {
|
||||
rethrow;
|
||||
} catch (_) {
|
||||
throw const ApiException(
|
||||
type: ApiErrorType.network,
|
||||
message: 'Kunde inte spara matsedeln.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteByDate(String date, {String? token}) async {
|
||||
try {
|
||||
await _api.deleteJson(MealPlanApiPaths.removeByDate(date), token: token);
|
||||
} on ApiException {
|
||||
rethrow;
|
||||
} catch (_) {
|
||||
throw const ApiException(
|
||||
type: ApiErrorType.network,
|
||||
message: 'Kunde inte ta bort dagens planering.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user