feat: add isPrivate field to Product model and implement private product creation and retrieval
This commit is contained in:
@@ -6,6 +6,9 @@ import '../../features/admin/domain/admin_category_node.dart';
|
||||
///
|
||||
/// Returnerar det valda produkt-id:t, eller null om användaren avbryter.
|
||||
///
|
||||
/// [onCreate] — valfri callback för att skapa ny produkt; anropas med produktnamnet
|
||||
/// och returnerar `ProductOption` för den skapade produkten.
|
||||
///
|
||||
/// Anropas via [CategoryThenProductPicker.show].
|
||||
class CategoryThenProductPicker {
|
||||
CategoryThenProductPicker._();
|
||||
@@ -35,12 +38,16 @@ class CategoryThenProductPicker {
|
||||
///
|
||||
/// [preselectedCategoryId] — om satt scrollas trädet till den noden och den
|
||||
/// markeras visuellt. Användaren kan fortfarande välja en annan kategori.
|
||||
///
|
||||
/// [onCreate] — valfri callback; om satt visas "Skapa ny" i produktpickern.
|
||||
/// Anropas med produktnamnet och ska returnera den nya `ProductOption`.
|
||||
static Future<int?> show(
|
||||
BuildContext context, {
|
||||
required List<AdminCategoryNode> categoryTree,
|
||||
required List<ProductOption> products,
|
||||
int? currentProductId,
|
||||
int? preselectedCategoryId,
|
||||
Future<ProductOption?> Function(String name, int categoryId)? onCreate,
|
||||
}) async {
|
||||
// Steg 1 — välj kategori
|
||||
final selectedCategory = await showModalBottomSheet<AdminCategoryNode>(
|
||||
@@ -64,6 +71,11 @@ class CategoryThenProductPicker {
|
||||
.toList();
|
||||
final useList = filtered.isNotEmpty ? filtered : products;
|
||||
|
||||
// Bygg eventuell onCreate-callback med categoryId inbunden
|
||||
final onCreateBound = onCreate == null
|
||||
? null
|
||||
: (String name) => onCreate(name, selectedCategory.id);
|
||||
|
||||
// Steg 2 — välj produkt
|
||||
if (!context.mounted) return null;
|
||||
return ProductPickerField.showSheet(
|
||||
@@ -72,6 +84,7 @@ class CategoryThenProductPicker {
|
||||
value: currentProductId,
|
||||
label: 'Produkt i "${selectedCategory.name}"',
|
||||
categoryFilter: null, // redan förfiltrerat
|
||||
onCreate: onCreateBound,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,8 @@ class ProductPickerField extends StatelessWidget {
|
||||
/// Returnerar valt produkt-id, null (ingen ändring), eller [_clearSelectionToken] (rensa).
|
||||
///
|
||||
/// [categoryFilter] — om satt visas bara produkter vars categoryId finns i listan.
|
||||
/// Används med AI-kategorisuggestion för att förifiltrera på rätt kategorigren.
|
||||
/// [onCreate] — valfri callback för att skapa en ny produkt; anropas med produktnamnet
|
||||
/// och returnerar den nya `ProductOption` om skapandet lyckades.
|
||||
static Future<int?> showSheet(
|
||||
BuildContext context, {
|
||||
required List<ProductOption> products,
|
||||
@@ -120,6 +121,7 @@ class ProductPickerField extends StatelessWidget {
|
||||
String label = 'Produkt',
|
||||
String? initialQuery,
|
||||
Set<int>? categoryFilter,
|
||||
Future<ProductOption?> Function(String name)? onCreate,
|
||||
}) async {
|
||||
// Filtrera på kategori om angiven, men visa alla om filtret ger nollresultat
|
||||
final baseList = categoryFilter != null && categoryFilter.isNotEmpty
|
||||
@@ -134,17 +136,54 @@ class ProductPickerField extends StatelessWidget {
|
||||
builder: (sheetContext) {
|
||||
final controller = TextEditingController(text: initialQuery ?? '');
|
||||
var query = initialQuery ?? '';
|
||||
// Mutable lokal kopia — nya produkter kan läggas till
|
||||
var displayList = useList.toList();
|
||||
|
||||
return StatefulBuilder(
|
||||
builder: (ctx, setModalState) {
|
||||
final normalizedQuery = query.trim().toLowerCase();
|
||||
final filtered = normalizedQuery.isEmpty
|
||||
? useList
|
||||
: useList
|
||||
? displayList
|
||||
: displayList
|
||||
.where((p) => p.name.toLowerCase().contains(normalizedQuery))
|
||||
.toList();
|
||||
|
||||
final isFiltered = useList.length < products.length;
|
||||
final isFiltered = displayList.length < products.length;
|
||||
|
||||
// "Skapa ny produkt"-dialog
|
||||
Future<void> openCreateDialog() async {
|
||||
final nameCtrl = TextEditingController(text: query.trim());
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: ctx,
|
||||
builder: (dCtx) => AlertDialog(
|
||||
title: const Text('Ny produkt'),
|
||||
content: TextField(
|
||||
controller: nameCtrl,
|
||||
autofocus: true,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Produktnamn',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dCtx, false),
|
||||
child: const Text('Avbryt'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(dCtx, true),
|
||||
child: const Text('Skapa'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !ctx.mounted) return;
|
||||
final newProduct = await onCreate!(nameCtrl.text);
|
||||
if (newProduct == null || !ctx.mounted) return;
|
||||
setModalState(() => displayList = [...displayList, newProduct]);
|
||||
if (ctx.mounted) Navigator.pop(ctx, newProduct.id);
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
height: MediaQuery.of(ctx).size.height * 0.85,
|
||||
@@ -161,12 +200,18 @@ class ProductPickerField extends StatelessWidget {
|
||||
Text(label, style: Theme.of(ctx).textTheme.titleMedium),
|
||||
if (isFiltered)
|
||||
Text(
|
||||
'Visar ${useList.length} produkter i föreslagen kategori',
|
||||
'Visar ${displayList.length} produkter i föreslagen kategori',
|
||||
style: Theme.of(ctx).textTheme.labelSmall?.copyWith(color: Colors.green.shade700),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (onCreate != null)
|
||||
TextButton.icon(
|
||||
onPressed: openCreateDialog,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Skapa ny'),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () => Navigator.pop(ctx, _clearSelectionToken),
|
||||
icon: const Icon(Icons.clear),
|
||||
|
||||
Reference in New Issue
Block a user