Files
recipe-app/flutter/lib/features/inventory/presentation/inventory_screen.dart
T

243 lines
8.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/api/api_error_mapper.dart';
import '../../../core/ui/async_state_views.dart';
import '../../auth/data/auth_providers.dart';
import '../data/inventory_providers.dart';
import '../domain/inventory_item.dart';
class InventoryScreen extends ConsumerWidget {
const InventoryScreen({super.key});
static const _locationOptions = <String>['', 'Kyl', 'Frys', 'Skafferi'];
static const _sortOptions = <({String value, String label})>[
(value: '', label: 'Senast tillagda'),
(value: 'nameAsc', label: 'Namn A-O'),
(value: 'bestBeforeAsc', label: 'Bast fore stigande'),
(value: 'bestBeforeDesc', label: 'Bast fore fallande'),
];
@override
Widget build(BuildContext context, WidgetRef ref) {
final location = ref.watch(inventoryLocationFilterProvider);
final sort = ref.watch(inventorySortFilterProvider);
final inventoryAsync = ref.watch(inventoryProvider);
return inventoryAsync.when(
loading: () => const LoadingStateView(label: 'Laddar inventarie...'),
error: (e, _) => ErrorStateView(
message: mapErrorToUserMessage(e),
onRetry: () => ref.invalidate(inventoryProvider),
),
data: (items) {
final filterSection = Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Filter och sortering',
style: TextStyle(fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _locationOptions
.map(
(option) => ChoiceChip(
label: Text(option.isEmpty ? 'Alla' : option),
selected: location == option,
onSelected: (_) => ref
.read(inventoryLocationFilterProvider.notifier)
.state = option,
),
)
.toList(),
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: sort,
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Sortering',
border: OutlineInputBorder(),
),
items: _sortOptions
.map(
(option) => DropdownMenuItem<String>(
value: option.value,
child: Text(option.label),
),
)
.toList(),
onChanged: (value) {
ref.read(inventorySortFilterProvider.notifier).state =
value ?? '';
},
),
],
),
);
if (items.isEmpty) {
return Stack(
children: [
ListView(
padding: const EdgeInsets.only(bottom: 88),
children: [
filterSection,
const EmptyStateView(title: 'Inventariet ar tomt.'),
],
),
Positioned(
right: 16,
bottom: 16,
child: FloatingActionButton.extended(
onPressed: () => context.push('/inventory/create'),
icon: const Icon(Icons.add),
label: const Text('Lägg till'),
),
),
],
);
}
return Stack(
children: [
ListView.separated(
padding: const EdgeInsets.only(bottom: 88),
itemCount: items.length + 1,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
if (index == 0) return filterSection;
final item = items[index - 1];
return _InventoryTile(item: item);
},
),
Positioned(
right: 16,
bottom: 16,
child: FloatingActionButton.extended(
onPressed: () => context.push('/inventory/create'),
icon: const Icon(Icons.add),
label: const Text('Lägg till'),
),
),
],
);
},
);
}
}
class _InventoryTile extends StatelessWidget {
final InventoryItem item;
const _InventoryTile({required this.item});
@override
Widget build(BuildContext context) {
final subtitle = [
'${item.quantity} ${item.unit}',
if (item.location != null && item.location!.isNotEmpty) item.location!,
if (item.bestBeforeDate != null) 'Bäst före: ${_formatDate(item.bestBeforeDate!)}',
].join(' · ');
return ListTile(
title: Text(item.productName),
subtitle: Text(subtitle),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (item.opened)
const Padding(
padding: EdgeInsets.only(right: 4),
child: Chip(
label: Text('Oppnad'),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
),
),
Tooltip(
message: 'Konsumera',
child: IconButton(
icon: const Icon(Icons.remove_circle_outline),
onPressed: () => context.push('/inventory/${item.id}/consume'),
),
),
Tooltip(
message: 'Redigera',
child: IconButton(
icon: const Icon(Icons.edit_outlined),
onPressed: () => context.push('/inventory/${item.id}/edit'),
),
),
_DeleteInventoryButton(item: item),
],
),
onTap: () => context.push('/inventory/${item.id}'),
);
}
String _formatDate(String iso) {
try {
final dt = DateTime.parse(iso);
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')}';
} catch (_) {
return iso;
}
}
}
class _DeleteInventoryButton extends ConsumerWidget {
final InventoryItem item;
const _DeleteInventoryButton({required this.item});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Tooltip(
message: 'Ta bort',
child: IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: () async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Ta bort inventariepost?'),
content: Text('Vill du ta bort "${item.productName}"?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Avbryt'),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Ta bort'),
),
],
),
);
if (confirmed != true) return;
try {
final token = await ref.read(authStateProvider.future);
await ref
.read(inventoryRepositoryProvider)
.deleteInventoryItem(item.id, token: token);
ref.invalidate(inventoryProvider);
} catch (error) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(mapErrorToUserMessage(error))),
);
}
},
),
);
}
}