From f0dac56802a25d46776e1e3b5dc0d67cdfe2bee9 Mon Sep 17 00:00:00 2001 From: efrilm Date: Tue, 12 Aug 2025 20:44:27 +0700 Subject: [PATCH] feat: home page --- android/app/build.gradle.kts | 2 +- lib/common/theme/app_color.dart | 2 +- lib/common/theme/app_value.dart | 7 +- lib/common/theme/theme.dart | 8 + lib/presentation/pages/home/home_page.dart | 216 +++++++++++++- .../pages/home/widgets/activity.dart | 95 ++++++ .../pages/home/widgets/activity_tile.dart | 103 +++++++ .../pages/home/widgets/feature.dart | 96 ++++++ .../pages/home/widgets/feature_tile.dart | 62 ++++ .../pages/home/widgets/header.dart | 186 ++++++++++++ .../pages/home/widgets/performance.dart | 281 ++++++++++++++++++ .../pages/home/widgets/stats.dart | 122 ++++++++ .../pages/home/widgets/stats_tile.dart | 111 +++++++ lib/presentation/pages/home/widgets/task.dart | 96 ++++++ .../pages/home/widgets/task_tile.dart | 68 +++++ 15 files changed, 1447 insertions(+), 8 deletions(-) create mode 100644 lib/presentation/pages/home/widgets/activity.dart create mode 100644 lib/presentation/pages/home/widgets/activity_tile.dart create mode 100644 lib/presentation/pages/home/widgets/feature.dart create mode 100644 lib/presentation/pages/home/widgets/feature_tile.dart create mode 100644 lib/presentation/pages/home/widgets/header.dart create mode 100644 lib/presentation/pages/home/widgets/performance.dart create mode 100644 lib/presentation/pages/home/widgets/stats.dart create mode 100644 lib/presentation/pages/home/widgets/stats_tile.dart create mode 100644 lib/presentation/pages/home/widgets/task.dart create mode 100644 lib/presentation/pages/home/widgets/task_tile.dart diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a64dad4..fd02140 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -8,7 +8,7 @@ plugins { android { namespace = "com.apskel.apskel_owner" compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + ndkVersion = "27.0.12077973" compileOptions { sourceCompatibility = JavaVersion.VERSION_11 diff --git a/lib/common/theme/app_color.dart b/lib/common/theme/app_color.dart index c2b1798..ab8f9b2 100644 --- a/lib/common/theme/app_color.dart +++ b/lib/common/theme/app_color.dart @@ -12,7 +12,7 @@ class AppColor { static const Color secondaryDark = Color(0xFF388E3C); // Background Colors - static const Color background = Color(0xFFF5F5F5); + static const Color background = Color(0xFFF8F9FA); static const Color backgroundLight = Color(0xFFFFFFFF); static const Color backgroundDark = Color(0xFF1A1A1A); static const Color surface = Color(0xFFFFFFFF); diff --git a/lib/common/theme/app_value.dart b/lib/common/theme/app_value.dart index 53ed1a6..327463c 100644 --- a/lib/common/theme/app_value.dart +++ b/lib/common/theme/app_value.dart @@ -1,8 +1,7 @@ part of 'theme.dart'; class AppValue { - static const double padding = 16.0; - static const double margin = 16.0; - static const double radius = 8.0; - static const double elevation = 4.0; + static const double padding = 20.0; + static const double margin = 20.0; + static const double radius = 16.0; } diff --git a/lib/common/theme/theme.dart b/lib/common/theme/theme.dart index f8c9585..fff9fac 100644 --- a/lib/common/theme/theme.dart +++ b/lib/common/theme/theme.dart @@ -55,5 +55,13 @@ class ThemeApp { fontWeight: FontWeight.w500, ), ), + appBarTheme: AppBarTheme( + backgroundColor: AppColor.white, + elevation: 1, + titleTextStyle: AppStyle.lg.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.primary, + ), + ), ); } diff --git a/lib/presentation/pages/home/home_page.dart b/lib/presentation/pages/home/home_page.dart index 7e4b3ad..879e1c8 100644 --- a/lib/presentation/pages/home/home_page.dart +++ b/lib/presentation/pages/home/home_page.dart @@ -1,12 +1,224 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import '../../../common/theme/theme.dart'; +import '../../components/spacer/spacer.dart'; +import 'widgets/activity.dart'; +import 'widgets/feature.dart'; +import 'widgets/header.dart'; +import 'widgets/performance.dart'; +import 'widgets/stats.dart'; +import 'widgets/task.dart'; + @RoutePage() -class HomePage extends StatelessWidget { +class HomePage extends StatefulWidget { const HomePage({super.key}); + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State with TickerProviderStateMixin { + late ScrollController _scrollController; + late AnimationController _headerAnimationController; + late AnimationController _contentAnimationController; + late AnimationController _appBarAnimationController; + + late Animation _appBarOpacityAnimation; + late Animation _appBarSlideAnimation; + + bool _showAppBar = false; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + _scrollController.addListener(_scrollListener); + + _headerAnimationController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + _contentAnimationController = AnimationController( + duration: const Duration(milliseconds: 1200), + vsync: this, + ); + + // AppBar Animation Controller + _appBarAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + // AppBar Animations + _appBarOpacityAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _appBarAnimationController, + curve: Curves.easeInOut, + ), + ); + + _appBarSlideAnimation = + Tween(begin: const Offset(0.0, -1.0), end: Offset.zero).animate( + CurvedAnimation( + parent: _appBarAnimationController, + curve: Curves.easeOutCubic, + ), + ); + + _startAnimations(); + } + + void _startAnimations() { + _headerAnimationController.forward(); + Future.delayed(const Duration(milliseconds: 200), () { + _contentAnimationController.forward(); + }); + } + + void _scrollListener() { + const double threshold = 200.0; + + if (_scrollController.offset > threshold && !_showAppBar) { + setState(() { + _showAppBar = true; + }); + _appBarAnimationController.forward(); + } else if (_scrollController.offset <= threshold && _showAppBar) { + _appBarAnimationController.reverse().then((_) { + if (mounted) { + setState(() { + _showAppBar = false; + }); + } + }); + } + } + + @override + void dispose() { + _scrollController.removeListener(_scrollListener); + _scrollController.dispose(); + _headerAnimationController.dispose(); + _contentAnimationController.dispose(); + _appBarAnimationController.dispose(); + super.dispose(); + } + + PreferredSizeWidget? _buildAnimatedAppBar() { + if (!_showAppBar) return null; + + return PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: SlideTransition( + position: _appBarSlideAnimation, + child: FadeTransition( + opacity: _appBarOpacityAnimation, + child: AppBar( + title: const Text( + 'AppSkel POS Owner', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 18, + letterSpacing: -0.5, + ), + ), + backgroundColor: AppColor.white, + foregroundColor: AppColor.textPrimary, + elevation: 0, + scrolledUnderElevation: 8, + shadowColor: AppColor.primary.withOpacity(0.1), + actions: [ + Container( + margin: const EdgeInsets.only(right: 16), + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColor.primary.withOpacity(0.1), + AppColor.primaryLight.withOpacity(0.05), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppColor.primary.withOpacity(0.1), + width: 1, + ), + ), + child: const Icon( + Icons.notifications_none_rounded, + color: AppColor.primary, + size: 20, + ), + ), + ], + ), + ), + ), + ); + } + @override Widget build(BuildContext context) { - return Center(child: Text('HomePage')); + return Scaffold( + backgroundColor: AppColor.background, + appBar: _buildAnimatedAppBar(), + body: CustomScrollView( + controller: _scrollController, + physics: const BouncingScrollPhysics(), + slivers: [ + // Enhanced Header + SliverToBoxAdapter( + child: AnimatedBuilder( + animation: _headerAnimationController, + builder: (context, child) { + return Transform.translate( + offset: Offset( + 0, + 50 * (1 - _headerAnimationController.value), + ), + child: Opacity( + opacity: _headerAnimationController.value, + child: HomeHeader(), + ), + ); + }, + ), + ), + + // Main Content + SliverToBoxAdapter( + child: AnimatedBuilder( + animation: _contentAnimationController, + builder: (context, child) { + return Transform.translate( + offset: Offset( + 0, + 30 * (1 - _contentAnimationController.value), + ), + child: Opacity( + opacity: _contentAnimationController.value, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HomeFeature(), + HomeStats(), + HomeTask(), + HomeActivity(), + HomePerformance(), + const SpaceHeight(40), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); } } diff --git a/lib/presentation/pages/home/widgets/activity.dart b/lib/presentation/pages/home/widgets/activity.dart new file mode 100644 index 0000000..dfa5b7e --- /dev/null +++ b/lib/presentation/pages/home/widgets/activity.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; +import 'activity_tile.dart'; + +class HomeActivity extends StatelessWidget { + const HomeActivity({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: AppValue.padding, + ).copyWith(bottom: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Aktivitas Terkini', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w700, + color: AppColor.textPrimary, + letterSpacing: -0.5, + ), + ), + TextButton.icon( + onPressed: () {}, + icon: const Icon(Icons.arrow_forward_rounded, size: 16), + label: const Text('Lihat Semua'), + style: TextButton.styleFrom( + foregroundColor: AppColor.primary, + textStyle: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + ), + ], + ), + const SpaceHeight(16), + Container( + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColor.border.withOpacity(0.5)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + children: [ + HomeActivityTile( + title: 'Transaksi Berhasil', + subtitle: 'Kasir-01 • Rp 125.000', + time: '2 menit lalu', + icon: Icons.check_circle_rounded, + color: AppColor.success, + isHighlighted: true, + ), + const Divider(height: 1, color: AppColor.border), + HomeActivityTile( + title: 'Stok Menipis', + subtitle: 'Kopi Arabica • 5 unit tersisa', + time: '15 menit lalu', + icon: Icons.warning_amber_rounded, + color: AppColor.warning, + isHighlighted: false, + ), + const Divider(height: 1, color: AppColor.border), + HomeActivityTile( + title: 'Login Kasir', + subtitle: 'Sari masuk shift pagi', + time: '1 Jam lalu', + icon: Icons.login_rounded, + color: AppColor.info, + isHighlighted: false, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/activity_tile.dart b/lib/presentation/pages/home/widgets/activity_tile.dart new file mode 100644 index 0000000..75ec095 --- /dev/null +++ b/lib/presentation/pages/home/widgets/activity_tile.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; + +class HomeActivityTile extends StatelessWidget { + final String title; + final String subtitle; + final String time; + final IconData icon; + final Color color; + final bool isHighlighted; + const HomeActivityTile({ + super.key, + required this.title, + required this.subtitle, + required this.time, + required this.icon, + required this.color, + required this.isHighlighted, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: isHighlighted ? color.withOpacity(0.02) : Colors.transparent, + borderRadius: isHighlighted ? BorderRadius.circular(16) : null, + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [color.withOpacity(0.1), color.withOpacity(0.05)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.2), width: 1), + ), + child: Icon(icon, color: color, size: 20), + ), + const SpaceWidth(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, + letterSpacing: -0.2, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SpaceHeight(4), + Text( + subtitle, + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + time, + style: AppStyle.xs.copyWith( + fontSize: 11, + color: AppColor.textLight, + fontWeight: FontWeight.w500, + ), + ), + if (isHighlighted) ...[ + const SpaceHeight(4), + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + ), + ], + ], + ), + ], + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/feature.dart b/lib/presentation/pages/home/widgets/feature.dart new file mode 100644 index 0000000..9c69b2b --- /dev/null +++ b/lib/presentation/pages/home/widgets/feature.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:line_icons/line_icons.dart'; + +import '../../../../common/theme/theme.dart'; +import 'feature_tile.dart'; + +class HomeFeature extends StatelessWidget { + const HomeFeature({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric( + vertical: 24, + horizontal: AppValue.padding, + ).copyWith(bottom: 0), + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 10), + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.circular(AppValue.radius), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 10, + offset: const Offset(0, 5), + spreadRadius: 0, + ), + ], + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HomeFeatureTile( + title: 'Penjualan', + color: const Color(0xFF4CAF50), + icon: LineIcons.receipt, + onTap: () {}, + ), + HomeFeatureTile( + title: 'Pembelian', + color: const Color(0xFF2196F3), + icon: LineIcons.shoppingCart, + onTap: () {}, + ), + HomeFeatureTile( + title: 'Biaya', + color: const Color(0xFF8BC34A), + icon: LineIcons.moneyCheck, + onTap: () {}, + ), + HomeFeatureTile( + title: 'Product', + color: const Color(0xFFFF9800), + icon: LineIcons.box, + onTap: () {}, + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HomeFeatureTile( + title: 'Laporan', + color: const Color(0xFFE91E63), + icon: LineIcons.pieChart, + onTap: () {}, + ), + HomeFeatureTile( + title: 'Kas & Bank', + color: const Color(0xFF9C27B0), + icon: LineIcons.university, + onTap: () {}, + ), + HomeFeatureTile( + title: 'Aset Tetap', + color: const Color(0xFF00BCD4), + icon: LineIcons.businessTime, + onTap: () {}, + ), + HomeFeatureTile( + title: 'Kontak', + color: const Color(0xFFFF5722), + icon: LineIcons.userPlus, + onTap: () {}, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/feature_tile.dart b/lib/presentation/pages/home/widgets/feature_tile.dart new file mode 100644 index 0000000..98a2a5c --- /dev/null +++ b/lib/presentation/pages/home/widgets/feature_tile.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; + +class HomeFeatureTile extends StatelessWidget { + final String title; + final IconData icon; + final Color color; + final Function() onTap; + const HomeFeatureTile({ + super.key, + required this.title, + required this.icon, + required this.color, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Expanded( + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(AppValue.radius), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [color.withOpacity(0.1), color.withOpacity(0.05)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: color.withOpacity(0.2), width: 1), + ), + child: Icon(icon, color: color, size: 28), + ), + const SpaceHeight(12), + Text( + title, + style: AppStyle.sm.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.textPrimary, + letterSpacing: -0.2, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/header.dart b/lib/presentation/pages/home/widgets/header.dart new file mode 100644 index 0000000..2a2eeae --- /dev/null +++ b/lib/presentation/pages/home/widgets/header.dart @@ -0,0 +1,186 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; + +class HomeHeader extends StatelessWidget { + const HomeHeader({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: 280, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColor.primary, + AppColor.primaryLight, + AppColor.primaryLight.withOpacity(0.8), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + stops: const [0.0, 0.7, 1.0], + ), + boxShadow: [ + BoxShadow( + color: AppColor.primary.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Stack( + children: [ + // Decorative circles + Positioned( + top: -50, + right: -50, + child: Container( + width: 150, + height: 150, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColor.white.withOpacity(0.1), + ), + ), + ), + Positioned( + top: 80, + right: -20, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColor.white.withOpacity(0.05), + ), + ), + ), + SafeArea(child: _buildContent()), + ], + ), + ); + } + + Padding _buildContent() { + return Padding( + padding: EdgeInsets.all(AppValue.padding), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Top bar + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'AppSkel POS Owner', + style: AppStyle.lg.copyWith( + color: AppColor.white.withOpacity(0.9), + fontWeight: FontWeight.w600, + letterSpacing: 0.3, + ), + ), + const SpaceHeight(2), + Text( + 'Dashboard', + style: AppStyle.sm.copyWith( + color: AppColor.textLight, + fontSize: 11, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColor.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: AppColor.white.withOpacity(0.3), + width: 1, + ), + ), + child: const Icon( + Icons.notifications_none_rounded, + color: AppColor.white, + size: 20, + ), + ), + ], + ), + + const SpaceHeight(24), + + // Greeting Section + Text( + 'Selamat Pagi,', + style: AppStyle.lg.copyWith( + color: AppColor.white, + fontWeight: FontWeight.w500, + ), + ), + const SpaceHeight(2), + Text( + 'Vira Vania! 👋', + style: AppStyle.h4.copyWith( + color: AppColor.white, + fontWeight: FontWeight.w800, + letterSpacing: -0.5, + ), + ), + const SpaceHeight(8), + Text( + 'Mari tingkatkan performa bisnis Anda hari ini', + style: AppStyle.md.copyWith( + color: AppColor.white.withOpacity(0.85), + fontWeight: FontWeight.w400, + height: 1.3, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + + const SpaceHeight(16), + + // Today's highlight + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: AppColor.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppColor.white.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.trending_up_rounded, + color: AppColor.white, + size: 14, + ), + const SizedBox(width: 6), + Text( + 'Penjualan hari ini +25%', + style: AppStyle.sm.copyWith( + color: AppColor.white, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/performance.dart b/lib/presentation/pages/home/widgets/performance.dart new file mode 100644 index 0000000..f128625 --- /dev/null +++ b/lib/presentation/pages/home/widgets/performance.dart @@ -0,0 +1,281 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; + +class HomePerformance extends StatelessWidget { + const HomePerformance({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: AppValue.padding, + ).copyWith(bottom: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Performa Minggu Ini', + style: AppStyle.h6.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.textPrimary, + letterSpacing: -0.5, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColor.success.withOpacity(0.1), + AppColor.success.withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppColor.success.withOpacity(0.2), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.arrow_upward_rounded, + color: AppColor.success, + size: 14, + ), + const SpaceWidth(4), + Text( + '89%', + style: AppStyle.sm.copyWith( + color: AppColor.success, + fontSize: 12, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ], + ), + const SpaceHeight(20), + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColor.border.withOpacity(0.5)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildPerformanceBar( + 'Sen', + 0.8, + AppColor.primary, + 'Rp 2.1M', + ), + _buildPerformanceBar( + 'Sel', + 0.6, + AppColor.primary, + 'Rp 1.8M', + ), + _buildPerformanceBar( + 'Rab', + 0.9, + AppColor.success, + 'Rp 2.4M', + ), + _buildPerformanceBar( + 'Kam', + 0.7, + AppColor.primary, + 'Rp 1.9M', + ), + _buildPerformanceBar( + 'Jum', + 1.0, + AppColor.success, + 'Rp 2.5M', + ), + _buildPerformanceBar( + 'Sab', + 0.85, + AppColor.success, + 'Rp 2.2M', + ), + _buildPerformanceBar( + 'Min', + 0.4, + AppColor.textLight, + 'Rp 1.2M', + ), + ], + ), + const SpaceHeight(24), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColor.primary.withOpacity(0.05), + AppColor.primary.withOpacity(0.02), + ], + ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppColor.primary.withOpacity(0.1), + width: 1, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Target Minggu Ini', + style: TextStyle( + fontSize: 12, + color: AppColor.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + const SpaceHeight(4), + Text( + 'Rp 15.000.000', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColor.textPrimary, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + 'Tercapai', + style: TextStyle( + fontSize: 12, + color: AppColor.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + const SpaceHeight(4), + Row( + children: [ + Text( + '89%', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColor.success, + ), + ), + const SpaceWidth(4), + Icon( + Icons.trending_up_rounded, + color: AppColor.success, + size: 16, + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildPerformanceBar( + String day, + double percentage, + Color color, + String amount, + ) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Amount label + Text( + amount, + style: AppStyle.xs.copyWith( + fontSize: 10, + fontWeight: FontWeight.w600, + color: AppColor.textSecondary, + ), + ), + const SpaceHeight(8), + // Performance bar + Container( + height: 80, + width: 12, + decoration: BoxDecoration( + color: AppColor.border.withOpacity(0.3), + borderRadius: BorderRadius.circular(6), + ), + child: Align( + alignment: Alignment.bottomCenter, + child: AnimatedContainer( + duration: Duration(milliseconds: 800 + (day.hashCode % 400)), + curve: Curves.easeOutCubic, + height: 80 * percentage, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [color, color.withOpacity(0.7)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + borderRadius: BorderRadius.circular(6), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + ), + ), + ), + const SpaceHeight(12), + // Day label + Text( + day, + style: AppStyle.xs.copyWith( + color: AppColor.textSecondary, + fontWeight: FontWeight.w600, + ), + ), + ], + ); + } +} diff --git a/lib/presentation/pages/home/widgets/stats.dart b/lib/presentation/pages/home/widgets/stats.dart new file mode 100644 index 0000000..721b78e --- /dev/null +++ b/lib/presentation/pages/home/widgets/stats.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; +import 'stats_tile.dart'; + +class HomeStats extends StatelessWidget { + const HomeStats({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: AppValue.padding, + ).copyWith(bottom: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Ringkasan Hari Ini', + style: AppStyle.h6.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.textPrimary, + letterSpacing: -0.5, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: AppColor.success.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppColor.success.withOpacity(0.2), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.trending_up_rounded, + color: AppColor.success, + size: 14, + ), + const SpaceWidth(4), + Text( + 'Live', + style: AppStyle.sm.copyWith( + color: AppColor.success, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + const SpaceHeight(20), + Row( + children: [ + Expanded( + child: HomeStatsTile( + title: 'Total Penjualan', + value: 'Rp 2.450.000', + icon: Icons.trending_up_rounded, + color: AppColor.success, + change: '+12%', + subtitle: 'dari kemarin', + ), + ), + + const SpaceWidth(16), + Expanded( + child: HomeStatsTile( + title: 'Transaksi', + value: '85', + icon: Icons.receipt_long_rounded, + color: AppColor.info, + change: '+8%', + subtitle: 'lebih tinggi', + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: HomeStatsTile( + title: 'Profit Bersih', + value: 'Rp 735.000', + icon: Icons.account_balance_wallet_rounded, + color: AppColor.warning, + change: '+15%', + subtitle: 'margin sehat', + ), + ), + const SpaceWidth(16), + Expanded( + child: HomeStatsTile( + title: 'Pelanggan Baru', + value: '42', + icon: Icons.person_add_rounded, + color: AppColor.primary, + change: '+3%', + subtitle: 'bertambah', + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/stats_tile.dart b/lib/presentation/pages/home/widgets/stats_tile.dart new file mode 100644 index 0000000..fa61461 --- /dev/null +++ b/lib/presentation/pages/home/widgets/stats_tile.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; + +class HomeStatsTile extends StatelessWidget { + final String title; + final String value; + final IconData icon; + final Color color; + final String change; + final String subtitle; + const HomeStatsTile({ + super.key, + required this.title, + required this.value, + required this.icon, + required this.color, + required this.change, + required this.subtitle, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.circular(AppValue.radius), + border: Border.all(color: AppColor.border.withOpacity(0.5)), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.08), + blurRadius: 20, + offset: const Offset(0, 8), + spreadRadius: 0, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [color.withOpacity(0.1), color.withOpacity(0.05)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.2), width: 1), + ), + child: Icon(icon, color: color, size: 20), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: AppColor.success.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + change, + style: AppStyle.xs.copyWith( + color: AppColor.success, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + const SpaceHeight(16), + FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: Text( + value, + style: AppStyle.xxl.copyWith( + fontWeight: FontWeight.w800, + color: AppColor.textPrimary, + letterSpacing: -0.5, + ), + ), + ), + const SpaceHeight(4), + Text( + title, + style: AppStyle.md.copyWith( + fontSize: 13, + color: AppColor.textSecondary, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SpaceHeight(2), + Text( + subtitle, + style: AppStyle.xs.copyWith( + color: AppColor.textLight, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/task.dart b/lib/presentation/pages/home/widgets/task.dart new file mode 100644 index 0000000..5b47dab --- /dev/null +++ b/lib/presentation/pages/home/widgets/task.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; +import 'task_tile.dart'; + +class HomeTask extends StatelessWidget { + const HomeTask({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, + horizontal: AppValue.padding, + ).copyWith(bottom: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Tugas Harian', + style: AppStyle.h6.copyWith( + fontWeight: FontWeight.w700, + color: AppColor.textPrimary, + letterSpacing: -0.5, + ), + ), + Text( + '3/5 Selesai', + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w600, + color: AppColor.primary, + ), + ), + ], + ), + const SpaceHeight(20), + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColor.white, + borderRadius: BorderRadius.circular(AppValue.radius), + border: Border.all(color: AppColor.border.withOpacity(0.5)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + children: [ + HomeTaskTile( + title: 'Cek stok produk terlaris', + subtitle: 'Pastikan ketersediaan produk favorit pelanggan', + isCompleted: true, + color: AppColor.success, + ), + const SpaceHeight(16), + HomeTaskTile( + title: 'Review laporan penjualan kemarin', + subtitle: 'Analisis performa dan identifikasi peluang', + isCompleted: true, + color: AppColor.success, + ), + const SpaceHeight(16), + HomeTaskTile( + title: 'Update harga produk musiman', + subtitle: 'Sesuaikan harga berdasarkan demand pasar', + isCompleted: true, + color: AppColor.success, + ), + const SpaceHeight(16), + HomeTaskTile( + title: 'Backup data transaksi', + subtitle: 'Pastikan data aman dan tersimpan', + color: AppColor.warning, + ), + const SpaceHeight(16), + HomeTaskTile( + title: 'Training tim kasir baru', + subtitle: 'Onboarding karyawan untuk shift sore', + color: AppColor.info, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/pages/home/widgets/task_tile.dart b/lib/presentation/pages/home/widgets/task_tile.dart new file mode 100644 index 0000000..f1295c4 --- /dev/null +++ b/lib/presentation/pages/home/widgets/task_tile.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; + +class HomeTaskTile extends StatelessWidget { + final String title; + final String subtitle; + final bool isCompleted; + final Color color; + const HomeTaskTile({ + super.key, + required this.title, + required this.subtitle, + this.isCompleted = false, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: isCompleted ? color : Colors.transparent, + borderRadius: BorderRadius.circular(6), + border: Border.all(color: color, width: 2), + ), + child: isCompleted + ? Icon(Icons.check_rounded, color: AppColor.white, size: 16) + : null, + ), + const SpaceWidth(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: AppStyle.md.copyWith( + fontWeight: FontWeight.w600, + color: isCompleted + ? AppColor.textSecondary + : AppColor.textPrimary, + decoration: isCompleted + ? TextDecoration.lineThrough + : TextDecoration.none, + ), + ), + const SpaceHeight(2), + Text( + subtitle, + style: AppStyle.sm.copyWith( + color: AppColor.textLight, + decoration: isCompleted + ? TextDecoration.lineThrough + : TextDecoration.none, + ), + ), + ], + ), + ), + ], + ); + } +}