import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../models/profile.model.dart'; import '../../../models/programmer_task.model.dart'; import '../../../providers/profile_provider.dart'; import '../../../providers/programmer_task_work_logs_provider.dart'; import '../../../utils/app_time.dart'; import '../../../utils/programmer_task_ledger.dart'; import '../../../widgets/app_section_header.dart'; import '../../../widgets/quill_field.dart'; import '../programmer_task_prompts.dart'; /// Work-log list + composer. The assignee logs progress notes (no time field); /// a non-assignee helper gets Hours + Minutes fields and is prompted to deduct /// that time from their own running task. The assignee is prompted to pause /// after logging. /// /// When [workDate] is supplied the entry is a correction for a past day: /// the header names that date and the pause/deduct prompts are skipped. class WorkLogSection extends ConsumerStatefulWidget { const WorkLogSection({super.key, required this.task, required this.isAssignee, this.workDate}); final ProgrammerTask task; final bool isAssignee; final DateTime? workDate; @override ConsumerState createState() => _WorkLogSectionState(); } class _WorkLogSectionState extends ConsumerState { final _descQuill = quillControllerFromDelta(null); final _hoursCtrl = TextEditingController(); final _minutesCtrl = TextEditingController(); bool _saving = false; @override void dispose() { _descQuill.dispose(); _hoursCtrl.dispose(); _minutesCtrl.dispose(); super.dispose(); } Future _submit() async { if (_saving) return; if (quillPlainText(_descQuill).isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Describe what you worked on')), ); return; } int? minutes; if (!widget.isAssignee) { final h = int.tryParse(_hoursCtrl.text.trim()) ?? 0; final m = int.tryParse(_minutesCtrl.text.trim()) ?? 0; minutes = h * 60 + m; if (minutes <= 0) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Enter the time you spent helping')), ); return; } } setState(() => _saving = true); final messenger = ScaffoldMessenger.of(context); try { await ref.read(programmerTaskWorkLogsControllerProvider).addWorkLog( taskId: widget.task.id, description: quillToDeltaJson(_descQuill), minutes: minutes, workDate: widget.workDate, ); _descQuill.clear(); _hoursCtrl.clear(); _minutesCtrl.clear(); if (!mounted) return; if (widget.workDate == null) { if (widget.isAssignee) { await maybePromptPauseTimer(context, ref, widget.task); } else if (minutes != null) { await maybePromptDeductHelperTime(context, ref, minutes, widget.task.id); } } } catch (e) { messenger.showSnackBar(SnackBar(content: Text('Could not save: $e'))); } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { final tt = Theme.of(context).textTheme; final cs = Theme.of(context).colorScheme; final logsAsync = ref.watch(programmerTaskWorkLogsProvider(widget.task.id)); final profiles = ref.watch(profilesProvider).valueOrNull ?? const []; final profileById = {for (final p in profiles) p.id: p}; final logs = logsAsync.valueOrNull ?? const []; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AppSectionHeader( widget.workDate != null ? 'Correction for ${AppTime.formatDate(widget.workDate!)}' : 'Work Log', ), QuillFieldEditor( controller: _descQuill, placeholder: widget.isAssignee ? 'What did you work on?' : 'What did you help with?', height: 120, ), const SizedBox(height: 8), Row( children: [ if (!widget.isAssignee) ...[ SizedBox( width: 78, child: TextField( controller: _hoursCtrl, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], decoration: const InputDecoration( labelText: 'Hours', isDense: true), ), ), const SizedBox(width: 8), SizedBox( width: 88, child: TextField( controller: _minutesCtrl, keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], decoration: const InputDecoration( labelText: 'Minutes', isDense: true), ), ), ], const Spacer(), FilledButton.tonalIcon( onPressed: _saving ? null : _submit, icon: _saving ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.add, size: 18), label: const Text('Log'), ), ], ), const SizedBox(height: 12), if (logs.isEmpty) Text('No work logged yet.', style: tt.bodySmall?.copyWith(color: cs.onSurfaceVariant)) else for (final log in logs) Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( profileById[log.authorId]?.fullName ?? 'Someone', style: tt.bodyMedium ?.copyWith(fontWeight: FontWeight.w600), ), ), if (log.minutes != null) Padding( padding: const EdgeInsets.only(right: 8), child: Text( formatDurationHm(Duration(minutes: log.minutes!)), style: tt.bodySmall?.copyWith( color: cs.primary, fontWeight: FontWeight.w700), ), ), Text(AppTime.relative(log.createdAt), style: tt.bodySmall ?.copyWith(color: cs.onSurfaceVariant)), ], ), const SizedBox(height: 2), QuillReadOnly(log.description, style: tt.bodyMedium), ], ), ), ], ); } }