second commit
This commit is contained in:
337
lib/presentation/welcome/screens/welcome_screen.dart
Normal file
337
lib/presentation/welcome/screens/welcome_screen.dart
Normal file
@@ -0,0 +1,337 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../app/providers.dart';
|
||||
import '../../../core/constants/app_constants.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/widgets/glass_card.dart';
|
||||
import '../../pos/providers/cart_controller.dart';
|
||||
import '../widgets/welcome_illustration.dart';
|
||||
|
||||
/// Screen 1 — the terminal's resting state between sales.
|
||||
class WelcomeScreen extends ConsumerWidget {
|
||||
const WelcomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final session = ref.watch(cashierSessionProvider);
|
||||
final clock = ref.watch(clockProvider).value ?? DateTime.now();
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(gradient: AppColors.primaryGradient),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
_TopBar(cashier: session.name, now: clock),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppSpacing.xxl),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 920),
|
||||
child: GlassCard(
|
||||
blur: 18,
|
||||
padding: EdgeInsets.all(
|
||||
context.responsive(
|
||||
compact: AppSpacing.xxl,
|
||||
expanded: AppSpacing.giant,
|
||||
),
|
||||
),
|
||||
radius: AppRadius.xxl,
|
||||
shadows: AppColors.shadowLg,
|
||||
child: context.isCompact
|
||||
? const _StackedLayout()
|
||||
: const _SideBySideLayout(),
|
||||
),
|
||||
),
|
||||
).animate().fadeIn(duration: 350.ms).slideY(
|
||||
begin: 0.04,
|
||||
end: 0,
|
||||
curve: Curves.easeOutCubic,
|
||||
),
|
||||
),
|
||||
),
|
||||
const _BottomHint(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SideBySideLayout extends StatelessWidget {
|
||||
const _SideBySideLayout();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: const [
|
||||
Expanded(flex: 4, child: WelcomeIllustration(size: 260)),
|
||||
SizedBox(width: AppSpacing.giant),
|
||||
Expanded(flex: 5, child: _WelcomeContent()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StackedLayout extends StatelessWidget {
|
||||
const _StackedLayout();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
WelcomeIllustration(size: 150),
|
||||
SizedBox(height: AppSpacing.xxl),
|
||||
_WelcomeContent(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WelcomeContent extends ConsumerWidget {
|
||||
const _WelcomeContent();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Welcome to',
|
||||
style: context.text.titleMedium?.copyWith(
|
||||
color: AppColors.textSecondary,
|
||||
letterSpacing: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'Nearle POS',
|
||||
style: context.text.displaySmall?.copyWith(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
'Start a new sale by identifying the shopper, '
|
||||
'or skip straight to billing.',
|
||||
style: context.text.bodyMedium
|
||||
?.copyWith(color: AppColors.textSecondary),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xxxl),
|
||||
|
||||
_WelcomeAction(
|
||||
icon: Icons.person_add_alt_1_rounded,
|
||||
title: 'New Customer',
|
||||
subtitle: 'Register and start earning loyalty points',
|
||||
onTap: () => context.push(AppRoutes.registerCustomer),
|
||||
primary: true,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_WelcomeAction(
|
||||
icon: Icons.badge_outlined,
|
||||
title: 'Existing Customer',
|
||||
subtitle: 'Look up by mobile number',
|
||||
onTap: () => context.push(AppRoutes.existingCustomer),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_WelcomeAction(
|
||||
icon: Icons.directions_walk_rounded,
|
||||
title: 'Skip Customer',
|
||||
subtitle: 'Walk-in sale, no loyalty tracking',
|
||||
onTap: () {
|
||||
ref.read(cartControllerProvider.notifier).reset();
|
||||
context.go(AppRoutes.pos);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A tall, unmistakable target — the cashier taps this hundreds of times a day.
|
||||
class _WelcomeAction extends StatefulWidget {
|
||||
const _WelcomeAction({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.onTap,
|
||||
this.primary = false,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final VoidCallback onTap;
|
||||
final bool primary;
|
||||
|
||||
@override
|
||||
State<_WelcomeAction> createState() => _WelcomeActionState();
|
||||
}
|
||||
|
||||
class _WelcomeActionState extends State<_WelcomeAction> {
|
||||
bool _hovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bg = widget.primary
|
||||
? AppColors.primary
|
||||
: (_hovered ? AppColors.primarySurface : AppColors.surface);
|
||||
final fg =
|
||||
widget.primary ? AppColors.textOnPrimary : AppColors.textPrimary;
|
||||
final sub = widget.primary
|
||||
? AppColors.textOnPrimary.withValues(alpha: 0.78)
|
||||
: AppColors.textSecondary;
|
||||
|
||||
return MouseRegion(
|
||||
onEnter: (_) => setState(() => _hovered = true),
|
||||
onExit: (_) => setState(() => _hovered = false),
|
||||
child: AnimatedContainer(
|
||||
duration: AppMotion.fast,
|
||||
transform: Matrix4.translationValues(0, _hovered ? -2 : 0, 0),
|
||||
child: Material(
|
||||
color: bg,
|
||||
borderRadius: AppRadius.brLg,
|
||||
child: InkWell(
|
||||
onTap: widget.onTap,
|
||||
borderRadius: AppRadius.brLg,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.xl,
|
||||
vertical: AppSpacing.lg,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: AppRadius.brLg,
|
||||
border: Border.all(
|
||||
color: widget.primary
|
||||
? Colors.transparent
|
||||
: AppColors.border,
|
||||
),
|
||||
boxShadow: widget.primary && _hovered
|
||||
? AppColors.shadowMd
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 46,
|
||||
height: 46,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.primary
|
||||
? Colors.white.withValues(alpha: 0.18)
|
||||
: AppColors.primarySurface,
|
||||
borderRadius: AppRadius.brMd,
|
||||
),
|
||||
child: Icon(
|
||||
widget.icon,
|
||||
color: widget.primary
|
||||
? AppColors.textOnPrimary
|
||||
: AppColors.primary,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.lg),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.title,
|
||||
style: context.text.titleMedium?.copyWith(color: fg),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
widget.subtitle,
|
||||
style: context.text.bodySmall?.copyWith(color: sub),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.arrow_forward_rounded, color: sub, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TopBar extends StatelessWidget {
|
||||
const _TopBar({required this.cashier, required this.now});
|
||||
|
||||
final String cashier;
|
||||
final DateTime now;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.xxl,
|
||||
vertical: AppSpacing.lg,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.storefront_rounded,
|
||||
color: Colors.white, size: 26),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Text(
|
||||
AppConstants.storeName,
|
||||
style: context.text.titleLarge?.copyWith(color: Colors.white),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${Formatters.date(now)} ${Formatters.time(now)}',
|
||||
style: context.text.bodyMedium
|
||||
?.copyWith(color: Colors.white.withValues(alpha: 0.85)),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.xxl),
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Colors.white.withValues(alpha: 0.2),
|
||||
child: Text(
|
||||
Formatters.initials(cashier),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Text(cashier,
|
||||
style: context.text.bodyMedium?.copyWith(color: Colors.white)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomHint extends StatelessWidget {
|
||||
const _BottomHint();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: AppSpacing.xl),
|
||||
child: Text(
|
||||
'Scan a barcode at any time to begin a walk-in sale',
|
||||
style: context.text.bodySmall
|
||||
?.copyWith(color: Colors.white.withValues(alpha: 0.7)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user