feat: add dev menu (Kinda)

This commit is contained in:
2026-07-29 01:56:24 +02:00
parent 50e6fca9d1
commit da5588cd1c
4 changed files with 597 additions and 0 deletions
+6
View File
@@ -4,6 +4,7 @@ import 'package:kooltab2/views/settings_view.dart';
import '../viewmodels/pin_lock_view_model.dart';
import '../views/bar_screen_view.dart';
import '../views/dev_menu_view.dart';
import '../views/history_screen_view.dart';
import '../views/pin_lock_view.dart';
import '../views/product_form_view.dart';
@@ -74,6 +75,11 @@ GoRouter createAppRouter(PinLockViewModel pinLockViewModel) {
path: '/settings',
builder: (context, state) => const SettingsScreenView(),
),
GoRoute(
path: '/dev',
builder: (context, state) => const DevMenuView(),
),
],
);
}
+10
View File
@@ -4,6 +4,7 @@ import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
import 'package:kooltab2/services/pin_lock_service.dart';
import 'package:kooltab2/services/settings_service.dart';
import 'package:kooltab2/viewmodels/dev_menu_view_model.dart';
import 'package:kooltab2/viewmodels/history_view_model.dart';
import 'package:kooltab2/viewmodels/inventory_view_model.dart';
import 'package:kooltab2/viewmodels/pin_lock_view_model.dart';
@@ -81,6 +82,15 @@ Future<void> main() async {
settingsService: context.read<SettingsService>(),
),
),
ChangeNotifierProvider<DevMenuViewModel>(
create: (context) => DevMenuViewModel(
database: context.read<AppDatabase>(),
productService: context.read<ProductService>(),
barTabService: context.read<BarTabService>(),
settingsService: context.read<SettingsService>(),
pinLockService: PinLockService(),
),
),
],
child: AppBootstrap(child: KoolTabApp(router: appRouter)),
),
+257
View File
@@ -0,0 +1,257 @@
import 'package:flutter/foundation.dart';
import 'package:kooltab2/database/app_database.dart';
import 'package:kooltab2/services/bar_tab_service.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: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> 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();
}
}
}
+324
View File
@@ -0,0 +1,324 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../viewmodels/dev_menu_view_model.dart';
class DevMenuView extends StatefulWidget {
const DevMenuView({super.key});
@override
State<DevMenuView> createState() => _DevMenuViewState();
}
class _DevMenuViewState extends State<DevMenuView> {
DevMenuViewModel? _vm;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_vm = context.read<DevMenuViewModel>();
_vm!.addListener(_onVmChange);
});
}
void _onVmChange() {
if (!mounted) return;
final vm = _vm;
if (vm == null) return;
final message = vm.lastAction;
if (message == null) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
@override
void dispose() {
_vm?.removeListener(_onVmChange);
super.dispose();
}
@override
Widget build(BuildContext context) {
final vm = context.watch<DevMenuViewModel>();
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => context.pop(),
icon: const Icon(Icons.arrow_back),
),
title: const Text('Dev Menu'),
centerTitle: true,
),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 12),
children: [
_Section(
title: 'Data Management',
children: [
_Tile(
icon: Icons.delete_sweep_rounded,
title: 'Clear all open tabs',
onTap: vm.isLoading ? null : () => vm.clearOpenTabs(),
),
_Tile(
icon: Icons.history_rounded,
title: 'Clear closed tab history',
onTap: vm.isLoading ? null : () => vm.clearClosedTabHistory(),
),
_Tile(
icon: Icons.inventory_2_rounded,
title: 'Reset all stock',
onTap: vm.isLoading ? null : () => vm.resetAllStock(),
),
_Tile(
icon: Icons.delete_forever_rounded,
title: 'Clear all products',
onTap: vm.isLoading ? null : () => vm.clearAllProducts(),
),
_Tile(
icon: Icons.add_box_rounded,
title: 'Seed demo data',
onTap: vm.isLoading ? null : () => vm.seedDemoData(),
),
],
),
_Section(
title: 'Debugging',
children: [
_Tile(
icon: Icons.lock_reset_rounded,
title: 'Reset PIN',
subtitle: 'Remove PIN lock',
onTap: vm.isLoading ? null : () => vm.resetPin(),
),
_Tile(
icon: Icons.bug_report_rounded,
title: 'Toggle debug overlay',
subtitle: 'Show FPS, memory, widget count',
onTap: () => _showDebugOverlayInfo(),
),
],
),
_Section(
title: 'Feature Toggles',
children: [
_Tile(
icon: Icons.update_rounded,
title: 'Mock update available',
subtitle: 'Simulate OTA update dialog',
onTap: () => _showMockUpdateDialog(),
),
],
),
_Section(
title: 'Testing Helpers',
children: [
_Tile(
icon: Icons.receipt_long_rounded,
title: 'Create test tab',
subtitle: 'Add tab with random items',
onTap: vm.isLoading ? null : () => vm.createTestTab(),
),
_Tile(
icon: Icons.grid_view_rounded,
title: 'Add 100 test products',
subtitle: 'Stress test product grid',
onTap: vm.isLoading ? null : () => vm.addTestProducts(),
),
_Tile(
icon: Icons.warning_rounded,
title: 'Simulate low stock',
subtitle: 'Set all products below threshold',
onTap: vm.isLoading ? null : () => vm.simulateLowStock(),
),
],
),
_Section(
title: 'Performance',
children: [
_Tile(
icon: Icons.image_rounded,
title: 'Clear image cache',
onTap: vm.isLoading ? null : () => vm.clearImageCache(),
),
_Tile(
icon: Icons.refresh_rounded,
title: 'Reload product images',
onTap: vm.isLoading ? null : () => vm.reloadProductImages(),
),
],
),
if (vm.isLoading)
const Padding(
padding: EdgeInsets.all(20),
child: Center(child: CircularProgressIndicator()),
),
],
),
);
}
void _showDebugOverlayInfo() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Debug Overlay'),
content: const Text(
'The debug overlay shows FPS, memory usage, and widget counts. '
'Enable it via Flutter DevTools in debug mode.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}
void _showMockUpdateDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Update available'),
content: const Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Version 99.0.0 is available.'),
SizedBox(height: 12),
Text('Bug fixes and performance improvements.'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Later'),
),
FilledButton(
onPressed: () => Navigator.pop(context),
child: const Text('Update'),
),
],
),
);
}
}
class _Section extends StatelessWidget {
final String title;
final List<Widget> children;
const _Section({required this.title, required this.children});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title.toUpperCase(),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
color: scheme.onSurface.withValues(alpha: 0.5),
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: scheme.onSurface.withValues(alpha: 0.04),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: scheme.onSurface.withValues(alpha: 0.06),
),
),
clipBehavior: Clip.antiAlias,
child: Column(children: children),
),
],
),
);
}
}
class _Tile extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final VoidCallback? onTap;
const _Tile({
required this.icon,
required this.title,
this.subtitle,
this.onTap,
});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Icon(
icon,
size: 22,
color: scheme.onSurface.withValues(alpha: 0.7),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontWeight: FontWeight.w600,
color: scheme.onSurface,
),
),
if (subtitle != null)
Text(
subtitle!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: scheme.onSurface.withValues(alpha: 0.5),
),
),
],
),
),
if (onTap != null)
Icon(
Icons.chevron_right_rounded,
color: scheme.onSurface.withValues(alpha: 0.3),
),
],
),
),
),
);
}
}