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: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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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: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();
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user