import 'package:didvan/models/new_statistic/new_statistics_model.dart'; import 'package:didvan/views/home/new_statistic/new_statistics_state.dart'; import 'package:didvan/views/widgets/didvan/text.dart'; import 'package:didvan/views/widgets/home_app_bar.dart'; import 'package:didvan/views/widgets/mini_chart.dart'; import 'package:didvan/views/widgets/shimmer_placeholder.dart'; import 'package:didvan/views/widgets/state_handlers/state_handler.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:persian_number_utility/persian_number_utility.dart'; import 'package:provider/provider.dart'; class NewStatistic extends StatefulWidget { const NewStatistic({super.key}); @override State createState() => _NewStatisticState(); } class _NewStatisticState extends State { int? _selectedCategory; int _visibleLimit = 40; @override void initState() { super.initState(); context.read().init(); } @override Widget build(BuildContext context) { final state = context.watch(); return Scaffold( body: RefreshIndicator( onRefresh: state.getStatistic, child: CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ const SliverToBoxAdapter(child: HomeAppBar(showSearchField: false)), SliverPadding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 24), sliver: SliverToBoxAdapter( child: StateHandler( state: state, enableEmptyState: false, onRetry: state.getStatistic, placeholder: const _IndustryPulseLoading(), builder: (context, value) { if (value.errorMessage != null) { return _ErrorView( message: value.errorMessage!, onRetry: value.getStatistic, ); } if (value.contents.isEmpty) { return const _EmptyView(); } final selected = value.contents .any((item) => item.category == _selectedCategory) ? _selectedCategory : value.contents.first.category; final category = value.contents.firstWhere( (item) => item.category == selected, ); final visibleItems = category.contents.take(_visibleLimit).toList(); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _MarketHero( bannerUrl: value.bannerUrl, generatedAt: value.generatedAt, ), const SizedBox(height: 20), SizedBox( height: 42, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: value.contents.length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (context, index) { final item = value.contents[index]; final isSelected = item.category == selected; final colorScheme = Theme.of(context).colorScheme; return ChoiceChip( label: DidvanText( item.header, color: isSelected ? colorScheme.onPrimary : colorScheme.onSurface, fontSize: 12, fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500, ), selected: isSelected, showCheckmark: false, backgroundColor: colorScheme.surfaceContainerHighest, selectedColor: colorScheme.primary, disabledColor: colorScheme.surfaceContainerHighest, side: BorderSide( color: isSelected ? colorScheme.primary : colorScheme.outline .withValues(alpha: .45), ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 8, ), onSelected: (_) => setState( () { _selectedCategory = item.category; _visibleLimit = 40; }, ), ); }, ), ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ DidvanText( category.header, style: Theme.of(context) .textTheme .titleLarge ?.copyWith(fontWeight: FontWeight.w800), ), DidvanText( '${category.contents.length} شاخص' .toPersianDigit(), color: Theme.of(context) .colorScheme .onSurfaceVariant, ), ], ), const SizedBox(height: 12), LayoutBuilder( builder: (context, constraints) { final columns = constraints.maxWidth >= 700 ? 3 : 2; return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: visibleItems.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: columns, crossAxisSpacing: 10, mainAxisSpacing: 10, childAspectRatio: columns == 3 ? 1.35 : 1.05, ), itemBuilder: (context, index) => _MarketCard(item: visibleItems[index]), ); }, ), if (visibleItems.length < category.contents.length) ...[ const SizedBox(height: 16), OutlinedButton.icon( onPressed: () => setState(() => _visibleLimit += 40), icon: const Icon(Icons.add_rounded), label: const Text('نمایش موارد بیشتر'), ), ], ], ); }, ), ), ), ], ), ), ); } } /// Top of the «نبض صنعت» tab: a banner image managed from the panel /// (`/panel/market-symbols`) plus the last-updated timestamp. When no banner /// has been uploaded the image area is simply omitted. class _MarketHero extends StatelessWidget { final String? bannerUrl; final DateTime? generatedAt; const _MarketHero({required this.bannerUrl, required this.generatedAt}); @override Widget build(BuildContext context) { final theme = Theme.of(context); final hasBanner = bannerUrl != null && bannerUrl!.isNotEmpty; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (hasBanner) ClipRRect( borderRadius: BorderRadius.circular(24), child: AspectRatio( aspectRatio: 16 / 9, child: CachedNetworkImage( imageUrl: bannerUrl!, fit: BoxFit.cover, placeholder: (_, __) => const ShimmerPlaceholder( width: double.infinity, height: double.infinity, ), errorWidget: (_, __, ___) => const SizedBox.shrink(), ), ), ), if (generatedAt != null) ...[ SizedBox(height: hasBanner ? 10 : 0), DidvanText( 'آخرین بروزرسانی: ${_time(generatedAt!.toLocal())}' .toPersianDigit(), color: theme.colorScheme.onSurfaceVariant, fontSize: 11, ), ], ], ); } static String _time(DateTime value) => '${value.year}/${value.month.toString().padLeft(2, '0')}/${value.day.toString().padLeft(2, '0')} ${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}'; } class _MarketCard extends StatelessWidget { final Content item; const _MarketCard({required this.item}); @override Widget build(BuildContext context) { final theme = Theme.of(context); // The API percentage is the canonical change direction. A sparkline may // cover a slightly different time window and must not contradict it. final changePercent = item.data.dp.toDouble(); final isUp = changePercent > 0; final isDown = changePercent < 0; final chartTrend = isUp ? 'high' : (isDown ? 'low' : 'none'); final trendColor = isUp ? const Color(0xff19a974) : isDown ? theme.colorScheme.error : theme.colorScheme.onSurfaceVariant; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: theme.colorScheme.surface, borderRadius: BorderRadius.circular(18), border: Border.all(color: theme.dividerColor.withValues(alpha: .35)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ _MarketLogo(item: item, color: trendColor), const SizedBox(width: 9), Expanded( child: DidvanText( item.title.toPersianDigit(), maxLines: 1, overflow: TextOverflow.ellipsis, fontWeight: FontWeight.w700, ), ), if (item.label.isNotEmpty) Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), decoration: BoxDecoration( color: theme.colorScheme.primary.withValues(alpha: .08), borderRadius: BorderRadius.circular(8), ), child: DidvanText(item.label.toPersianDigit(), color: theme.colorScheme.primary, fontSize: 9), ), ], ), const Spacer(), if (item.data.prices.length > 1) MiniChart( label: item.label, width: double.infinity, height: 30, lineColor: trendColor, changePercent: item.data.dp.toDouble(), trend: chartTrend, prices: item.data.prices, ), DidvanText(item.data.p.toPersianDigit(), fontSize: 18, fontWeight: FontWeight.w800), const SizedBox(height: 5), Row( children: [ Icon( isUp ? Icons.arrow_upward_rounded : isDown ? Icons.arrow_downward_rounded : Icons.remove_rounded, color: trendColor, size: 15, ), const SizedBox(width: 3), DidvanText('${item.data.formattedAbsDp}٪'.toPersianDigit(), color: trendColor, fontSize: 12, fontWeight: FontWeight.w700), if (item.subtitle != null) ...[ const SizedBox(width: 6), Expanded( child: DidvanText( item.subtitle!.toPersianDigit(), maxLines: 1, overflow: TextOverflow.ellipsis, color: theme.colorScheme.onSurfaceVariant, fontSize: 10, textAlign: TextAlign.end, ), ), ], ], ), ], ), ); } } class _MarketLogo extends StatelessWidget { final Content item; final Color color; const _MarketLogo({required this.item, required this.color}); @override Widget build(BuildContext context) { final background = Theme.of(context).colorScheme.surfaceContainerHighest; final fallback = Icon(Icons.show_chart_rounded, color: color, size: 20); return Container( width: 36, height: 36, padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: background, shape: BoxShape.circle, border: Border.all(color: color.withValues(alpha: .18)), ), clipBehavior: Clip.antiAlias, child: item.imageUrl == null || item.imageUrl!.isEmpty ? fallback : CachedNetworkImage( imageUrl: item.imageUrl!, fit: BoxFit.contain, placeholder: (_, __) => fallback, errorWidget: (_, __, ___) => fallback, ), ); } } class _ErrorView extends StatelessWidget { final String message; final Future Function() onRetry; const _ErrorView({required this.message, required this.onRetry}); @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.only(top: 80), child: Column( children: [ Icon(Icons.cloud_off_rounded, size: 52, color: Theme.of(context).colorScheme.error), const SizedBox(height: 14), DidvanText(message, textAlign: TextAlign.center), const SizedBox(height: 12), FilledButton.icon( onPressed: onRetry, icon: const Icon(Icons.refresh_rounded), label: const Text('تلاش دوباره'), ), ], ), ); } class _EmptyView extends StatelessWidget { const _EmptyView(); @override Widget build(BuildContext context) => const Padding( padding: EdgeInsets.only(top: 80), child: Column( children: [ Icon(Icons.query_stats_rounded, size: 52), SizedBox(height: 12), DidvanText('در حال حاضر داده‌ای برای نمایش وجود ندارد.'), ], ), ); } class _IndustryPulseLoading extends StatelessWidget { const _IndustryPulseLoading(); @override Widget build(BuildContext context) => const Column( children: [ ShimmerPlaceholder( height: 190, borderRadius: BorderRadius.all(Radius.circular(24))), SizedBox(height: 20), ShimmerPlaceholder( height: 42, borderRadius: BorderRadius.all(Radius.circular(16))), SizedBox(height: 20), Row( children: [ Expanded( child: ShimmerPlaceholder( height: 170, borderRadius: BorderRadius.all(Radius.circular(18)))), SizedBox(width: 10), Expanded( child: ShimmerPlaceholder( height: 170, borderRadius: BorderRadius.all(Radius.circular(18)))), ], ), ], ); }