second commit

This commit is contained in:
2026-07-29 11:41:53 +05:30
parent fcccf22bac
commit d72522e737
211 changed files with 19260 additions and 0 deletions

View 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)),
),
);
}
}

View File

@@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import '../../../core/theme/app_colors.dart';
/// Vector shopping-cart illustration drawn in code.
///
/// Painting it avoids shipping a raster asset and keeps it crisp on 4K desktop
/// displays as well as tablet screens.
class WelcomeIllustration extends StatelessWidget {
const WelcomeIllustration({super.key, this.size = 240});
final double size;
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: CustomPaint(painter: _CartPainter()),
)
.animate(onPlay: (c) => c.repeat(reverse: true))
.moveY(begin: 0, end: -8, duration: 2400.ms, curve: Curves.easeInOut);
}
}
class _CartPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final w = size.width;
final h = size.height;
final unit = w / 100;
// Soft backdrop disc.
canvas.drawCircle(
Offset(w * 0.5, h * 0.5),
w * 0.46,
Paint()..color = AppColors.primarySurface,
);
// Decorative arc.
canvas.drawArc(
Rect.fromCircle(center: Offset(w * 0.5, h * 0.5), radius: w * 0.46),
-1.1,
2.0,
false,
Paint()
..color = AppColors.primaryBorder
..style = PaintingStyle.stroke
..strokeWidth = unit * 1.6
..strokeCap = StrokeCap.round,
);
final stroke = Paint()
..color = AppColors.primary
..style = PaintingStyle.stroke
..strokeWidth = unit * 3
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
final fill = Paint()..color = AppColors.primary.withValues(alpha: 0.16);
// Cart basket.
final basket = Path()
..moveTo(w * 0.30, h * 0.36)
..lineTo(w * 0.78, h * 0.36)
..lineTo(w * 0.70, h * 0.60)
..lineTo(w * 0.37, h * 0.60)
..close();
canvas.drawPath(basket, fill);
canvas.drawPath(basket, stroke);
// Handle running to the push bar.
canvas.drawPath(
Path()
..moveTo(w * 0.16, h * 0.26)
..lineTo(w * 0.24, h * 0.26)
..lineTo(w * 0.30, h * 0.36),
stroke,
);
// Basket ribs.
for (var i = 1; i <= 2; i++) {
final t = i / 3;
canvas.drawLine(
Offset(w * (0.30 + 0.48 * t), h * 0.36),
Offset(w * (0.37 + 0.33 * t), h * 0.60),
stroke..strokeWidth = unit * 1.6,
);
}
stroke.strokeWidth = unit * 3;
// Wheels.
for (final dx in [0.44, 0.66]) {
canvas.drawCircle(
Offset(w * dx, h * 0.70),
unit * 5,
Paint()..color = AppColors.surface,
);
canvas.drawCircle(Offset(w * dx, h * 0.70), unit * 5, stroke);
}
// Groceries poking out of the basket.
_item(canvas, Offset(w * 0.42, h * 0.30), unit * 5.5,
AppColors.tierGold.withValues(alpha: 0.9));
_item(canvas, Offset(w * 0.55, h * 0.27), unit * 6.5,
AppColors.success.withValues(alpha: 0.85));
_item(canvas, Offset(w * 0.67, h * 0.31), unit * 5,
AppColors.danger.withValues(alpha: 0.75));
// Receipt tape drifting away from the terminal.
final receipt = Path()
..moveTo(w * 0.80, h * 0.20)
..lineTo(w * 0.94, h * 0.20)
..lineTo(w * 0.94, h * 0.44)
..lineTo(w * 0.905, h * 0.40)
..lineTo(w * 0.87, h * 0.44)
..lineTo(w * 0.835, h * 0.40)
..lineTo(w * 0.80, h * 0.44)
..close();
canvas.drawPath(receipt, Paint()..color = AppColors.surface);
canvas.drawPath(
receipt,
Paint()
..color = AppColors.primaryLight
..style = PaintingStyle.stroke
..strokeWidth = unit * 1.4
..strokeJoin = StrokeJoin.round,
);
// Receipt lines.
final line = Paint()
..color = AppColors.primaryBorder
..strokeWidth = unit * 1.2
..strokeCap = StrokeCap.round;
for (var i = 0; i < 3; i++) {
final y = h * (0.25 + i * 0.05);
canvas.drawLine(Offset(w * 0.835, y), Offset(w * 0.905, y), line);
}
}
void _item(Canvas canvas, Offset center, double radius, Color color) {
canvas.drawCircle(center, radius, Paint()..color = color);
canvas.drawCircle(
center,
radius,
Paint()
..color = Colors.white.withValues(alpha: 0.5)
..style = PaintingStyle.stroke
..strokeWidth = 1.5,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}