feat: add remote pull and push
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../utils/navigation.dart';
|
||||
import '../viewmodels/admin_pin_view_model.dart';
|
||||
import '../viewmodels/remote_sync_view_model.dart';
|
||||
|
||||
class AdminScreenView extends StatelessWidget {
|
||||
const AdminScreenView({super.key});
|
||||
@@ -15,9 +16,39 @@ class AdminScreenView extends StatelessWidget {
|
||||
adminPinViewModel.lock();
|
||||
}
|
||||
|
||||
Future<void> _confirmUnlink(BuildContext context) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Disconnect from server'),
|
||||
content: const Text(
|
||||
'This will unlink the device from the remote server. '
|
||||
'Open tabs will no longer be synced. Continue?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: Text(l10n.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
style: FilledButton.styleFrom(backgroundColor: Colors.red),
|
||||
child: const Text('Disconnect'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true && context.mounted) {
|
||||
context.read<RemoteSyncViewModel>().unlink();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final remoteSync = context.watch<RemoteSyncViewModel>();
|
||||
|
||||
return PopScope(
|
||||
onPopInvokedWithResult: (didPop, result) {
|
||||
@@ -68,6 +99,81 @@ class AdminScreenView extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_AdminSection(
|
||||
title: 'Remote Server',
|
||||
children: [
|
||||
if (remoteSync.isConfigured) ...[
|
||||
_AdminTile(
|
||||
icon: Icons.cloud_done_outlined,
|
||||
title: 'Connected',
|
||||
subtitle: 'Device linked to remote server',
|
||||
trailing: TextButton(
|
||||
onPressed: () => _confirmUnlink(context),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Disconnect'),
|
||||
),
|
||||
onTap: null,
|
||||
),
|
||||
] else
|
||||
_AdminTile(
|
||||
icon: Icons.cloud_outlined,
|
||||
title: 'Not connected',
|
||||
subtitle: 'Link to remote dashboard',
|
||||
onTap: () => context.push('/register'),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (remoteSync.isConfigured) ...[
|
||||
const SizedBox(height: 8),
|
||||
_AdminSection(
|
||||
title: l10n.remoteActivity,
|
||||
children: [
|
||||
_AdminTile(
|
||||
icon: Icons.sync,
|
||||
title: l10n.syncNow,
|
||||
subtitle: remoteSync.isSyncing
|
||||
? l10n.syncing
|
||||
: remoteSync.lastSyncAt != null
|
||||
? l10n.lastSync(remoteSync.lastSyncAt!.toLocal().toString().substring(0, 19))
|
||||
: l10n.notSyncedYet,
|
||||
trailing: remoteSync.isSyncing
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: null,
|
||||
onTap: () => remoteSync.syncNow(),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (remoteSync.activity.isNotEmpty)
|
||||
_AdminSection(
|
||||
title: l10n.remoteActivityHistory,
|
||||
children: remoteSync.activity.map((entry) {
|
||||
return ListTile(
|
||||
dense: true,
|
||||
leading: Icon(
|
||||
entry.result == 'completed'
|
||||
? Icons.check_circle_outline
|
||||
: entry.result == 'conflict'
|
||||
? Icons.warning_amber_outlined
|
||||
: Icons.error_outline,
|
||||
size: 20,
|
||||
),
|
||||
title: Text(
|
||||
'${entry.commandType} (${entry.commandId.substring(0, 8)}...)',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${entry.processedAt.toLocal()} - ${entry.result}',
|
||||
style: const TextStyle(fontSize: 11),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -121,12 +227,14 @@ class _AdminTile extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String? subtitle;
|
||||
final Widget? trailing;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const _AdminTile({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.trailing,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@@ -169,6 +277,10 @@ class _AdminTile extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trailing != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
trailing!,
|
||||
],
|
||||
if (onTap != null)
|
||||
Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
@@ -180,4 +292,4 @@ class _AdminTile extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import '../models/bar_tab.dart';
|
||||
import '../models/product.dart';
|
||||
import '../models/tab_item.dart';
|
||||
import '../models/remote_command.dart';
|
||||
import '../services/product_service.dart';
|
||||
import '../utils/app_updater.dart';
|
||||
import '../viewmodels/bar_screen_view_model.dart';
|
||||
@@ -38,6 +39,43 @@ class _BarScreenViewState extends State<BarScreenView> {
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_checkRemoteEvent();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant BarScreenView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_checkRemoteEvent();
|
||||
}
|
||||
|
||||
void _checkRemoteEvent() {
|
||||
final viewModel = context.read<BarScreenViewModel>();
|
||||
final event = viewModel.lastRemoteEvent;
|
||||
if (event == null) return;
|
||||
|
||||
viewModel.clearRemoteEvent();
|
||||
|
||||
if (event.type == RemoteSyncEventType.tabClosedRemotely) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final message = l10n.tabClosedRemotely(
|
||||
event.tabCustomerName ?? '',
|
||||
event.requestedBy ?? '',
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
@@ -83,7 +121,7 @@ class _BarScreenViewState extends State<BarScreenView> {
|
||||
const SizedBox(width: 6),
|
||||
IconButton(
|
||||
tooltip: l10n.refresh,
|
||||
onPressed: () => viewModel.load(),
|
||||
onPressed: () => viewModel.loadAndSync(),
|
||||
icon: const Icon(Icons.refresh_rounded),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
|
||||
@@ -83,7 +83,11 @@ class _PaymentPicker extends StatelessWidget {
|
||||
|
||||
const _PaymentPicker({required this.selected, required this.onChanged});
|
||||
|
||||
static const _options = [PaymentMethod.cash, PaymentMethod.payconiq];
|
||||
static const _options = [
|
||||
PaymentMethod.cash,
|
||||
PaymentMethod.payconiq,
|
||||
PaymentMethod.paymentPending,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -93,7 +97,11 @@ class _PaymentPicker extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: _options.map((option) {
|
||||
final value = option;
|
||||
final label = value == PaymentMethod.cash ? l10n.cash : l10n.payconiq;
|
||||
final label = switch (value) {
|
||||
PaymentMethod.cash => l10n.cash,
|
||||
PaymentMethod.payconiq => l10n.payconiq,
|
||||
PaymentMethod.paymentPending => l10n.paymentPending,
|
||||
};
|
||||
final isSelected = selected == value;
|
||||
|
||||
return Padding(
|
||||
@@ -111,7 +119,7 @@ class _PaymentPicker extends StatelessWidget {
|
||||
size: 16,
|
||||
color: Colors.lightGreen,
|
||||
)
|
||||
else
|
||||
else if (value == PaymentMethod.payconiq)
|
||||
SvgPicture.asset(
|
||||
'assets/icons/payconic.svg',
|
||||
width: 16,
|
||||
@@ -120,6 +128,12 @@ class _PaymentPicker extends StatelessWidget {
|
||||
Colors.pinkAccent,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
)
|
||||
else
|
||||
const Icon(
|
||||
Icons.hourglass_empty,
|
||||
size: 16,
|
||||
color: Colors.orange,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(label),
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../viewmodels/registration_view_model.dart';
|
||||
import '../viewmodels/remote_sync_view_model.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class RegistrationView extends StatefulWidget {
|
||||
const RegistrationView({super.key});
|
||||
|
||||
@override
|
||||
State<RegistrationView> createState() => _RegistrationViewState();
|
||||
}
|
||||
|
||||
class _RegistrationViewState extends State<RegistrationView> {
|
||||
final _deviceNameController = TextEditingController(text: 'Bar terminal 1');
|
||||
final _regCodeController = TextEditingController();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<RegistrationViewModel>().fetchVenues();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_deviceNameController.dispose();
|
||||
_regCodeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = context.watch<RegistrationViewModel>();
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
child: _buildStep(vm, l10n),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStep(RegistrationViewModel vm, AppLocalizations l10n) {
|
||||
switch (vm.step) {
|
||||
case RegistrationStep.loadingVenues:
|
||||
return _buildLoading();
|
||||
case RegistrationStep.error:
|
||||
return _buildError(vm);
|
||||
case RegistrationStep.venuesLoaded:
|
||||
if (vm.selectedVenue == null) {
|
||||
return _buildVenueSelection(vm, l10n);
|
||||
}
|
||||
return _buildDeviceDetails(vm, l10n);
|
||||
case RegistrationStep.registering:
|
||||
return _buildRegistering();
|
||||
case RegistrationStep.registered:
|
||||
return _buildSuccess(vm, l10n);
|
||||
case RegistrationStep.initial:
|
||||
return _buildLoading();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildLoading() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
Icon(
|
||||
Icons.cloud_outlined,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const CircularProgressIndicator(strokeWidth: 3),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Connecting to server…',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildError(RegistrationViewModel vm) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
Icon(
|
||||
Icons.cloud_off_rounded,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Failed to connect',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
vm.errorMessage ?? 'Unknown error',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: () => vm.retry(),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVenueSelection(RegistrationViewModel vm, AppLocalizations l10n) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Icon(
|
||||
Icons.cloud_outlined,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Connect to remote server',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Select your venue to link this device',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
'Select venue',
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.15),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
for (final venue in vm.venues)
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => vm.selectVenue(venue),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.store_outlined,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
venue.name,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right_rounded,
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextButton(
|
||||
onPressed: () => vm.retry(),
|
||||
child: const Text('Refresh venues'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeviceDetails(RegistrationViewModel vm, AppLocalizations l10n) {
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Icon(
|
||||
Icons.store_outlined,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
vm.selectedVenue!.name,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Enter device details to complete pairing',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
TextFormField(
|
||||
controller: _deviceNameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Device name',
|
||||
hintText: 'e.g. Bar terminal 1',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
validator: (v) => v == null || v.trim().isEmpty ? 'Device name is required' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _regCodeController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Registration code',
|
||||
hintText: '6-character code from dashboard',
|
||||
border: OutlineInputBorder(),
|
||||
counterText: '',
|
||||
),
|
||||
maxLength: 6,
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
validator: (v) => v == null || v.trim().length < 6 ? 'Enter the 6-character code' : null,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (vm.errorMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Text(
|
||||
vm.errorMessage!,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
vm.reset();
|
||||
},
|
||||
child: const Text('Back'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: () async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
await vm.registerDevice(
|
||||
deviceName: _deviceNameController.text.trim(),
|
||||
registrationCode: _regCodeController.text.trim().toUpperCase(),
|
||||
);
|
||||
},
|
||||
child: const Text('Register'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRegistering() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
const CircularProgressIndicator(strokeWidth: 3),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Registering device…',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuccess(RegistrationViewModel vm, AppLocalizations l10n) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Icon(
|
||||
Icons.check_circle_rounded,
|
||||
size: 80,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Device registered!',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'${vm.selectedVenue?.name ?? "Venue"} is now linked.',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Open tabs will sync to the dashboard.',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
FilledButton.icon(
|
||||
onPressed: () {
|
||||
context.read<RemoteSyncViewModel>().startSync();
|
||||
context.go('/bar');
|
||||
},
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
label: const Text('Start using KoolTab'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ class _ClosedTabCardState extends State<ClosedTabCard> {
|
||||
return switch (method) {
|
||||
PaymentMethod.payconiq => Icons.qr_code_rounded,
|
||||
PaymentMethod.cash => Icons.money_rounded,
|
||||
PaymentMethod.paymentPending => Icons.hourglass_empty,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -102,6 +103,7 @@ class _ClosedTabCardState extends State<ClosedTabCard> {
|
||||
switch (closedTab.paymentMethod) {
|
||||
PaymentMethod.cash => l10n.cash,
|
||||
PaymentMethod.payconiq => l10n.payconiq,
|
||||
PaymentMethod.paymentPending => l10n.paymentPending,
|
||||
},
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
|
||||
Reference in New Issue
Block a user