import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/api_client.dart'; import '../../core/app_colors.dart'; import '../../core/app_design_tokens.dart'; import '../../core/app_module_visuals.dart'; import '../../core/app_theme.dart'; import '../../core/navigation_provider.dart'; import '../../providers/data_providers.dart'; import '../../widgets/app_empty_state.dart'; import '../../widgets/app_future_view.dart'; import '../../widgets/app_toast.dart'; import '../../widgets/common_widgets.dart'; import '../care_plan_ui_logic.dart'; class EnterpriseExercisePlanPage extends ConsumerStatefulWidget { const EnterpriseExercisePlanPage({super.key}); @override ConsumerState createState() => _EnterpriseExercisePlanPageState(); } class _EnterpriseExercisePlanPageState extends ConsumerState { Future>>? _future; final Set _busyItems = {}; final Set _deletingPlans = {}; @override void initState() { super.initState(); _load(); } void _load() => setState(() { _future = ref.read(exerciseServiceProvider).getPlans(); }); Future _refresh() async { _load(); await _future; } Future _toggleCheckIn(String itemId) async { if (itemId.isEmpty || _busyItems.contains(itemId)) return; setState(() => _busyItems.add(itemId)); try { await ref.read(exerciseServiceProvider).checkIn(itemId); ref.invalidate(currentExercisePlanProvider); _load(); } catch (error) { if (mounted) { AppToast.show( context, error is ApiException ? error.message : '打卡失败,请稍后重试', type: AppToastType.error, ); } } finally { if (mounted) setState(() => _busyItems.remove(itemId)); } } Future _confirmDelete(String id, String title) async { if (id.isEmpty || _deletingPlans.contains(id)) return; final confirmed = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('删除运动计划'), content: Text('确定删除“$title”吗?删除后无法恢复。'), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: const Text('取消'), ), TextButton( onPressed: () => Navigator.pop(dialogContext, true), style: TextButton.styleFrom(foregroundColor: AppColors.errorText), child: const Text('删除'), ), ], ), ); if (confirmed != true || !mounted) return; setState(() => _deletingPlans.add(id)); try { await ref.read(exerciseServiceProvider).deletePlan(id); ref.invalidate(currentExercisePlanProvider); _load(); if (mounted) { AppToast.show(context, '运动计划已删除', type: AppToastType.success); } } catch (error) { if (mounted) { AppToast.show( context, error is ApiException ? error.message : '删除失败,请稍后重试', type: AppToastType.error, ); } } finally { if (mounted) setState(() => _deletingPlans.remove(id)); } } @override Widget build(BuildContext context) { return GradientScaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => popRoute(ref), ), title: const Text('运动计划'), ), floatingActionButton: AppCreateFab( tooltip: '新建运动计划', onPressed: () => pushRoute(ref, 'exerciseCreate'), ), body: AppFutureView>>( future: _future, onRetry: _load, errorTitle: '运动计划加载失败', onData: (_, plans) { if (plans.isEmpty) { return AppEmptyState( icon: AppModuleVisuals.exercise.icon, title: '暂无运动计划', subtitle: '点击右下角添加运动计划', iconColor: AppColors.exercise, ); } final todayKey = _dateKey(DateTime.now()); final validPlans = plans.where((plan) { return (plan['items'] as List?)?.isNotEmpty == true; }).toList(); final todayItems = validPlans .expand(_itemsOf) .where((item) => item['scheduledDate']?.toString() == todayKey) .where((item) => item['isRestDay'] != true) .toList(); final completedToday = todayItems .where((item) => item['isCompleted'] == true) .length; final groups = >>{ for (final phase in CarePlanPhase.values) phase: [], }; for (final plan in validPlans) { groups[resolveCarePlanPhase( startDate: plan['startDate']?.toString(), endDate: plan['endDate']?.toString(), )]! .add(plan); } return RefreshIndicator( onRefresh: _refresh, child: ListView( physics: const AlwaysScrollableScrollPhysics(), padding: AppSpacing.pageWithFab, children: [ _TodayExercisePanel( items: todayItems, completed: completedToday, busyItems: _busyItems, onCheckIn: _toggleCheckIn, ), const SizedBox(height: 18), for (final phase in CarePlanPhase.values) if (groups[phase]!.isNotEmpty) ...[ _ExercisePlanGroup( phase: phase, plans: groups[phase]!, todayKey: todayKey, busyItems: _busyItems, deletingPlans: _deletingPlans, onCheckIn: _toggleCheckIn, onDelete: _confirmDelete, ), const SizedBox(height: 18), ], ], ), ); }, ), ); } } class _TodayExercisePanel extends StatelessWidget { final List> items; final int completed; final Set busyItems; final Future Function(String) onCheckIn; const _TodayExercisePanel({ required this.items, required this.completed, required this.busyItems, required this.onCheckIn, }); @override Widget build(BuildContext context) { final progress = items.isEmpty ? 0.0 : completed / items.length; final next = items.cast?>().firstWhere( (item) => item?['isCompleted'] != true, orElse: () => null, ); if (items.isEmpty) { return Container( padding: AppSpacing.panel, decoration: BoxDecoration( color: Colors.white, borderRadius: AppRadius.lgBorder, ), child: Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: AppColors.exerciseLight, borderRadius: AppRadius.smBorder, ), child: Icon( AppModuleVisuals.exercise.icon, color: AppColors.exercise, size: 22, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('今日休息', style: AppTextStyles.sectionTitle), SizedBox(height: 3), Text('今天没有安排运动,保持轻松活动即可', style: AppTextStyles.listSubtitle), ], ), ), ], ), ); } return Container( padding: AppSpacing.panel, decoration: BoxDecoration( color: Colors.white, borderRadius: AppRadius.lgBorder, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('今日任务', style: AppTextStyles.sectionTitle), const SizedBox(height: 3), Text( '$completed / ${items.length} 已完成', style: AppTextStyles.listSubtitle, ), ], ), ), if (next != null) _CompactActionButton( busy: busyItems.contains(next['id']?.toString() ?? ''), completed: false, onPressed: () => onCheckIn(next['id']?.toString() ?? ''), ), ], ), const SizedBox(height: 14), ClipRRect( borderRadius: AppRadius.pillBorder, child: LinearProgressIndicator( minHeight: 5, value: progress, backgroundColor: AppColors.cardInner, valueColor: const AlwaysStoppedAnimation(AppColors.exercise), ), ), if (next != null) ...[ const SizedBox(height: 12), Text( '${next['exerciseType'] ?? '运动'} · ${next['durationMinutes'] ?? 0} 分钟', style: AppTextStyles.listTitle.copyWith(fontSize: 16), ), ], ], ), ); } } class _ExercisePlanGroup extends StatelessWidget { final CarePlanPhase phase; final List> plans; final String todayKey; final Set busyItems; final Set deletingPlans; final Future Function(String) onCheckIn; final Future Function(String, String) onDelete; const _ExercisePlanGroup({ required this.phase, required this.plans, required this.todayKey, required this.busyItems, required this.deletingPlans, required this.onCheckIn, required this.onDelete, }); @override Widget build(BuildContext context) { final color = _phaseColor(phase); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(left: 2, bottom: 9), child: Row( children: [ Container( width: 8, height: 8, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), const SizedBox(width: 8), Text( '${_phaseLabel(phase)}(${plans.length})', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w800, color: color, ), ), ], ), ), Container( clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: Colors.white, borderRadius: AppRadius.lgBorder, ), child: Column( children: List.generate(plans.length, (index) { final plan = plans[index]; final items = _itemsOf(plan); final id = plan['id']?.toString() ?? ''; final title = _exerciseTitle(items); final isDeleting = deletingPlans.contains(id); return SwipeDeleteTile( key: ValueKey(id), margin: EdgeInsets.zero, borderRadius: BorderRadius.zero, enabled: !isDeleting, onDelete: () => onDelete(id, title), child: _ExercisePlanRow( plan: plan, title: title, phase: phase, todayKey: todayKey, busyItems: busyItems, deleting: isDeleting, showDivider: index < plans.length - 1, onCheckIn: onCheckIn, ), ); }), ), ), ], ); } } class _ExercisePlanRow extends StatelessWidget { final Map plan; final String title; final CarePlanPhase phase; final String todayKey; final Set busyItems; final bool deleting; final bool showDivider; final Future Function(String) onCheckIn; const _ExercisePlanRow({ required this.plan, required this.title, required this.phase, required this.todayKey, required this.busyItems, required this.deleting, required this.showDivider, required this.onCheckIn, }); @override Widget build(BuildContext context) { final items = _itemsOf(plan); final done = items.where((item) => item['isCompleted'] == true).length; final progress = items.isEmpty ? 0.0 : done / items.length; final todayItem = items.cast?>().firstWhere( (item) => item?['scheduledDate']?.toString() == todayKey, orElse: () => null, ); final canCheckIn = phase == CarePlanPhase.active && todayItem != null && todayItem['isRestDay'] != true; final itemId = todayItem?['id']?.toString() ?? ''; final completed = todayItem?['isCompleted'] == true; return Material( color: Colors.white, child: Stack( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 12, 12, 12), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: AppColors.exerciseLight, borderRadius: AppRadius.smBorder, ), child: Icon( AppModuleVisuals.exercise.icon, color: AppColors.exercise, size: 22, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, style: AppTextStyles.listTitle.copyWith( fontSize: 17, ), ), ), const SizedBox(width: 8), Text( '$done/${items.length}天', style: AppTextStyles.tag, ), ], ), const SizedBox(height: 4), Text( '${_compactDate(plan['startDate'])} - ${_compactDate(plan['endDate'])}', style: AppTextStyles.listSubtitle.copyWith( fontSize: 13, ), ), const SizedBox(height: 8), ClipRRect( borderRadius: AppRadius.pillBorder, child: LinearProgressIndicator( minHeight: 4, value: progress, backgroundColor: AppColors.cardInner, valueColor: AlwaysStoppedAnimation( _phaseColor(phase), ), ), ), ], ), ), if (canCheckIn && !deleting) ...[ const SizedBox(width: 8), _CompactActionButton( busy: busyItems.contains(itemId) || deleting, completed: completed, onPressed: () => onCheckIn(itemId), ), ], if (deleting) const Padding( padding: EdgeInsets.all(12), child: SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ), ), ], ), ), if (showDivider) const Positioned( left: 68, right: 0, bottom: 0, child: Divider( height: 1, thickness: 0.7, color: AppColors.divider, ), ), ], ), ); } } class _CompactActionButton extends StatelessWidget { final bool busy; final bool completed; final VoidCallback onPressed; const _CompactActionButton({ required this.busy, required this.completed, required this.onPressed, }); @override Widget build(BuildContext context) { return SizedBox( height: 38, child: OutlinedButton( onPressed: busy ? null : onPressed, style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 14), backgroundColor: completed ? AppColors.successLight : Colors.white, foregroundColor: completed ? AppColors.successText : AppColors.exercise, side: BorderSide( color: completed ? AppColors.successText : AppColors.exercise, ), shape: RoundedRectangleBorder(borderRadius: AppRadius.smBorder), ), child: busy ? const SizedBox( width: 15, height: 15, child: CircularProgressIndicator(strokeWidth: 2), ) : Text(completed ? '撤销' : '打卡', style: AppTextStyles.miniButton), ), ); } } List> _itemsOf(Map plan) => (plan['items'] as List?)?.cast>() ?? []; String _exerciseTitle(List> items) { final names = items .where((item) => item['isRestDay'] != true) .map((item) => item['exerciseType']?.toString().trim() ?? '') .where((name) => name.isNotEmpty) .toSet() .toList(); if (names.isEmpty) return '运动计划'; return names.length == 1 ? names.first : '${names.first}等${names.length}项'; } String _dateKey(DateTime date) => '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; String _compactDate(Object? value) { final parsed = DateTime.tryParse(value?.toString() ?? ''); return parsed == null ? '--' : '${parsed.year}.${parsed.month.toString().padLeft(2, '0')}.${parsed.day.toString().padLeft(2, '0')}'; } String _phaseLabel(CarePlanPhase phase) => switch (phase) { CarePlanPhase.active => '进行中', CarePlanPhase.upcoming => '即将开始', CarePlanPhase.ended => '已结束', }; Color _phaseColor(CarePlanPhase phase) => switch (phase) { CarePlanPhase.active => AppColors.successText, CarePlanPhase.upcoming => AppColors.blueMeasure, CarePlanPhase.ended => AppColors.textHint, };