import 'package:flutter/material.dart'; import 'package:flutter/semantics.dart'; import 'package:patbond_flutter/core/network/api_exception.dart'; import 'package:patbond_flutter/core/theme/app_theme.dart'; import 'package:patbond_flutter/core/widgets/app_text_field.dart'; import 'package:patbond_flutter/core/widgets/inline_error_banner.dart'; import 'package:patbond_flutter/core/widgets/primary_button.dart'; import 'package:patbond_flutter/features/pets/health_record_analytics.dart'; import 'package:patbond_flutter/features/pets/health_record_display.dart'; import 'package:patbond_flutter/features/pets/pet_exceptions.dart'; import 'package:patbond_flutter/features/pets/pet_models.dart'; import 'package:patbond_flutter/features/pets/pets_repository.dart'; /// 照护提醒创建表单页(T2-14)。 /// /// - 四类提醒类型(驱虫/体检/用药/其他)必选;标题必填;到期日期必选 /// (今日取此刻、未来日期取当日 12:00,转 UTC——与记录表单同一约定)。 /// - 契约:创建恒为 pending(不收 status);title/dueAt 无编辑端点, /// 改期路径为忽略后重建。 /// - 埋点:health_record_create_started/succeeded/failed /// (recordType=reminder)。 class CareReminderFormPage extends StatefulWidget { const CareReminderFormPage({ required this.repository, required this.petId, super.key, this.analytics, this.entryPoint = HealthRecordEntryPoint.recordList, }); final PetsRepository repository; final String petId; final HealthRecordAnalytics? analytics; final HealthRecordEntryPoint entryPoint; @override State createState() => _CareReminderFormPageState(); } class _CareReminderFormPageState extends State { final _titleCtrl = TextEditingController(); CareReminderType? _type; DateTime? _dueDate; String? _typeError; String? _titleError; String? _dueError; String? _formError; bool _submitting = false; bool _startedFired = false; int _attemptSeq = 0; late final DateTime _openedAt; @override void initState() { super.initState(); _openedAt = DateTime.now(); } @override void dispose() { _titleCtrl.dispose(); super.dispose(); } void _markStarted() { if (_startedFired) return; _startedFired = true; widget.analytics?.createStarted( recordType: HealthRecordType.reminder, entryPoint: widget.entryPoint, ); } void _trackFailed(HealthRecordFailureReason reason, [int? errorCode]) { widget.analytics?.createFailed( recordType: HealthRecordType.reminder, reason: reason, attemptSeq: _attemptSeq, errorCode: errorCode, ); } void _showFormError(String message) { setState(() => _formError = message); SemanticsService.sendAnnouncement( View.of(context), message, TextDirection.ltr, ); } /// 到期时刻:今日取此刻,其余日期取当日 12:00(本地),提交前转 UTC。 DateTime _dueAt() { final now = DateTime.now(); final date = _dueDate!; final sameDay = date.year == now.year && date.month == now.month && date.day == now.day; final local = sameDay ? now : DateTime(date.year, date.month, date.day, 12); return local.toUtc(); } Future _submit() async { if (_submitting) return; _attemptSeq++; final typeError = _type == null ? '请选择提醒类型' : null; final titleError = _titleCtrl.text.trim().isEmpty ? '请输入提醒内容' : null; final dueError = _dueDate == null ? '请选择到期日期' : null; if (typeError != null || titleError != null || dueError != null) { setState(() { _typeError = typeError; _titleError = titleError; _dueError = dueError; }); _trackFailed(HealthRecordFailureReason.validationError); return; } setState(() { _submitting = true; _formError = null; }); try { final reminder = await widget.repository.createCareReminder( widget.petId, CreateCareReminderRequest( reminderType: _type!, title: _titleCtrl.text.trim(), dueAt: _dueAt(), ), ); widget.analytics?.createSucceeded( recordType: HealthRecordType.reminder, durationMs: DateTime.now().difference(_openedAt).inMilliseconds, ); if (mounted) Navigator.of(context).pop(reminder); } on PetAccessDeniedException { if (!mounted) return; _showFormError('你没有权限为该宠物添加提醒'); _trackFailed(HealthRecordFailureReason.permissionDenied, 40300); } on PetNotFoundException { if (!mounted) return; final navigator = Navigator.of(context); ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('宠物不存在或已被删除'))); _trackFailed(HealthRecordFailureReason.notFound, 40401); navigator.pop(); } on ApiRateLimitException { if (!mounted) return; _showFormError('操作过于频繁,请稍后再试'); _trackFailed(HealthRecordFailureReason.rateLimited); } on ApiBusinessException catch (error) { if (!mounted) return; final isParam = error.code == ApiCodes.paramError; _showFormError(isParam ? '请检查填写内容后重试' : '保存失败,请稍后重试'); _trackFailed( isParam ? HealthRecordFailureReason.validationError : HealthRecordFailureReason.serverError, error.code, ); } on ApiNetworkException { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('网络异常,请检查网络后重试'), action: SnackBarAction(label: '重试', onPressed: _submit), ), ); _trackFailed(HealthRecordFailureReason.networkError); } on SessionExpiredException { // 会话失效:认证状态机自动回登录页。 } finally { if (mounted) setState(() => _submitting = false); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, foregroundColor: AppColors.ink, title: const Text('添加照护提醒'), centerTitle: true, titleTextStyle: const TextStyle( color: AppColors.ink, fontSize: 16, fontWeight: FontWeight.w800, ), ), body: SafeArea( child: ListView( padding: const EdgeInsets.fromLTRB(20, 8, 20, 30), children: [ const Text( '提醒类型', style: TextStyle( color: AppColors.inkSoft, fontSize: 13, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: [ for (final type in CareReminderType.values) ChoiceChip( label: Text(careReminderTypeLabel(type)), selected: _type == type, onSelected: _submitting ? null : (_) { _markStarted(); setState(() { _type = type; _typeError = null; _formError = null; }); }, ), ], ), if (_typeError != null) ...[ const SizedBox(height: 6), Text( _typeError!, style: const TextStyle(color: AppColors.error, fontSize: 12), ), ], const SizedBox(height: 18), AppTextField( label: '提醒内容(如:体内外驱虫)', controller: _titleCtrl, prefixIcon: Icons.notifications_outlined, errorText: _titleError, enabled: !_submitting, textInputAction: TextInputAction.done, onChanged: (_) { _markStarted(); if (_titleError != null || _formError != null) { setState(() { _titleError = null; _formError = null; }); } }, onSubmitted: (_) => _submit(), ), const SizedBox(height: 14), ListTile( shape: RoundedRectangleBorder( side: const BorderSide(color: AppColors.border), borderRadius: BorderRadius.circular(AppRadius.lg), ), tileColor: AppColors.surface, leading: const Icon(Icons.event_outlined, color: AppColors.muted), title: const Text('到期日期', style: TextStyle(fontSize: 14)), subtitle: Text( _dueDate == null ? '未选择' : dateToJson(_dueDate!), style: const TextStyle(color: AppColors.inkSoft, fontSize: 12), ), trailing: const Icon( Icons.calendar_month_outlined, color: AppColors.muted, ), enabled: !_submitting, onTap: () async { final now = DateTime.now(); final value = await showDatePicker( context: context, initialDate: _dueDate ?? now, firstDate: now, lastDate: DateTime(now.year + 5), ); if (value != null && mounted) { _markStarted(); setState(() { _dueDate = value; _dueError = null; }); } }, ), if (_dueError != null) ...[ const SizedBox(height: 6), Text( _dueError!, style: const TextStyle(color: AppColors.error, fontSize: 12), ), ], if (_formError != null) ...[ const SizedBox(height: 16), InlineErrorBanner(message: _formError!), ], const SizedBox(height: 24), PrimaryButton( label: '保存提醒', isLoading: _submitting, onPressed: _submit, ), ], ), ), ); } }