Files
kooltab/lib/viewmodels/dev_menu_view_model.dart
T

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();
}
}
}