diff --git a/lib/presentation/components/button/action_icon_button.dart b/lib/presentation/components/button/action_icon_button.dart new file mode 100644 index 0000000..deeac78 --- /dev/null +++ b/lib/presentation/components/button/action_icon_button.dart @@ -0,0 +1,30 @@ +part of 'button.dart'; + +class ActionIconButton extends StatelessWidget { + const ActionIconButton({super.key, required this.onTap, required this.icon}); + + final Function()? onTap; + final IconData icon; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(right: 8), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColor.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: AppColor.textWhite, size: 20), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/components/button/button.dart b/lib/presentation/components/button/button.dart index 881669f..ebbf30f 100644 --- a/lib/presentation/components/button/button.dart +++ b/lib/presentation/components/button/button.dart @@ -5,3 +5,4 @@ import '../../../common/theme/theme.dart'; import '../spacer/spacer.dart'; part 'elevated_button.dart'; +part 'action_icon_button.dart'; diff --git a/lib/presentation/pages/report/report_page.dart b/lib/presentation/pages/report/report_page.dart index da379df..3884eb4 100644 --- a/lib/presentation/pages/report/report_page.dart +++ b/lib/presentation/pages/report/report_page.dart @@ -1,12 +1,132 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:line_icons/line_icons.dart'; +import 'dart:math' as math; + +import '../../../common/theme/theme.dart'; +import '../../components/button/button.dart'; +import '../../components/spacer/spacer.dart'; +import 'widgets/appbar.dart'; +import 'widgets/quick_stats.dart'; +import 'widgets/report_action.dart'; +import 'widgets/revenue_summary.dart'; +import 'widgets/sales.dart'; +import 'widgets/top_product.dart'; @RoutePage() -class ReportPage extends StatelessWidget { +class ReportPage extends StatefulWidget { const ReportPage({super.key}); + @override + State createState() => _ReportPageState(); +} + +class _ReportPageState extends State with TickerProviderStateMixin { + late AnimationController _fadeController; + late AnimationController _slideController; + late AnimationController _rotationController; + late Animation _fadeAnimation; + late Animation _slideAnimation; + late Animation _rotationAnimation; + + @override + void initState() { + super.initState(); + + _fadeController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + _slideController = AnimationController( + duration: const Duration(milliseconds: 1000), + vsync: this, + ); + + _rotationController = AnimationController( + duration: const Duration(seconds: 3), + vsync: this, + )..repeat(); + + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _fadeController, curve: Curves.easeInOut), + ); + + _slideAnimation = + Tween(begin: const Offset(0, 0.3), end: Offset.zero).animate( + CurvedAnimation(parent: _slideController, curve: Curves.elasticOut), + ); + + _rotationAnimation = Tween( + begin: 0, + end: 2 * math.pi, + ).animate(_rotationController); + + _fadeController.forward(); + _slideController.forward(); + } + + @override + void dispose() { + _fadeController.dispose(); + _slideController.dispose(); + _rotationController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return Center(child: Text('ReportPage')); + return Scaffold( + backgroundColor: AppColor.background, + body: CustomScrollView( + slivers: [ + // Custom App Bar with Hero Effect + SliverAppBar( + expandedHeight: 120, + floating: false, + pinned: true, + backgroundColor: AppColor.primary, + centerTitle: false, + flexibleSpace: ReportAppBar(rotationAnimation: _rotationAnimation), + actions: [ + ActionIconButton(onTap: () {}, icon: LineIcons.download), + ActionIconButton(onTap: () {}, icon: LineIcons.filter), + SpaceWidth(8), + ], + ), + + // Content + SliverPadding( + padding: EdgeInsets.all(AppValue.padding), + sliver: SliverList( + delegate: SliverChildListDelegate([ + FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, + child: Column( + children: [ + ReportRevenueSummary( + rotationAnimation: _rotationAnimation, + ), + const SpaceHeight(24), + ReportQuickStats(), + const SpaceHeight(24), + ReportSales(), + const SpaceHeight(24), + ReportTopProduct(), + const SpaceHeight(24), + ReportAction(), + const SpaceHeight(20), + ], + ), + ), + ), + ]), + ), + ), + ], + ), + ); } } diff --git a/lib/presentation/pages/report/widgets/appbar.dart b/lib/presentation/pages/report/widgets/appbar.dart new file mode 100644 index 0000000..09c34fa --- /dev/null +++ b/lib/presentation/pages/report/widgets/appbar.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; + +class ReportAppBar extends StatelessWidget { + final Animation rotationAnimation; + const ReportAppBar({super.key, required this.rotationAnimation}); + + @override + Widget build(BuildContext context) { + return FlexibleSpaceBar( + titlePadding: const EdgeInsets.only(left: 20, bottom: 16), + title: Text( + 'Laporan Bisnis', + style: AppStyle.xl.copyWith( + color: AppColor.textWhite, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + background: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: AppColor.primaryGradient, + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Stack( + children: [ + Positioned( + right: -20, + top: -20, + child: AnimatedBuilder( + animation: rotationAnimation, + builder: (context, child) { + return Transform.rotate( + angle: rotationAnimation.value, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColor.white.withOpacity(0.1), + ), + ), + ); + }, + ), + ), + Positioned( + left: -30, + bottom: -30, + child: AnimatedBuilder( + animation: rotationAnimation, + builder: (context, child) { + return Transform.rotate( + angle: -rotationAnimation.value * 0.5, + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColor.white.withOpacity(0.05), + ), + ), + ); + }, + ), + ), + Positioned( + right: 80, + bottom: 30, + child: AnimatedBuilder( + animation: rotationAnimation, + builder: (context, child) { + return Transform.rotate( + angle: -rotationAnimation.value * 0.2, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: AppColor.white.withOpacity(0.08), + ), + ), + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/pages/report/widgets/quick_stats.dart b/lib/presentation/pages/report/widgets/quick_stats.dart new file mode 100644 index 0000000..9e46687 --- /dev/null +++ b/lib/presentation/pages/report/widgets/quick_stats.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; +import 'stat_tile.dart'; + +class ReportQuickStats extends StatelessWidget { + const ReportQuickStats({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + duration: const Duration(milliseconds: 800), + builder: (context, value, child) { + return Transform.scale( + scale: value, + child: ReportStatTile( + title: 'Total Transaksi', + value: '245', + icon: Icons.receipt_long, + color: AppColor.info, + change: '+8.2%', + animatedValue: 245 * value, + ), + ); + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + duration: const Duration(milliseconds: 1000), + builder: (context, value, child) { + return Transform.scale( + scale: value, + child: ReportStatTile( + title: 'Rata-rata', + value: 'Rp 63.061', + icon: Icons.trending_up, + color: AppColor.warning, + change: '+5.1%', + animatedValue: 63061 * value, + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/lib/presentation/pages/report/widgets/report_action.dart b/lib/presentation/pages/report/widgets/report_action.dart new file mode 100644 index 0000000..44fb29c --- /dev/null +++ b/lib/presentation/pages/report/widgets/report_action.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; + +class ReportAction extends StatefulWidget { + const ReportAction({super.key}); + + @override + State createState() => _ReportActionState(); +} + +class _ReportActionState extends State { + final actions = [ + { + 'title': 'Laporan Detail Penjualan', + 'subtitle': 'Analisis mendalam transaksi harian', + 'icon': Icons.assignment, + 'color': AppColor.primary, + 'gradient': [AppColor.primary, AppColor.primaryLight], + }, + { + 'title': 'Monitor Stok Produk', + 'subtitle': 'Tracking inventory real-time', + 'icon': Icons.inventory_2, + 'color': AppColor.info, + 'gradient': [AppColor.info, const Color(0xFF64B5F6)], + }, + { + 'title': 'Analisis Keuangan', + 'subtitle': 'Profit, loss & cash flow analysis', + 'icon': Icons.account_balance_wallet, + 'color': AppColor.success, + 'gradient': [AppColor.success, AppColor.secondaryLight], + }, + ]; + + @override + Widget build(BuildContext context) { + return Column( + children: actions.map((action) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () {}, + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + (action['color'] as Color).withOpacity(0.1), + (action['color'] as Color).withOpacity(0.05), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: (action['color'] as Color).withOpacity(0.3), + width: 1.5, + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: action['gradient'] as List, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: (action['color'] as Color).withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Icon( + action['icon'] as IconData, + color: AppColor.white, + size: 28, + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + action['title'] as String, + style: AppStyle.lg.copyWith( + color: AppColor.textPrimary, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SpaceHeight(4), + Text( + action['subtitle'] as String, + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + fontSize: 13, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: (action['color'] as Color).withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.arrow_forward_ios, + color: action['color'] as Color, + size: 16, + ), + ), + ], + ), + ), + ), + ), + ); + }).toList(), + ); + } +} diff --git a/lib/presentation/pages/report/widgets/revenue_summary.dart b/lib/presentation/pages/report/widgets/revenue_summary.dart new file mode 100644 index 0000000..86ec191 --- /dev/null +++ b/lib/presentation/pages/report/widgets/revenue_summary.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; + +class ReportRevenueSummary extends StatelessWidget { + final Animation rotationAnimation; + const ReportRevenueSummary({super.key, required this.rotationAnimation}); + + @override + Widget build(BuildContext context) { + return Container( + height: 180, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: AppColor.primary.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: AppColor.primaryGradient, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(24), + ), + ), + // Floating elements + Positioned( + right: 20, + top: 20, + child: AnimatedBuilder( + animation: rotationAnimation, + builder: (context, child) { + return Transform.rotate( + angle: rotationAnimation.value * 0.3, + child: Container( + width: 60, + height: 60, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColor.white.withOpacity(0.1), + ), + ), + ); + }, + ), + ), + Positioned( + right: 60, + bottom: 30, + child: AnimatedBuilder( + animation: rotationAnimation, + builder: (context, child) { + return Transform.rotate( + angle: -rotationAnimation.value * 0.2, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: AppColor.white.withOpacity(0.08), + ), + ), + ); + }, + ), + ), + // Content + Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColor.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.account_balance_wallet, + color: AppColor.textWhite, + size: 20, + ), + ), + const SpaceWidth(12), + Text( + 'Total Pendapatan', + style: AppStyle.lg.copyWith( + color: AppColor.textWhite, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const Spacer(), + Text( + 'Rp 15.450.000', + style: AppStyle.h1.copyWith( + color: AppColor.textWhite, + fontWeight: FontWeight.bold, + letterSpacing: -1, + ), + ), + const SpaceHeight(8), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: AppColor.success.withOpacity(0.9), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.trending_up, + color: AppColor.textWhite, + size: 16, + ), + SpaceWidth(4), + Text( + '+12.5%', + style: AppStyle.sm.copyWith( + color: AppColor.textWhite, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + const SpaceWidth(12), + Text( + 'dari periode sebelumnya', + style: AppStyle.sm.copyWith(color: AppColor.textWhite), + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/pages/report/widgets/sales.dart b/lib/presentation/pages/report/widgets/sales.dart new file mode 100644 index 0000000..e0b6f69 --- /dev/null +++ b/lib/presentation/pages/report/widgets/sales.dart @@ -0,0 +1,321 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; + +class ReportSales extends StatelessWidget { + const ReportSales({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: AppColor.surface, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: AppColor.textSecondary.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Grafik Penjualan', + style: AppStyle.xxl.copyWith( + color: AppColor.textPrimary, + + fontWeight: FontWeight.bold, + ), + ), + const SpaceHeight(4), + Text( + '7 hari terakhir', + style: AppStyle.md.copyWith(color: AppColor.textSecondary), + ), + ], + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColor.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.show_chart, + color: AppColor.primary, + size: 24, + ), + ), + ], + ), + const SpaceHeight(20), + + // Chart Container + Container( + height: 280, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppColor.primary.withOpacity(0.05), + AppColor.backgroundLight, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppColor.primary.withOpacity(0.1), + width: 2, + ), + ), + child: LineChart( + LineChartData( + gridData: FlGridData( + show: true, + drawHorizontalLine: true, + drawVerticalLine: false, + horizontalInterval: 500000, + getDrawingHorizontalLine: (value) { + return FlLine( + color: AppColor.border.withOpacity(0.3), + strokeWidth: 1, + dashArray: [5, 5], + ); + }, + ), + titlesData: FlTitlesData( + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 60, + getTitlesWidget: (value, meta) { + return Text( + '${(value / 1000000).toStringAsFixed(1)}M', + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + fontWeight: FontWeight.w500, + ), + ); + }, + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 32, + getTitlesWidget: (value, meta) { + const days = [ + 'Sen', + 'Sel', + 'Rab', + 'Kam', + 'Jum', + 'Sab', + 'Min', + ]; + if (value.toInt() >= 0 && value.toInt() < days.length) { + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + days[value.toInt()], + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + ); + } + return const Text(''); + }, + ), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + ), + borderData: FlBorderData(show: false), + minX: 0, + maxX: 6, + minY: 0, + maxY: 3000000, + lineBarsData: [ + // Main sales line + LineChartBarData( + spots: [ + const FlSpot(0, 1800000), // Senin + const FlSpot(1, 2200000), // Selasa + const FlSpot(2, 1900000), // Rabu + const FlSpot(3, 2600000), // Kamis + const FlSpot(4, 2300000), // Jumat + const FlSpot(5, 2800000), // Sabtu + const FlSpot(6, 2500000), // Minggu + ], + isCurved: true, + curveSmoothness: 0.35, + gradient: LinearGradient( + colors: [AppColor.primary, AppColor.primaryLight], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + barWidth: 4, + isStrokeCapRound: true, + belowBarData: BarAreaData( + show: true, + gradient: LinearGradient( + colors: [ + AppColor.primary.withOpacity(0.3), + AppColor.primary.withOpacity(0.1), + Colors.transparent, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 6, + color: AppColor.surface, + strokeWidth: 3, + strokeColor: AppColor.primary, + ); + }, + ), + ), + // Secondary line for comparison + LineChartBarData( + spots: [ + const FlSpot(0, 1500000), + const FlSpot(1, 1800000), + const FlSpot(2, 1600000), + const FlSpot(3, 2100000), + const FlSpot(4, 1900000), + const FlSpot(5, 2300000), + const FlSpot(6, 2100000), + ], + isCurved: true, + curveSmoothness: 0.35, + color: AppColor.success.withOpacity(0.7), + barWidth: 3, + isStrokeCapRound: true, + dashArray: [8, 4], + belowBarData: BarAreaData(show: false), + dotData: FlDotData( + show: true, + getDotPainter: (spot, percent, barData, index) { + return FlDotCirclePainter( + radius: 4, + color: AppColor.success, + strokeWidth: 2, + strokeColor: AppColor.surface, + ); + }, + ), + ), + ], + lineTouchData: LineTouchData( + enabled: true, + touchTooltipData: LineTouchTooltipData( + tooltipPadding: const EdgeInsets.all(12), + getTooltipItems: (List touchedBarSpots) { + return touchedBarSpots.map((barSpot) { + final flSpot = barSpot; + const days = [ + 'Senin', + 'Selasa', + 'Rabu', + 'Kamis', + 'Jumat', + 'Sabtu', + 'Minggu', + ]; + + return LineTooltipItem( + '${days[flSpot.x.toInt()]}\n', + const TextStyle( + color: AppColor.textWhite, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + children: [ + TextSpan( + text: + 'Rp ${(flSpot.y / 1000000).toStringAsFixed(1)}M', + style: AppStyle.sm.copyWith( + color: AppColor.textWhite, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + }).toList(); + }, + ), + touchCallback: + (FlTouchEvent event, LineTouchResponse? touchResponse) { + // Handle touch events here if needed + }, + handleBuiltInTouches: true, + ), + ), + ), + ), + + const SpaceHeight(16), + + // Legend + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildLegendItem('Minggu Ini', AppColor.primary), + const SpaceWidth(24), + _buildLegendItem('Minggu Lalu', AppColor.success), + ], + ), + ], + ), + ); + } + + Widget _buildLegendItem(String label, Color color) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 16, + height: 3, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(2), + ), + ), + const SpaceWidth(8), + Text( + label, + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + + fontWeight: FontWeight.w500, + ), + ), + ], + ); + } +} diff --git a/lib/presentation/pages/report/widgets/stat_tile.dart b/lib/presentation/pages/report/widgets/stat_tile.dart new file mode 100644 index 0000000..863aab5 --- /dev/null +++ b/lib/presentation/pages/report/widgets/stat_tile.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; + +class ReportStatTile extends StatelessWidget { + final String title; + final String value; + final IconData icon; + final Color color; + final String change; + final double animatedValue; + const ReportStatTile({ + super.key, + required this.title, + required this.value, + required this.icon, + required this.color, + required this.change, + required this.animatedValue, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppColor.surface, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.1), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + border: Border.all(color: color.withOpacity(0.2), width: 1.5), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [color.withOpacity(0.2), color.withOpacity(0.1)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: color, size: 24), + ), + const Spacer(), + 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.sm.copyWith( + color: AppColor.success, + fontWeight: FontWeight.w700, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Text( + title, + style: AppStyle.md.copyWith( + color: AppColor.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: AppStyle.xxl.copyWith( + color: color, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/pages/report/widgets/top_product.dart b/lib/presentation/pages/report/widgets/top_product.dart new file mode 100644 index 0000000..57404c3 --- /dev/null +++ b/lib/presentation/pages/report/widgets/top_product.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; + +import '../../../../common/theme/theme.dart'; +import '../../../components/spacer/spacer.dart'; + +class ReportTopProduct extends StatelessWidget { + const ReportTopProduct({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: AppColor.surface, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: AppColor.textSecondary.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Produk Terlaris', + style: AppStyle.xxl.copyWith( + color: AppColor.textPrimary, + fontWeight: FontWeight.bold, + ), + ), + const SpaceHeight(4), + Text( + 'Ranking penjualan tertinggi', + style: AppStyle.md.copyWith(color: AppColor.textSecondary), + ), + ], + ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColor.warning.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.star, + color: AppColor.warning, + size: 24, + ), + ), + ], + ), + const SpaceHeight(20), + _buildEnhancedProductItem( + 'Kopi Americano', + 'Rp 25.000', + '145 terjual', + 1, + ), + _buildEnhancedProductItem( + 'Nasi Goreng Spesial', + 'Rp 35.000', + '98 terjual', + 2, + ), + _buildEnhancedProductItem( + 'Mie Ayam Bakso', + 'Rp 28.000', + '87 terjual', + 3, + ), + ], + ), + ); + } + + Widget _buildEnhancedProductItem( + String name, + String price, + String sold, + int rank, + ) { + final isFirst = rank == 1; + return Container( + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: isFirst + ? LinearGradient( + colors: [ + AppColor.warning.withOpacity(0.1), + AppColor.warning.withOpacity(0.05), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, + color: isFirst ? null : AppColor.backgroundLight, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isFirst ? AppColor.warning.withOpacity(0.3) : AppColor.border, + width: isFirst ? 2 : 1, + ), + ), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + gradient: isFirst + ? const LinearGradient( + colors: [AppColor.warning, Color(0xFFFFB74D)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : LinearGradient( + colors: [ + AppColor.primary.withOpacity(0.8), + AppColor.primaryLight.withOpacity(0.6), + ], + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: isFirst + ? AppColor.warning.withOpacity(0.3) + : AppColor.primary.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Center( + child: isFirst + ? const Icon( + Icons.emoji_events, + color: AppColor.white, + size: 24, + ) + : Text( + rank.toString(), + style: AppStyle.xl.copyWith( + color: AppColor.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SpaceWidth(16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: AppStyle.lg.copyWith( + color: AppColor.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + const SpaceHeight(4), + Row( + children: [ + Icon( + Icons.shopping_cart, + size: 14, + color: AppColor.textSecondary, + ), + const SpaceWidth(4), + Text( + sold, + style: AppStyle.sm.copyWith( + color: AppColor.textSecondary, + ), + ), + ], + ), + ], + ), + ), + Text( + price, + style: AppStyle.lg.copyWith( + color: isFirst ? AppColor.warning : AppColor.primary, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 0678b10..ce8fdee 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -297,6 +297,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -329,6 +337,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "577aeac8ca414c25333334d7c4bb246775234c0e44b38b10a82b559dd4d764e7" + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter: dependency: "direct main" description: flutter @@ -1039,4 +1055,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.8.1 <4.0.0" - flutter: ">=3.27.0" + flutter: ">=3.27.4" diff --git a/pubspec.yaml b/pubspec.yaml index 808f716..e05cdc1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: awesome_dio_interceptor: ^1.3.0 line_icons: ^2.0.3 flutter_spinkit: ^5.2.2 + fl_chart: ^1.0.0 dev_dependencies: flutter_test: