feat: move inventory to own view model, add signing keys
This commit is contained in:
+68
-237
@@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:kooltab2/viewmodels/pin_lock_view_model.dart';
|
||||
import 'package:kooltab2/viewmodels/product_list_view_model.dart';
|
||||
import 'package:kooltab2/views/dialogs/close_tab_dialog.dart';
|
||||
import 'package:kooltab2/views/dialogs/new_tab_dialog.dart';
|
||||
import 'package:kooltab2/views/widgets/product_tile.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'dart:io';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
|
||||
import '../models/bar_tab.dart';
|
||||
@@ -49,10 +51,10 @@ class BarScreenView extends StatelessWidget {
|
||||
const SizedBox(width: 6),
|
||||
IconButton(
|
||||
tooltip: 'logout',
|
||||
onPressed: (){
|
||||
onPressed: () {
|
||||
Provider.of<PinLockViewModel>(context, listen: false).lock();
|
||||
},
|
||||
icon: const Icon(Icons.logout)
|
||||
icon: const Icon(Icons.logout),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
@@ -118,10 +120,10 @@ class BarScreenView extends StatelessWidget {
|
||||
tabs: viewModel.tabs,
|
||||
selectedTab: viewModel.selectedTab,
|
||||
selectedTabId: viewModel.selectedTabId,
|
||||
onNewTabPressed: () => _showNewTabDialog(context),
|
||||
onNewTabPressed: () => showNewTabDialog(context),
|
||||
onTabSelected: viewModel.selectTab,
|
||||
onItemQuantityChanged: viewModel.changeItemQuantity,
|
||||
onCloseTabPressed: () => _confirmCloseTab(context),
|
||||
onCloseTabPressed: () => confirmCloseTab(context),
|
||||
onTabClosed: viewModel.closeTab,
|
||||
),
|
||||
),
|
||||
@@ -131,83 +133,6 @@ class BarScreenView extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showNewTabDialog(BuildContext context) async {
|
||||
final controller = TextEditingController();
|
||||
|
||||
final name = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Text('Open new tab'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Customer / group name',
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
Navigator.of(dialogContext).pop(value);
|
||||
},
|
||||
),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop(controller.text);
|
||||
},
|
||||
child: const Text('Open tab'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (name == null || name.trim().isEmpty) return;
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
await context.read<BarScreenViewModel>().createTab(name.trim());
|
||||
}
|
||||
|
||||
Future<void> _confirmCloseTab(BuildContext context) async {
|
||||
final viewModel = context.read<BarScreenViewModel>();
|
||||
final tab = viewModel.selectedTab;
|
||||
|
||||
if (tab == null) return;
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
title: Text('Close ${tab.customerName}ʼs tab?'),
|
||||
content: Text('Current total: ${tab.formattedTotal}\n\n'),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(dialogContext).colorScheme.error,
|
||||
),
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('Close tab'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
await viewModel.closeSelectedTab();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -329,7 +254,7 @@ class _ProductGrid extends StatelessWidget {
|
||||
itemBuilder: (context, index) {
|
||||
final product = products[index];
|
||||
|
||||
return _ProductTile(
|
||||
return ProductTile(
|
||||
product: product,
|
||||
enabled: hasSelectedTab,
|
||||
onTap: () => onProductTap(product),
|
||||
@@ -344,98 +269,6 @@ class _ProductGrid extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _ProductTile extends StatelessWidget {
|
||||
final Product product;
|
||||
final bool enabled;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ProductTile({
|
||||
required this.product,
|
||||
required this.enabled,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
bool get _hasImage {
|
||||
final imagePath = product.imagePath;
|
||||
|
||||
if (imagePath == null || imagePath.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return File(imagePath).existsSync();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: scheme.onSurface.withValues(alpha: 0.08), width: 1),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
child: Material(
|
||||
color: theme.cardTheme.color ?? scheme.surface,
|
||||
child: InkWell(
|
||||
onTap: enabled ? onTap : null,
|
||||
child: Opacity(
|
||||
opacity: enabled ? 1 : 0.4,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
_hasImage
|
||||
? Image.file(
|
||||
File(product.imagePath!),
|
||||
fit: BoxFit.scaleDown,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
)
|
||||
: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.image_not_supported_outlined,
|
||||
size: 34,
|
||||
color: scheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'No image',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_hasImage)
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withValues(alpha: 0.35),
|
||||
],
|
||||
stops: const [0.6, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tab panel
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -446,8 +279,7 @@ class _TabPanel extends StatefulWidget {
|
||||
final String? selectedTabId;
|
||||
final VoidCallback onNewTabPressed;
|
||||
final ValueChanged<String> onTabSelected;
|
||||
final Future<void> Function(TabItem item, int quantity)
|
||||
onItemQuantityChanged;
|
||||
final Future<void> Function(TabItem item, int quantity) onItemQuantityChanged;
|
||||
final VoidCallback onCloseTabPressed;
|
||||
final ValueChanged<String> onTabClosed;
|
||||
|
||||
@@ -572,27 +404,27 @@ class _TabPanelState extends State<_TabPanel> {
|
||||
Expanded(
|
||||
child: widget.selectedTab == null
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.receipt_long_outlined,
|
||||
size: 36,
|
||||
color: scheme.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Select or open a tab',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.receipt_long_outlined,
|
||||
size: 36,
|
||||
color: scheme.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Select or open a tab',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: _SelectedTabDetails(
|
||||
tab: widget.selectedTab!,
|
||||
onItemQuantityChanged: widget.onItemQuantityChanged,
|
||||
onCloseTabPressed: widget.onCloseTabPressed,
|
||||
),
|
||||
tab: widget.selectedTab!,
|
||||
onItemQuantityChanged: widget.onItemQuantityChanged,
|
||||
onCloseTabPressed: widget.onCloseTabPressed,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -601,7 +433,6 @@ class _TabPanelState extends State<_TabPanel> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _OpenTabsList extends StatelessWidget {
|
||||
final List<BarTab> tabs;
|
||||
final String? selectedTabId;
|
||||
@@ -721,9 +552,6 @@ class _OpenTabsList extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
class _SelectedTabDetails extends StatelessWidget {
|
||||
final BarTab tab;
|
||||
final Future<void> Function(TabItem item, int quantity) onItemQuantityChanged;
|
||||
@@ -752,50 +580,54 @@ class _SelectedTabDetails extends StatelessWidget {
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
Divider()
|
||||
Divider(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: tab.items.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.local_bar_outlined,
|
||||
size: 32,
|
||||
color: scheme.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tap products to add them',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.local_bar_outlined,
|
||||
size: 32,
|
||||
color: scheme.onSurface.withValues(alpha: 0.25),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Tap products to add them',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.separated(
|
||||
itemCount: tab.items.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final item = tab.items[index];
|
||||
itemCount: tab.items.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final item = tab.items[index];
|
||||
|
||||
return _TabItemRow(
|
||||
item: item,
|
||||
onQuantityChanged: onItemQuantityChanged,
|
||||
);
|
||||
},
|
||||
),
|
||||
return _TabItemRow(
|
||||
item: item,
|
||||
onQuantityChanged: onItemQuantityChanged,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.12),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.3),
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
@@ -807,8 +639,9 @@ class _SelectedTabDetails extends StatelessWidget {
|
||||
Text('Total', style: Theme.of(context).textTheme.bodySmall),
|
||||
Text(
|
||||
tab.formattedTotal,
|
||||
style: Theme.of(context).textTheme.headlineMedium
|
||||
?.copyWith(fontSize: 24),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.headlineMedium?.copyWith(fontSize: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -867,8 +700,7 @@ class _TabItemRow extends StatelessWidget {
|
||||
children: [
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () =>
|
||||
onQuantityChanged(item, item.quantity - 1),
|
||||
onPressed: () => onQuantityChanged(item, item.quantity - 1),
|
||||
icon: const Icon(Icons.remove_rounded, size: 18),
|
||||
),
|
||||
SizedBox(
|
||||
@@ -881,8 +713,7 @@ class _TabItemRow extends StatelessWidget {
|
||||
),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
onPressed: () =>
|
||||
onQuantityChanged(item, item.quantity + 1),
|
||||
onPressed: () => onQuantityChanged(item, item.quantity + 1),
|
||||
icon: const Icon(Icons.add_rounded, size: 18),
|
||||
),
|
||||
],
|
||||
@@ -900,4 +731,4 @@ class _TabItemRow extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:kooltab2/viewmodels/bar_screen_view_model.dart';
|
||||
import 'package:kooltab2/views/widgets/slide_confirm.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Future<void> confirmCloseTab(BuildContext context) async {
|
||||
final viewModel = context.read<BarScreenViewModel>();
|
||||
final tab = viewModel.selectedTab;
|
||||
|
||||
if (tab == null) return;
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
title: Text('Close ${tab.customerName}ʼs tab?'),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Current total: ${tab.formattedTotal}'),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
SlideConfirm(
|
||||
onConfirmed: () {
|
||||
Navigator.of(dialogContext).pop(true);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
await viewModel.closeSelectedTab();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:kooltab2/viewmodels/bar_screen_view_model.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
||||
Future<void> showNewTabDialog(BuildContext context) async {
|
||||
final controller = TextEditingController();
|
||||
|
||||
final name = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
title: const Text('Open new tab'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Customer / group name',
|
||||
),
|
||||
onSubmitted: (value) {
|
||||
Navigator.of(dialogContext).pop(value);
|
||||
},
|
||||
),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop(controller.text);
|
||||
},
|
||||
child: const Text('Open tab'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (name == null || name.trim().isEmpty) return;
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
await context.read<BarScreenViewModel>().createTab(name.trim());
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:kooltab2/models/product.dart';
|
||||
|
||||
|
||||
class ProductTile extends StatelessWidget {
|
||||
final Product product;
|
||||
final bool enabled;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const ProductTile({
|
||||
required this.product,
|
||||
required this.enabled,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
bool get _hasImage {
|
||||
final imagePath = product.imagePath;
|
||||
|
||||
if (imagePath == null || imagePath.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return File(imagePath).existsSync();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final scheme = theme.colorScheme;
|
||||
|
||||
final outOfStock = product.stockQuantity <= 0;
|
||||
final lowStock =
|
||||
product.stockQuantity > 0 &&
|
||||
product.stockQuantity <= product.lowStockThreshold; // adjust name
|
||||
|
||||
final borderColor = outOfStock
|
||||
? scheme.error.withValues(alpha: 0.7)
|
||||
: lowStock
|
||||
? Colors.amber.withValues(alpha: 0.9)
|
||||
: scheme.onSurface.withValues(alpha: 0.08);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: borderColor,
|
||||
width: outOfStock || lowStock ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
child: Material(
|
||||
color: theme.cardTheme.color ?? scheme.surface,
|
||||
child: InkWell(
|
||||
onTap: enabled && !outOfStock ? onTap : null,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: outOfStock ? 0.35 : (enabled ? 1 : 0.4),
|
||||
child: _hasImage
|
||||
? Image.file(
|
||||
File(product.imagePath!),
|
||||
fit: BoxFit.scaleDown,
|
||||
)
|
||||
: Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported_outlined,
|
||||
size: 34,
|
||||
color: scheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (_hasImage)
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withValues(alpha: 0.35),
|
||||
],
|
||||
stops: const [0.6, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Stock badge
|
||||
Positioned(
|
||||
top: 10,
|
||||
right: 10,
|
||||
child: _StockBadge(
|
||||
product: product,
|
||||
),
|
||||
),
|
||||
|
||||
// Out of stock overlay
|
||||
if (outOfStock)
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.error.withValues(alpha: 0.9),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
'OUT OF STOCK',
|
||||
style: TextStyle(
|
||||
color: scheme.onError,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StockBadge extends StatelessWidget {
|
||||
final Product product;
|
||||
|
||||
const _StockBadge({
|
||||
required this.product,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
if (product.stockQuantity <= 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final low =
|
||||
product.stockQuantity <= product.lowStockThreshold; // adjust name
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: low
|
||||
? Colors.amber.withValues(alpha: 0.9)
|
||||
: scheme.primary.withValues(alpha: 0.85),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
'${product.stockQuantity}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: low ? Colors.black : scheme.onPrimary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class SlideConfirm extends StatefulWidget {
|
||||
final VoidCallback onConfirmed;
|
||||
|
||||
const SlideConfirm({required this.onConfirmed});
|
||||
|
||||
@override
|
||||
State<SlideConfirm> createState() => _SlideConfirmState();
|
||||
}
|
||||
|
||||
class _SlideConfirmState extends State<SlideConfirm> {
|
||||
double _drag = 0;
|
||||
bool _confirmed = false;
|
||||
|
||||
static const double size = 52;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 58,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxDrag = constraints.maxWidth - size;
|
||||
|
||||
return Container(
|
||||
height: 58,
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.error.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
_confirmed ? 'Closing tab...' : 'Slide to confirm closing',
|
||||
style: TextStyle(
|
||||
color: scheme.error,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Positioned(
|
||||
left: _drag,
|
||||
child: GestureDetector(
|
||||
onHorizontalDragUpdate: (details) {
|
||||
if (_confirmed) return;
|
||||
|
||||
setState(() {
|
||||
_drag += details.delta.dx;
|
||||
_drag = _drag.clamp(0, maxDrag);
|
||||
});
|
||||
},
|
||||
onHorizontalDragEnd: (_) {
|
||||
if (_drag >= maxDrag * 0.85) {
|
||||
setState(() {
|
||||
_confirmed = true;
|
||||
_drag = maxDrag;
|
||||
});
|
||||
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 250),
|
||||
widget.onConfirmed,
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
_drag = 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.error,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.arrow_forward_rounded,
|
||||
color: scheme.onError,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user