feat(recipes): add recipe visibility and sharing features

- Implemented functionality to set recipe visibility (public/private) with appropriate checks for user permissions.
- Added ability to share recipes with other users, including validation for existing users and permissions.
- Introduced new DTOs for setting visibility and sharing recipes.
- Updated RecipesController and RecipesService to handle new endpoints for visibility and sharing.
- Enhanced inventory preview to consider user permissions and shared recipes.
- Updated front-end to support new sharing and visibility features, including UI changes for recipe detail and admin user management.
This commit is contained in:
Nils-Johan Gynther
2026-05-02 09:19:59 +02:00
parent f67bf8baef
commit 41ae7d4d06
17 changed files with 742 additions and 124 deletions
@@ -49,6 +49,19 @@ class AdminRepository {
return UserAdmin.fromJson(data);
}
Future<UserAdmin> setRecipeSharing(int userId, {required bool canShareRecipes}) async {
final token = await _token();
final data = await guardedApiCall(
_ref,
() => _apiClient.patchJson(
UserApiPaths.setRecipeSharing(userId),
body: {'canShareRecipes': canShareRecipes},
token: token,
),
);
return UserAdmin.fromJson(data);
}
Future<void> updateEmail(int userId, String email) async {
final token = await _token();
await guardedApiCall(
@@ -7,6 +7,7 @@ class UserAdmin {
final String? lastName;
final String role;
final bool isPremium;
final bool canShareRecipes;
final DateTime? createdAt;
const UserAdmin({
@@ -17,6 +18,7 @@ class UserAdmin {
this.lastName,
required this.role,
required this.isPremium,
required this.canShareRecipes,
this.createdAt,
});
@@ -28,6 +30,7 @@ class UserAdmin {
lastName: json['lastName'] as String?,
role: json['role'] as String? ?? 'user',
isPremium: json['isPremium'] as bool? ?? false,
canShareRecipes: json['canShareRecipes'] as bool? ?? true,
createdAt: json['createdAt'] != null ? DateTime.tryParse(json['createdAt'] as String) : null,
);
@@ -81,6 +81,26 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
}
}
Future<void> _toggleRecipeSharing(UserAdmin user) async {
final newValue = !user.canShareRecipes;
final confirmed = await _confirm(
context,
newValue ? 'Tillåt receptdelning' : 'Blockera receptdelning',
'${newValue ? 'Tillåt' : 'Blockera'} receptdelning för ${user.username}?',
);
if (!confirmed || !mounted) return;
try {
await ref
.read(adminRepositoryProvider)
.setRecipeSharing(user.id, canShareRecipes: newValue);
if (!mounted) return;
_load();
} catch (e) {
if (!mounted) return;
_showError(e);
}
}
Future<void> _resetPassword(UserAdmin user) async {
final confirmed = await _confirm(
context,
@@ -319,6 +339,7 @@ class _AdminUsersPanelState extends ConsumerState<AdminUsersPanel> {
user: _users[i],
onChangeRole: () => _changeRole(_users[i]),
onTogglePremium: () => _togglePremium(_users[i]),
onToggleRecipeSharing: () => _toggleRecipeSharing(_users[i]),
onEditEmail: () => _editEmail(_users[i]),
onResetPassword: () => _resetPassword(_users[i]),
onDelete: () => _deleteUser(_users[i]),
@@ -364,6 +385,7 @@ class _UserTile extends StatelessWidget {
final UserAdmin user;
final VoidCallback onChangeRole;
final VoidCallback onTogglePremium;
final VoidCallback onToggleRecipeSharing;
final VoidCallback onEditEmail;
final VoidCallback onResetPassword;
final VoidCallback onDelete;
@@ -372,6 +394,7 @@ class _UserTile extends StatelessWidget {
required this.user,
required this.onChangeRole,
required this.onTogglePremium,
required this.onToggleRecipeSharing,
required this.onEditEmail,
required this.onResetPassword,
required this.onDelete,
@@ -418,6 +441,16 @@ class _UserTile extends StatelessWidget {
backgroundColor: theme.colorScheme.tertiaryContainer,
),
],
const SizedBox(width: 4),
Chip(
label: Text(user.canShareRecipes ? 'Delning: På' : 'Delning: Av'),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
labelStyle: theme.textTheme.labelSmall,
backgroundColor: user.canShareRecipes
? theme.colorScheme.secondaryContainer
: theme.colorScheme.errorContainer,
),
],
),
],
@@ -432,6 +465,9 @@ class _UserTile extends StatelessWidget {
case 'premium':
onTogglePremium();
break;
case 'sharing':
onToggleRecipeSharing();
break;
case 'email':
onEditEmail();
break;
@@ -454,6 +490,14 @@ class _UserTile extends StatelessWidget {
value: 'premium',
child: Text(user.isPremium ? 'Ta bort Premium' : 'Ge Premium'),
),
PopupMenuItem(
value: 'sharing',
child: Text(
user.canShareRecipes
? 'Blockera receptdelning'
: 'Tillåt receptdelning',
),
),
const PopupMenuItem(
value: 'email',
child: Text('Ändra e-post'),