runable
This commit is contained in:
334
lib/features/scanner/presentation/pages/detail_page.dart
Normal file
334
lib/features/scanner/presentation/pages/detail_page.dart
Normal file
@@ -0,0 +1,334 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../data/models/scan_item.dart';
|
||||
import '../providers/form_provider.dart';
|
||||
import '../providers/scanner_provider.dart';
|
||||
|
||||
/// Detail page for editing scan data with 4 text fields and Save/Print buttons
|
||||
class DetailPage extends ConsumerStatefulWidget {
|
||||
final String barcode;
|
||||
|
||||
const DetailPage({
|
||||
required this.barcode,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<DetailPage> createState() => _DetailPageState();
|
||||
}
|
||||
|
||||
class _DetailPageState extends ConsumerState<DetailPage> {
|
||||
late final TextEditingController _field1Controller;
|
||||
late final TextEditingController _field2Controller;
|
||||
late final TextEditingController _field3Controller;
|
||||
late final TextEditingController _field4Controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_field1Controller = TextEditingController();
|
||||
_field2Controller = TextEditingController();
|
||||
_field3Controller = TextEditingController();
|
||||
_field4Controller = TextEditingController();
|
||||
|
||||
// Initialize controllers with existing data if available
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_loadExistingData();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_field1Controller.dispose();
|
||||
_field2Controller.dispose();
|
||||
_field3Controller.dispose();
|
||||
_field4Controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Load existing data from history if available
|
||||
void _loadExistingData() {
|
||||
final history = ref.read(scanHistoryProvider);
|
||||
final existingScan = history.firstWhere(
|
||||
(item) => item.barcode == widget.barcode,
|
||||
orElse: () => ScanItem(barcode: widget.barcode, timestamp: DateTime.now()),
|
||||
);
|
||||
|
||||
_field1Controller.text = existingScan.field1;
|
||||
_field2Controller.text = existingScan.field2;
|
||||
_field3Controller.text = existingScan.field3;
|
||||
_field4Controller.text = existingScan.field4;
|
||||
|
||||
// Update form provider with existing data
|
||||
final formNotifier = ref.read(formProviderFamily(widget.barcode).notifier);
|
||||
formNotifier.populateWithScanItem(existingScan);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final formState = ref.watch(formProviderFamily(widget.barcode));
|
||||
final formNotifier = ref.read(formProviderFamily(widget.barcode).notifier);
|
||||
|
||||
// Listen to form state changes for navigation
|
||||
ref.listen<FormDetailState>(
|
||||
formProviderFamily(widget.barcode),
|
||||
(previous, next) {
|
||||
if (next.isSaveSuccess && (previous?.isSaveSuccess != true)) {
|
||||
_showSuccessAndNavigateBack(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Edit Details'),
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Barcode Header
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Barcode',
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.barcode,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Form Fields
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Field 1
|
||||
_buildTextField(
|
||||
controller: _field1Controller,
|
||||
label: 'Field 1',
|
||||
onChanged: formNotifier.updateField1,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Field 2
|
||||
_buildTextField(
|
||||
controller: _field2Controller,
|
||||
label: 'Field 2',
|
||||
onChanged: formNotifier.updateField2,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Field 3
|
||||
_buildTextField(
|
||||
controller: _field3Controller,
|
||||
label: 'Field 3',
|
||||
onChanged: formNotifier.updateField3,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Field 4
|
||||
_buildTextField(
|
||||
controller: _field4Controller,
|
||||
label: 'Field 4',
|
||||
onChanged: formNotifier.updateField4,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Error Message
|
||||
if (formState.error != null) ...[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
formState.error!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Action Buttons
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Theme.of(context).dividerColor,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Row(
|
||||
children: [
|
||||
// Save Button
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: formState.isLoading ? null : () => _saveData(formNotifier),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
),
|
||||
child: formState.isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: const Text('Save'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Print Button
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: formState.isLoading ? null : () => _printData(formNotifier),
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size.fromHeight(48),
|
||||
),
|
||||
child: const Text('Print'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build text field widget
|
||||
Widget _buildTextField({
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required void Function(String) onChanged,
|
||||
}) {
|
||||
return TextField(
|
||||
controller: controller,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.3),
|
||||
),
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
}
|
||||
|
||||
/// Save form data
|
||||
Future<void> _saveData(FormNotifier formNotifier) async {
|
||||
// Clear any previous errors
|
||||
formNotifier.clearError();
|
||||
|
||||
// Attempt to save
|
||||
await formNotifier.saveData();
|
||||
}
|
||||
|
||||
/// Print form data
|
||||
Future<void> _printData(FormNotifier formNotifier) async {
|
||||
try {
|
||||
await formNotifier.printData();
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Print dialog opened'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Print failed: ${e.toString()}'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show success message and navigate back
|
||||
void _showSuccessAndNavigateBack(BuildContext context) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Data saved successfully!'),
|
||||
backgroundColor: Colors.green,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
|
||||
// Navigate back after a short delay
|
||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||
if (mounted) {
|
||||
context.pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
193
lib/features/scanner/presentation/pages/home_page.dart
Normal file
193
lib/features/scanner/presentation/pages/home_page.dart
Normal file
@@ -0,0 +1,193 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../providers/scanner_provider.dart';
|
||||
import '../widgets/barcode_scanner_widget.dart';
|
||||
import '../widgets/scan_result_display.dart';
|
||||
import '../widgets/scan_history_list.dart';
|
||||
|
||||
/// Home page with barcode scanner, result display, and history list
|
||||
class HomePage extends ConsumerWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final scannerState = ref.watch(scannerProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Barcode Scanner'),
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () {
|
||||
ref.read(scannerProvider.notifier).refreshHistory();
|
||||
},
|
||||
tooltip: 'Refresh History',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Barcode Scanner Section (Top Half)
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const BarcodeScannerWidget(),
|
||||
),
|
||||
),
|
||||
|
||||
// Scan Result Display
|
||||
ScanResultDisplay(
|
||||
barcode: scannerState.currentBarcode,
|
||||
onTap: scannerState.currentBarcode != null
|
||||
? () => _navigateToDetail(context, scannerState.currentBarcode!)
|
||||
: null,
|
||||
),
|
||||
|
||||
// Divider
|
||||
const Divider(height: 1),
|
||||
|
||||
// History Section (Bottom Half)
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// History Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Scan History',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (scannerState.history.isNotEmpty)
|
||||
Text(
|
||||
'${scannerState.history.length} items',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// History List
|
||||
Expanded(
|
||||
child: _buildHistorySection(context, ref, scannerState),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build history section based on current state
|
||||
Widget _buildHistorySection(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
ScannerState scannerState,
|
||||
) {
|
||||
if (scannerState.isLoading) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (scannerState.error != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Error loading history',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
scannerState.error!,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
ref.read(scannerProvider.notifier).refreshHistory();
|
||||
},
|
||||
child: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (scannerState.history.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.qr_code_scanner,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No scans yet',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Start scanning barcodes to see your history here',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ScanHistoryList(
|
||||
history: scannerState.history,
|
||||
onItemTap: (scanItem) => _navigateToDetail(context, scanItem.barcode),
|
||||
);
|
||||
}
|
||||
|
||||
/// Navigate to detail page with barcode
|
||||
void _navigateToDetail(BuildContext context, String barcode) {
|
||||
context.push('/detail/$barcode');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user