import 'dart:math'; import 'package:business_panel/core/config/app_colors.dart'; import 'package:business_panel/domain/entities/discount_entity.dart'; import 'package:business_panel/gen/assets.gen.dart'; import 'package:business_panel/presentation/sales_analysis/bloc/sales_analysis_bloc.dart'; import 'package:business_panel/presentation/widgets/comments_section.dart'; import 'package:business_panel/presentation/widgets/custom_app_bar_single.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:intl/intl.dart'; import 'package:persian_datetime_picker/persian_datetime_picker.dart'; class SalesAnalysisPage extends StatelessWidget { final DiscountEntity discount; const SalesAnalysisPage({super.key, required this.discount}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => SalesAnalysisBloc() ..add(FetchSalesData( discountId: discount.id, date: DateTime.now(), )), child: _SalesAnalysisView(discount: discount), ); } } class _SalesAnalysisView extends StatefulWidget { final DiscountEntity discount; const _SalesAnalysisView({required this.discount}); @override State<_SalesAnalysisView> createState() => _SalesAnalysisViewState(); } class _SalesAnalysisViewState extends State<_SalesAnalysisView> { DateTime _selectedDate = DateTime.now(); Future _pickDate(BuildContext context) async { final now = Jalali.now(); Jalali? picked = await showPersianDatePicker( context: context, initialDate: Jalali.fromDateTime(_selectedDate), firstDate: now.addDays(-365), lastDate: now, ); if (picked != null && context.mounted) { setState(() { _selectedDate = picked.toDateTime(); }); context.read().add(FetchSalesData( discountId: widget.discount.id, date: _selectedDate, )); } } String _formatAvgTime(double avgMinutes) { if (avgMinutes < 0) return "نامشخص"; if (avgMinutes < 1) { final int seconds = (avgMinutes * 60).round(); return seconds > 0 ? "$seconds ثانیه" : "-"; } if (avgMinutes >= 60) { final int hours = (avgMinutes / 60).floor(); final int minutes = (avgMinutes % 60).round(); String result = "$hours ساعت"; if (minutes > 0) result += " و $minutes دقیقه"; return result; } final int minutes = avgMinutes.floor(); final int seconds = ((avgMinutes - minutes) * 60).round(); String result = "$minutes دقیقه"; if (seconds > 0) result += " و $seconds ثانیه"; return result; } @override Widget build(BuildContext context) { final int discountPercentage = (widget.discount.price > 0 && widget.discount.price > widget.discount.nPrice) ? (((widget.discount.price - widget.discount.nPrice) / widget.discount.price) * 100).toInt() : 0; final jalaliDate = Jalali.fromDateTime(_selectedDate); final formattedDate = '${jalaliDate.formatter.wN} ${jalaliDate.day} ${jalaliDate.formatter.mN} ${jalaliDate.year}'; return Scaffold( appBar: CustomAppBarSingle(page: "تخفیف ها"), body: BlocListener( listener: (context, state) { if (state is SalesAnalysisLoaded) { if (state.kpiStatus == KpiStatus.loading) { context.read().add(FetchSalesStats(widget.discount.id)); } if (state.commentStatus == CommentStatus.loading) { context.read().add(FetchComments(widget.discount.id)); } } }, child: SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildDiscountInfo(discountPercentage), const SizedBox(height: 32), _buildSectionHeader("آنالیز ساعتی فروش"), const SizedBox(height: 25), _buildDatePickerField(context, formattedDate), const SizedBox(height: 24), _buildChartSection(), const SizedBox(height: 32), _buildSectionHeader("شاخص‌های کلیدی رزرو و فروش"), const SizedBox(height: 20), _buildKeyPerformanceIndicators(), const SizedBox(height: 32), _buildSectionHeader("نظرات کاربران"), _buildCommentsSection(), ], ), ), ), ); } Widget _buildCommentsSection() { return BlocBuilder( builder: (context, state) { if (state is SalesAnalysisLoaded) { switch (state.commentStatus) { case CommentStatus.loading: case CommentStatus.initial: return const Center(child: CircularProgressIndicator()); case CommentStatus.failure: return Center( child: Text( state.commentErrorMessage ?? 'خطا در بارگذاری نظرات', style: const TextStyle(color: Colors.red), ), ); case CommentStatus.success: if (state.comments.isEmpty) { return const Center(child: Text('هنوز نظری برای این تخفیف ثبت نشده است.')); } return CommentsSection(comments: state.comments); } } return const SizedBox.shrink(); }, ); } Widget _buildDiscountInfo(int discountPercentage) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionHeader("تخفیف ${widget.discount.type}"), const SizedBox(height: 10), const Divider(height: 1), const SizedBox(height: 14), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildProductImage(), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.discount.name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.normal, color: AppColors.hint), ), const SizedBox(height: 12), _buildPriceRow( icon: Assets.icons.ticketDiscount, child: Row( children: [ Text( NumberFormat('#,##0').format(widget.discount.price), style: const TextStyle(fontSize: 16, color: Colors.grey, decoration: TextDecoration.lineThrough), ), const SizedBox(width: 8), if (discountPercentage > 0) Text('($discountPercentage%)', style: const TextStyle(fontSize: 14, color: Colors.red)), ], ), ), const SizedBox(height: 12), _buildPriceRow( icon: Assets.icons.cardPos, child: Text( "${NumberFormat('#,##0').format(widget.discount.nPrice)} تومان", style: const TextStyle(fontSize: 16, color: Colors.red, fontWeight: FontWeight.bold), ), ), ], ), ), ], ), ], ); } Widget _buildDatePickerField(BuildContext context, String formattedDate) { return InkWell( onTap: () => _pickDate(context), child: InputDecorator( decoration: InputDecoration( labelText: "انتخاب تاریخ", suffixIcon: Padding( padding: const EdgeInsets.all(12.0), child: SvgPicture.asset(Assets.icons.calendarSearch, colorFilter: const ColorFilter.mode(AppColors.active, BlendMode.srcIn)), ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: AppColors.border), ), ), child: Text(formattedDate, style: const TextStyle(fontSize: 16)), ), ); } Widget _buildKeyPerformanceIndicators() { return BlocBuilder( buildWhen: (previous, current) => current is SalesAnalysisLoaded, builder: (context, state) { if (state is SalesAnalysisLoaded) { switch (state.kpiStatus) { case KpiStatus.loading: case KpiStatus.initial: return const Center(child: CircularProgressIndicator()); case KpiStatus.failure: return Center(child: Text(state.kpiErrorMessage ?? 'خطا', style: const TextStyle(color: Colors.red))); case KpiStatus.success: if (state.salesStats == null) { return const Center(child: Text('آماری برای نمایش وجود ندارد.')); } final stats = state.salesStats!; final conversionRate = stats.reservL > 0 ? (stats.orderL / stats.reservL) * 100 : 0.0; final formattedAmount = NumberFormat('#,##0').format(stats.amount); final formattedAvgTime = _formatAvgTime(stats.avgDiffMinutes); return Container( decoration: BoxDecoration(borderRadius: BorderRadius.circular(20)), child: ClipRRect( borderRadius: BorderRadius.circular(20), child: Column( children: [ _buildKpiRow('میانگین زمان رزرو تا خرید', formattedAvgTime, isOdd: true), _buildKpiRow('تعداد رزرو محصول تا این لحظه', '${stats.reservL}', isOdd: false), _buildKpiRow('تعداد خرید محصول تا این لحظه', '${stats.orderL}', isOdd: true), _buildKpiRow('نرخ تبدیل', '${conversionRate.toStringAsFixed(1)}٪', isOdd: false), _buildKpiRow('میزان فروش تا این لحظه', '$formattedAmount تومان', isOdd: true), ], ), ), ); } } return const Center(child: CircularProgressIndicator()); }, ); } Widget _buildKpiRow(String title, String value, {required bool isOdd}) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), color: isOdd ? AppColors.analyticsGrey : Colors.white, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(title, style: const TextStyle(color: Colors.black, fontSize: 14)), Text(value, style: const TextStyle(color: AppColors.active, fontWeight: FontWeight.normal, fontSize: 14)), ], ), ); } Widget _buildChartSection() { return BlocBuilder( builder: (context, state) { if (state is SalesAnalysisLoading || state is SalesAnalysisInitial) { return const Center(child: CircularProgressIndicator()); } if (state is SalesAnalysisError) { return Center(child: Text('خطا: ${state.message}', style: const TextStyle(color: Colors.red))); } if (state is SalesAnalysisLoaded) { final List barGroups = []; final data = state.salesData; double maxY = 0; data.forEach((key, value) { final num numericValue = value; if (numericValue > maxY) maxY = numericValue.toDouble(); }); maxY = (maxY == 0) ? 5 : (maxY * 1.2); int i = 0; data.forEach((key, value) { final num numericValue = value; barGroups.add( BarChartGroupData(x: i++, barRods: [ BarChartRodData( toY: numericValue.toDouble(), gradient: LinearGradient(colors: [Colors.blue.shade700, AppColors.primary], begin: Alignment.bottomCenter, end: Alignment.topCenter), width: 22, borderRadius: const BorderRadius.all(Radius.circular(6)), backDrawRodData: BackgroundBarChartRodData(show: true, toY: maxY, color: Colors.grey.shade200), ), ]), ); }); return SizedBox( height: 300, child: BarChart( BarChartData( maxY: maxY, alignment: BarChartAlignment.spaceAround, barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( getTooltipItem: (group, groupIndex, rod, rodIndex) { final key = data.keys.elementAt(group.x); return BarTooltipItem('$key\n', const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14), children: [TextSpan(text: (rod.toY).toInt().toString(), style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500))], ); }, ), ), titlesData: FlTitlesData( show: true, rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (double value, TitleMeta meta) { final titles = data.keys.toList(); if (value.toInt() >= titles.length) return const SizedBox(); // *** FIX IS HERE: Added meta *** return SideTitleWidget(meta: meta, space: 15.0, angle: -pi / 2, child: Text(titles[value.toInt()], style: const TextStyle(fontSize: 10))); }, reservedSize: 38, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, reservedSize: 28, interval: (maxY / 5).ceilToDouble(), getTitlesWidget: (double value, TitleMeta meta) { if (value == 0 || value > maxY) return const SizedBox(); // *** FIX IS HERE: Added meta *** return SideTitleWidget(meta: meta, child: Text(value.toInt().toString(), style: const TextStyle(fontSize: 10))); }, ), ), ), borderData: FlBorderData(show: false), gridData: FlGridData(show: true, drawVerticalLine: false, horizontalInterval: (maxY / 5).ceilToDouble(), getDrawingHorizontalLine: (value) => const FlLine(color: Color(0xffe7e8ec), strokeWidth: 1)), barGroups: barGroups, ), ), ); } return const Center(child: Text("برای مشاهده آمار، تاریخ را انتخاب کنید.")); }, ); } Widget _buildSectionHeader(String title) { return Row(children: [ Container(width: 4, height: 24, decoration: BoxDecoration(color: AppColors.active, borderRadius: BorderRadius.circular(2))), const SizedBox(width: 8), Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), ]); } Widget _buildPriceRow({required String icon, required Widget child}) { return Row(children: [ SvgPicture.asset(icon, width: 20, colorFilter: const ColorFilter.mode(Color.fromARGB(255, 157, 157, 155), BlendMode.srcIn)), const SizedBox(width: 8), child, ]); } Widget _buildProductImage() { return ClipRRect( borderRadius: BorderRadius.circular(15), child: (widget.discount.images.isNotEmpty && widget.discount.images.first.isNotEmpty) ? Image.network(widget.discount.images.first, width: 120, height: 120, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) => _buildImagePlaceholder()) : _buildImagePlaceholder(), ); } Widget _buildImagePlaceholder() { return Container( width: 120, height: 120, decoration: BoxDecoration(color: Colors.grey[200], borderRadius: BorderRadius.circular(15)), child: const Icon(Icons.store, color: Colors.grey, size: 60), ); } }