feat: add default mock data

This commit is contained in:
2026-07-30 04:01:01 +02:00
parent ae67878a3c
commit 715aa529d4
9 changed files with 242 additions and 7 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

+5
View File
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../database/app_database.dart';
import '../services/default_product_seeder.dart';
import '../viewmodels/bar_screen_view_model.dart'; import '../viewmodels/bar_screen_view_model.dart';
import '../viewmodels/product_list_view_model.dart'; import '../viewmodels/product_list_view_model.dart';
@@ -21,6 +23,9 @@ class _AppBootstrapState extends State<AppBootstrap> {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return; if (!mounted) return;
final database = context.read<AppDatabase>();
DefaultProductSeeder(database: database).seedIfEmpty();
context.read<ProductListViewModel>().ensureLoaded(); context.read<ProductListViewModel>().ensureLoaded();
context.read<BarScreenViewModel>().ensureLoaded(); context.read<BarScreenViewModel>().ensureLoaded();
}); });
+156
View File
@@ -0,0 +1,156 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:drift/drift.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:uuid/uuid.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import '../database/app_database.dart';
class DefaultProductSeeder {
final AppDatabase database;
DefaultProductSeeder({required this.database});
static const _defaultProducts = [
('Jupiler', 280, 80, 'Bier'),
('Stella Artois', 300, 60, 'Bier'),
('Leffe Blonde', 380, 30, 'Bier'),
('Duvel', 420, 25, 'Bier'),
('Hoegaarden', 350, 35, 'Bier'),
('Chimay Blue', 500, 20, 'Bier'),
('Witte Wijn', 380, 50, 'Wijn'),
('Rode Wijn', 400, 45, 'Wijn'),
('Rosé', 350, 40, 'Wijn'),
('Cava', 650, 15, 'Wijn'),
('Cola', 250, 100, 'Frisdrank'),
('Cola Zero', 250, 80, 'Frisdrank'),
('Spa Bruis', 200, 70, 'Frisdrank'),
('Spa Plat', 200, 70, 'Frisdrank'),
('Ice Tea', 280, 60, 'Frisdrank'),
('Limonade', 250, 50, 'Frisdrank'),
('Chips', 220, 40, 'Snacks'),
('Nootjes', 280, 35, 'Snacks'),
('Kroketten', 350, 30, 'Snacks'),
('Bitterballen', 320, 30, 'Snacks'),
('Koffie', 280, 200, 'Coffee'),
('Espresso', 250, 200, 'Coffee'),
('Latte Macchiato', 350, 150, 'Coffee'),
('Warme Chocomelk', 320, 100, 'Coffee'),
('Thee', 250, 150, 'Coffee'),
];
Future<void> seedIfEmpty() async {
try {
final count = await _getProductCount();
if (count > 0) return;
final imagesDir = await _ensureImagesDir();
await database.transaction(() async {
final uuid = const Uuid();
for (final (name, priceInCents, stock, category) in _defaultProducts) {
String? imagePath;
if (imagesDir != null) {
imagePath = await _copyBundledImage(name, imagesDir);
}
await database.into(database.products).insert(
ProductsCompanion.insert(
id: uuid.v4(),
name: name,
category: category,
stockQuantity: stock,
lowStockThreshold: 10,
priceInCents: priceInCents,
imagePath: Value(imagePath),
),
);
}
});
} catch (e, stack) {
debugPrint('DefaultProductSeeder: seedIfEmpty error: $e');
Sentry.captureException(e, stackTrace: stack);
}
}
Future<int> _getProductCount() async {
final countExpr = database.products.id.count();
final query = database.selectOnly(database.products)
..addColumns([countExpr]);
final row = await query.getSingle();
return row.read(countExpr) ?? 0;
}
Future<Directory?> _ensureImagesDir() async {
try {
final appDir = await getApplicationSupportDirectory();
final imagesDir = Directory(path.join(appDir.path, 'product_images'));
if (!imagesDir.existsSync()) {
await imagesDir.create(recursive: true);
}
return imagesDir;
} catch (e) {
debugPrint('DefaultProductSeeder: could not create images dir: $e');
return null;
}
}
Future<String?> _copyBundledImage(String productName, Directory imagesDir) async {
final slug = _toSlug(productName);
for (final ext in ['png', 'jpg', 'webp']) {
final assetKey = 'assets/products/$slug.$ext';
try {
final bytes = await rootBundle.load(assetKey);
final fileName = '${DateTime.now().millisecondsSinceEpoch}_$slug.$ext';
final destPath = path.join(imagesDir.path, fileName);
final destFile = File(destPath);
await destFile.writeAsBytes(bytes.buffer.asUint8List(
bytes.offsetInBytes,
bytes.lengthInBytes,
));
return destPath;
} catch (_) {
// Asset not found for this extension, try next
}
}
debugPrint('DefaultProductSeeder: no bundled image found for "$productName" '
'(looked for assets/products/$slug.{png,jpg,webp})');
return null;
}
String _toSlug(String name) {
return name
.toLowerCase()
.replaceAll(' ', '_')
.replaceAll("'", '')
.replaceAll('é', 'e')
.replaceAll('è', 'e')
.replaceAll('ê', 'e')
.replaceAll('ë', 'e')
.replaceAll('â', 'a')
.replaceAll('à', 'a')
.replaceAll('ä', 'a')
.replaceAll('û', 'u')
.replaceAll('ù', 'u')
.replaceAll('ü', 'u')
.replaceAll('ô', 'o')
.replaceAll('ö', 'o')
.replaceAll('ï', 'i')
.replaceAll('î', 'i')
.replaceAll('ç', 'c')
.replaceAll('œ', 'oe');
}
}
+65
View File
@@ -1,8 +1,12 @@
import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:path_provider/path_provider.dart';
import 'package:kooltab2/database/app_database.dart'; import 'package:kooltab2/database/app_database.dart';
import 'package:kooltab2/services/bar_tab_service.dart'; import 'package:kooltab2/services/bar_tab_service.dart';
import 'package:kooltab2/models/payment_method.dart'; import 'package:kooltab2/models/payment_method.dart';
import 'package:kooltab2/services/default_product_seeder.dart';
import 'package:kooltab2/services/pin_lock_service.dart'; import 'package:kooltab2/services/pin_lock_service.dart';
import 'package:kooltab2/services/product_service.dart'; import 'package:kooltab2/services/product_service.dart';
import 'package:kooltab2/services/settings_service.dart'; import 'package:kooltab2/services/settings_service.dart';
@@ -78,6 +82,67 @@ class DevMenuViewModel extends ChangeNotifier {
} }
} }
Future<void> seedDefaultProducts() async {
_isLoading = true;
notifyListeners();
try {
await DefaultProductSeeder(database: database).seedIfEmpty();
final count = await _getProductCount();
_lastAction = 'Seeded default products ($count total)';
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> clearAndReseedProducts() async {
_isLoading = true;
notifyListeners();
try {
await database.transaction(() async {
await database.delete(database.tabItems).go();
await database.delete(database.products).go();
});
await DefaultProductSeeder(database: database).seedIfEmpty();
final count = await _getProductCount();
_lastAction = 'Cleared and reseeded ($count products)';
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<int> _getProductCount() async {
final countExpr = database.products.id.count();
final query = database.selectOnly(database.products)
..addColumns([countExpr]);
final row = await query.getSingle();
return row.read(countExpr) ?? 0;
}
Future<void> clearProductImages() async {
_isLoading = true;
notifyListeners();
try {
final appDir = await getApplicationSupportDirectory();
final imagesDir = Directory('${appDir.path}/product_images');
if (await imagesDir.exists()) {
await imagesDir.delete(recursive: true);
}
_lastAction = 'Product images cleared';
} catch (e, stack) {
debugPrint('DevMenuViewModel: clearProductImages error: $e');
Sentry.captureException(e, stackTrace: stack);
_lastAction = 'Failed to clear images: $e';
} finally {
_isLoading = false;
notifyListeners();
}
}
Future<void> resetAllStock() async { Future<void> resetAllStock() async {
_isLoading = true; _isLoading = true;
notifyListeners(); notifyListeners();
+14 -6
View File
@@ -76,9 +76,16 @@ class _DevMenuViewState extends State<DevMenuView> {
onTap: vm.isLoading ? null : () => vm.clearClosedTabHistory(), onTap: vm.isLoading ? null : () => vm.clearClosedTabHistory(),
), ),
_Tile( _Tile(
icon: Icons.inventory_2_rounded, icon: Icons.add_box_rounded,
title: 'Reset all stock', title: 'Seed default products',
onTap: vm.isLoading ? null : () => vm.resetAllStock(), subtitle: 'Only seeds if table is empty',
onTap: vm.isLoading ? null : () => vm.seedDefaultProducts(),
),
_Tile(
icon: Icons.restart_alt_rounded,
title: 'Clear & reseed products',
subtitle: 'Wipes all products, then seeds defaults',
onTap: vm.isLoading ? null : () => vm.clearAndReseedProducts(),
), ),
_Tile( _Tile(
icon: Icons.delete_forever_rounded, icon: Icons.delete_forever_rounded,
@@ -86,9 +93,10 @@ class _DevMenuViewState extends State<DevMenuView> {
onTap: vm.isLoading ? null : () => vm.clearAllProducts(), onTap: vm.isLoading ? null : () => vm.clearAllProducts(),
), ),
_Tile( _Tile(
icon: Icons.add_box_rounded, icon: Icons.image_rounded,
title: 'Seed demo data', title: 'Clear product images',
onTap: vm.isLoading ? null : () => vm.seedDemoData(), subtitle: 'Deletes images from product_images folder',
onTap: vm.isLoading ? null : () => vm.clearProductImages(),
), ),
], ],
), ),
+2 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 2.0.0+1 version: 1.0.3+1
environment: environment:
sdk: ^3.12.2 sdk: ^3.12.2
@@ -88,6 +88,7 @@ flutter:
assets: assets:
- assets/icons/payconic.svg - assets/icons/payconic.svg
- assets/products/
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images # https://flutter.dev/to/resolution-aware-images