Files
nearle_pos/lib/presentation/customer/screens/customer_registration_screen.dart
2026-07-29 11:41:53 +05:30

322 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../app/providers.dart';
import '../../../core/router/app_router.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/utils/extensions.dart';
import '../../../core/utils/formatters.dart';
import '../../../core/utils/validators.dart';
import '../../../core/widgets/glass_card.dart';
import '../../../core/widgets/primary_button.dart';
import '../../../domain/entities/customer.dart';
import '../../pos/providers/cart_controller.dart';
import '../providers/customer_providers.dart';
/// Screen 2 — registers a shopper and drops straight into billing.
class CustomerRegistrationScreen extends ConsumerStatefulWidget {
const CustomerRegistrationScreen({super.key, this.prefillMobile});
final String? prefillMobile;
@override
ConsumerState<CustomerRegistrationScreen> createState() =>
_CustomerRegistrationScreenState();
}
class _CustomerRegistrationScreenState
extends ConsumerState<CustomerRegistrationScreen> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _mobile;
final _name = TextEditingController();
final _email = TextEditingController();
Gender _gender = Gender.unspecified;
DateTime? _dob;
bool _saving = false;
String? _serverError;
@override
void initState() {
super.initState();
_mobile = TextEditingController(text: widget.prefillMobile ?? '');
}
@override
void dispose() {
_mobile.dispose();
_name.dispose();
_email.dispose();
super.dispose();
}
Future<void> _save() async {
setState(() => _serverError = null);
if (!(_formKey.currentState?.validate() ?? false)) return;
setState(() => _saving = true);
try {
final customer = await ref.read(customerRepositoryProvider).create(
Customer(
id: '',
name: _name.text,
mobile: _mobile.text,
email: _email.text,
gender: _gender,
dateOfBirth: _dob,
),
);
ref.read(cartControllerProvider.notifier).attachCustomer(customer);
ref.invalidate(recentCustomersProvider);
if (!mounted) return;
context.go(AppRoutes.pos);
} catch (e) {
if (!mounted) return;
setState(() {
_saving = false;
_serverError = e is StateError ? e.message : 'Could not save customer.';
});
}
}
Future<void> _pickDob() async {
final now = DateTime.now();
final picked = await showDatePicker(
context: context,
initialDate: _dob ?? DateTime(now.year - 25),
firstDate: DateTime(now.year - 100),
lastDate: now,
helpText: 'Date of birth',
);
if (picked != null) setState(() => _dob = picked);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
title: const Text('New Customer'),
leading: IconButton(
icon: const Icon(Icons.arrow_back_rounded),
onPressed: () => context.pop(),
),
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(AppSpacing.xxl),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 640),
child: GlassCard(
padding: const EdgeInsets.all(AppSpacing.xxxl),
radius: AppRadius.xl,
shadows: AppColors.shadowMd,
child: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Register a shopper',
style: context.text.headlineSmall),
const SizedBox(height: AppSpacing.xs),
Text(
'Only the mobile number and name are required.',
style: context.text.bodySmall,
),
const SizedBox(height: AppSpacing.xxl),
_Field(
label: 'Mobile Number',
required: true,
child: TextFormField(
controller: _mobile,
autofocus: true,
keyboardType: TextInputType.phone,
maxLength: 10,
validator: Validators.mobile,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
decoration: const InputDecoration(
hintText: '10-digit mobile number',
prefixText: '+91 ',
counterText: '',
prefixIcon: Icon(Icons.phone_outlined),
),
),
),
_Field(
label: 'Customer Name',
required: true,
child: TextFormField(
controller: _name,
textCapitalization: TextCapitalization.words,
validator: Validators.name,
decoration: const InputDecoration(
hintText: 'Full name',
prefixIcon: Icon(Icons.person_outline_rounded),
),
),
),
_Field(
label: 'Email',
child: TextFormField(
controller: _email,
keyboardType: TextInputType.emailAddress,
validator: Validators.emailOptional,
decoration: const InputDecoration(
hintText: 'name@example.com',
prefixIcon: Icon(Icons.mail_outline_rounded),
),
),
),
_Field(
label: 'Gender',
child: Wrap(
spacing: AppSpacing.sm,
children: Gender.values
.map((g) => ChoiceChip(
label: Text(g.label),
selected: _gender == g,
onSelected: (_) =>
setState(() => _gender = g),
labelStyle: TextStyle(
color: _gender == g
? Colors.white
: AppColors.textSecondary,
fontWeight: FontWeight.w600,
),
))
.toList(),
),
),
_Field(
label: 'Date of Birth',
child: InkWell(
onTap: _pickDob,
borderRadius: AppRadius.brMd,
child: InputDecorator(
decoration: const InputDecoration(
prefixIcon: Icon(Icons.cake_outlined),
),
child: Text(
_dob == null
? 'Select a date (optional)'
: Formatters.date(_dob!),
style: TextStyle(
color: _dob == null
? AppColors.textTertiary
: AppColors.textPrimary,
fontSize: 15,
),
),
),
),
),
if (_serverError != null) ...[
const SizedBox(height: AppSpacing.sm),
Container(
width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: AppColors.dangerSurface,
borderRadius: AppRadius.brSm,
),
child: Row(children: [
const Icon(Icons.error_outline_rounded,
color: AppColors.danger, size: 18),
const SizedBox(width: AppSpacing.sm),
Expanded(
child: Text(
_serverError!,
style: const TextStyle(
color: AppColors.danger,
fontSize: 13.5,
),
),
),
]),
),
],
const SizedBox(height: AppSpacing.xxl),
Row(children: [
Expanded(
child: PrimaryButton(
label: 'Cancel',
tone: ButtonTone.neutral,
onPressed:
_saving ? null : () => context.pop(),
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
flex: 2,
child: PrimaryButton(
label: 'Save & Continue',
icon: Icons.check_rounded,
busy: _saving,
onPressed: _save,
),
),
]),
],
),
),
),
),
),
),
);
}
}
class _Field extends StatelessWidget {
const _Field({
required this.label,
required this.child,
this.required = false,
});
final String label;
final Widget child;
final bool required;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.lg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Text(label,
style: context.text.labelMedium
?.copyWith(color: AppColors.textSecondary)),
if (required)
const Text(' *',
style: TextStyle(color: AppColors.danger, fontSize: 13)),
if (!required)
Text(' optional',
style: context.text.labelSmall
?.copyWith(color: AppColors.textTertiary)),
]),
const SizedBox(height: AppSpacing.sm),
child,
],
),
);
}
}