476 lines
13 KiB
Dart
476 lines
13 KiB
Dart
import 'dart:math';
|
|
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';
|
|
import 'package:sentry_flutter/sentry_flutter.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
class DevMenuViewModel extends ChangeNotifier {
|
|
final AppDatabase database;
|
|
final ProductService productService;
|
|
final BarTabService barTabService;
|
|
final SettingsService settingsService;
|
|
final PinLockService pinLockService;
|
|
|
|
bool _isLoading = false;
|
|
String? _lastAction;
|
|
|
|
bool get isLoading => _isLoading;
|
|
String? get lastAction => _lastAction;
|
|
|
|
DevMenuViewModel({
|
|
required this.database,
|
|
required this.productService,
|
|
required this.barTabService,
|
|
required this.settingsService,
|
|
required this.pinLockService,
|
|
});
|
|
|
|
Future<void> clearOpenTabs() async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
await database.transaction(() async {
|
|
await database.delete(database.tabItems).go();
|
|
await database.delete(database.barTabs).go();
|
|
});
|
|
_lastAction = 'Cleared all open tabs';
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> clearClosedTabHistory() async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
await database.transaction(() async {
|
|
await database.delete(database.closedTabItems).go();
|
|
await database.delete(database.closedTabs).go();
|
|
});
|
|
_lastAction = 'Cleared closed tab history';
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> clearAllProducts() async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
await database.transaction(() async {
|
|
await database.delete(database.tabItems).go();
|
|
await database.delete(database.products).go();
|
|
});
|
|
_lastAction = 'Cleared all products';
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
try {
|
|
final products = await productService.getProducts();
|
|
for (final product in products) {
|
|
await productService.updateProduct(
|
|
product.copyWith(stockQuantity: 100),
|
|
);
|
|
}
|
|
_lastAction = 'Reset stock for ${products.length} products';
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> seedDemoData() async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final uuid = const Uuid();
|
|
|
|
final categories = ['Beer', 'Wine', 'Soft Drinks', 'Snacks'];
|
|
final demoProducts = [
|
|
('Heineken', 350, 50),
|
|
('Stella Artois', 380, 40),
|
|
('Leffe Blonde', 420, 30),
|
|
('Duvel', 450, 25),
|
|
('Hoegaarden', 380, 35),
|
|
('Chimay Blue', 550, 20),
|
|
('White Wine', 450, 60),
|
|
('Red Wine', 450, 55),
|
|
('Rosé', 400, 45),
|
|
('Champagne', 850, 15),
|
|
('Cola', 250, 100),
|
|
('Lemonade', 250, 90),
|
|
('Sparkling Water', 200, 80),
|
|
('Orange Juice', 300, 70),
|
|
('Chips', 250, 40),
|
|
('Nuts', 300, 35),
|
|
('Chocolate Bar', 200, 50),
|
|
('Ice Cream', 350, 25),
|
|
];
|
|
|
|
for (var i = 0; i < demoProducts.length; i++) {
|
|
final (name, priceInCents, stock) = demoProducts[i];
|
|
final category = categories[i ~/ 5];
|
|
|
|
await database
|
|
.into(database.products)
|
|
.insert(
|
|
ProductsCompanion.insert(
|
|
id: uuid.v4(),
|
|
name: name,
|
|
category: category,
|
|
stockQuantity: stock,
|
|
lowStockThreshold: 10,
|
|
priceInCents: priceInCents,
|
|
),
|
|
);
|
|
}
|
|
|
|
_lastAction = 'Seeded ${demoProducts.length} demo products';
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> resetPin() async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
await pinLockService.clearPin();
|
|
_lastAction = 'PIN reset (no PIN required)';
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> createTestTab() async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final products = await productService.getProducts();
|
|
if (products.isEmpty) {
|
|
_lastAction = 'No products to add to test tab';
|
|
return;
|
|
}
|
|
|
|
final tab = await barTabService.createTab(customerName: 'Test Customer');
|
|
final randomProducts = products.take(3).toList();
|
|
|
|
for (final product in randomProducts) {
|
|
await barTabService.addProductToTab(tabId: tab.id, product: product);
|
|
}
|
|
|
|
_lastAction = 'Created test tab with ${randomProducts.length} items';
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> addTestProducts({int count = 100}) async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final uuid = const Uuid();
|
|
|
|
for (var i = 0; i < count; i++) {
|
|
await database
|
|
.into(database.products)
|
|
.insert(
|
|
ProductsCompanion.insert(
|
|
id: uuid.v4(),
|
|
name: 'Test Product $i',
|
|
category: 'Test Category ${i % 5}',
|
|
stockQuantity: 50 + (i % 50),
|
|
lowStockThreshold: 5,
|
|
priceInCents: 100 + (i * 10),
|
|
),
|
|
);
|
|
}
|
|
|
|
_lastAction = 'Added $count test products';
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> simulateLowStock() async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final products = await productService.getProducts();
|
|
for (final product in products) {
|
|
await productService.updateProduct(
|
|
product.copyWith(stockQuantity: 3, lowStockThreshold: 10),
|
|
);
|
|
}
|
|
_lastAction = 'Simulated low stock for ${products.length} products';
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> clearImageCache() async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
_lastAction = 'Image cache cleared (no-op in debug mode)';
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> reloadProductImages() async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
_lastAction = 'Product images reloaded';
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
|
|
try {
|
|
throw Exception('Test GlitchTip error!');
|
|
} catch (exception, stackTrace) {
|
|
Sentry.captureException(exception, stackTrace: stackTrace);
|
|
}
|
|
}
|
|
|
|
Future<void> generateMockOrders({int count = 100}) async {
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
final products = await productService.getProducts();
|
|
if (products.isEmpty) {
|
|
_lastAction = 'No products available — seed demo data first';
|
|
return;
|
|
}
|
|
|
|
final uuid = const Uuid();
|
|
final random = Random();
|
|
final today = DateTime.now();
|
|
final startOfToday = DateTime(today.year, today.month, today.day);
|
|
final customerNames = [
|
|
'Alice',
|
|
'Bob',
|
|
'Charlie',
|
|
'Diana',
|
|
'Eve',
|
|
'Frank',
|
|
'Grace',
|
|
'Hank',
|
|
'Ivy',
|
|
'Jack',
|
|
'Kate',
|
|
'Leo',
|
|
'Mia',
|
|
'Noah',
|
|
'Olivia',
|
|
'Pete',
|
|
'Quinn',
|
|
'Rose',
|
|
'Sam',
|
|
'Tina',
|
|
'Umar',
|
|
'Vera',
|
|
'Wes',
|
|
'Xena',
|
|
'Yves',
|
|
'Zara',
|
|
];
|
|
const paymentMethods = [PaymentMethod.cash, PaymentMethod.payconiq];
|
|
final usedCustomers = <String>{};
|
|
|
|
await database.transaction(() async {
|
|
for (var i = 0; i < count; i++) {
|
|
final customer = customerNames[random.nextInt(customerNames.length)];
|
|
usedCustomers.add(customer);
|
|
final closedTabId = uuid.v4();
|
|
final distinctProductCount = min(
|
|
products.length,
|
|
1 + random.nextInt(5),
|
|
);
|
|
final shuffledProducts = [...products]..shuffle(random);
|
|
|
|
final orderDay = startOfToday.subtract(
|
|
Duration(days: random.nextInt(60)),
|
|
);
|
|
final closedAt = DateTime(
|
|
orderDay.year,
|
|
orderDay.month,
|
|
orderDay.day,
|
|
12 + random.nextInt(10),
|
|
random.nextInt(60),
|
|
);
|
|
|
|
// A few mock tabs span multiple days so the history day dropdowns
|
|
// also get realistic multi-day data.
|
|
final spanDays = random.nextInt(5) == 0 ? 1 + random.nextInt(2) : 0;
|
|
final firstPurchaseAt = closedAt.subtract(
|
|
Duration(
|
|
days: spanDays,
|
|
hours: random.nextInt(4),
|
|
minutes: random.nextInt(60),
|
|
),
|
|
);
|
|
final purchaseWindowMinutes = closedAt
|
|
.difference(firstPurchaseAt)
|
|
.inMinutes;
|
|
|
|
await database
|
|
.into(database.closedTabs)
|
|
.insert(
|
|
ClosedTabsCompanion.insert(
|
|
id: closedTabId,
|
|
originalTabId: uuid.v4(),
|
|
customerName: customer,
|
|
closedAt: closedAt,
|
|
paymentMethod: Value(
|
|
paymentMethods[random.nextInt(paymentMethods.length)].value,
|
|
),
|
|
),
|
|
);
|
|
|
|
for (var j = 0; j < distinctProductCount; j++) {
|
|
final product = shuffledProducts[j];
|
|
final quantity = 1 + random.nextInt(4);
|
|
|
|
for (var k = 0; k < quantity; k++) {
|
|
var purchasedAt = firstPurchaseAt.add(
|
|
Duration(minutes: random.nextInt(purchaseWindowMinutes + 1)),
|
|
);
|
|
|
|
if (spanDays > 0 && j == 0 && k == 0) {
|
|
purchasedAt = firstPurchaseAt;
|
|
} else if (spanDays > 0 &&
|
|
j == distinctProductCount - 1 &&
|
|
k == quantity - 1) {
|
|
purchasedAt = closedAt.subtract(
|
|
Duration(minutes: random.nextInt(30)),
|
|
);
|
|
}
|
|
|
|
await database
|
|
.into(database.closedTabItems)
|
|
.insert(
|
|
ClosedTabItemsCompanion.insert(
|
|
id: uuid.v4(),
|
|
closedTabId: closedTabId,
|
|
productId: product.id,
|
|
productName: product.name,
|
|
quantity: 1,
|
|
unitPriceInCents: product.priceInCents,
|
|
purchasedAt: Value(purchasedAt),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
_lastAction =
|
|
'Generated $count mock orders across '
|
|
'${usedCustomers.length} customers';
|
|
} finally {
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
}
|