feat: add default mock data
This commit is contained in:
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 |
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.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/product_list_view_model.dart';
|
||||
|
||||
@@ -21,6 +23,9 @@ class _AppBootstrapState extends State<AppBootstrap> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
|
||||
final database = context.read<AppDatabase>();
|
||||
DefaultProductSeeder(database: database).seedIfEmpty();
|
||||
|
||||
context.read<ProductListViewModel>().ensureLoaded();
|
||||
context.read<BarScreenViewModel>().ensureLoaded();
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:kooltab2/database/app_database.dart';
|
||||
import 'package:kooltab2/services/bar_tab_service.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/product_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 {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
@@ -76,9 +76,16 @@ class _DevMenuViewState extends State<DevMenuView> {
|
||||
onTap: vm.isLoading ? null : () => vm.clearClosedTabHistory(),
|
||||
),
|
||||
_Tile(
|
||||
icon: Icons.inventory_2_rounded,
|
||||
title: 'Reset all stock',
|
||||
onTap: vm.isLoading ? null : () => vm.resetAllStock(),
|
||||
icon: Icons.add_box_rounded,
|
||||
title: 'Seed default products',
|
||||
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(
|
||||
icon: Icons.delete_forever_rounded,
|
||||
@@ -86,9 +93,10 @@ class _DevMenuViewState extends State<DevMenuView> {
|
||||
onTap: vm.isLoading ? null : () => vm.clearAllProducts(),
|
||||
),
|
||||
_Tile(
|
||||
icon: Icons.add_box_rounded,
|
||||
title: 'Seed demo data',
|
||||
onTap: vm.isLoading ? null : () => vm.seedDemoData(),
|
||||
icon: Icons.image_rounded,
|
||||
title: 'Clear product images',
|
||||
subtitle: 'Deletes images from product_images folder',
|
||||
onTap: vm.isLoading ? null : () => vm.clearProductImages(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
+2
-1
@@ -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
|
||||
# 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.
|
||||
version: 2.0.0+1
|
||||
version: 1.0.3+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
@@ -88,6 +88,7 @@ flutter:
|
||||
|
||||
assets:
|
||||
- assets/icons/payconic.svg
|
||||
- assets/products/
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
Reference in New Issue
Block a user