import 'package:flutter/material.dart'; import '../../../models/programmer_day_sheet.model.dart'; import '../../../utils/programmer_day_rows.dart'; /// Pure helper: if the sheet is approved with a snapshot, returns the snapshot /// rows; otherwise returns the live rows. List rowsForSheet( ProgrammerDaySheet sheet, List liveRows, ) { if (sheet.status == DaySheetStatus.approved && sheet.approvedSnapshot != null) { return sheet.approvedSnapshot!.rows; } return liveRows; } // --------------------------------------------------------------------------- // showDisapproveDialog // --------------------------------------------------------------------------- /// Shows an AlertDialog collecting disapproval remarks and optionally /// toggling which rows are flagged. Executes [onConfirm] while showing a /// loading state; handles errors inline. Returns true if the action /// completed, or null if cancelled. Future showDisapproveDialog( BuildContext context, { required List flaggedRows, required Future Function( String remarks, List flaggedTaskIds) onConfirm, }) { return showDialog( context: context, barrierDismissible: false, builder: (ctx) => _DisapproveDialog(flaggedRows: flaggedRows, onConfirm: onConfirm), ); } class _DisapproveDialog extends StatefulWidget { const _DisapproveDialog({required this.flaggedRows, required this.onConfirm}); final List flaggedRows; final Future Function(String remarks, List flaggedTaskIds) onConfirm; @override State<_DisapproveDialog> createState() => _DisapproveDialogState(); } class _DisapproveDialogState extends State<_DisapproveDialog> { final _remarksCtrl = TextEditingController(); String? _error; bool _loading = false; late Set _selected; @override void initState() { super.initState(); _selected = {for (final r in widget.flaggedRows) r.taskId}; } @override void dispose() { _remarksCtrl.dispose(); super.dispose(); } Future _submit() async { final remarks = _remarksCtrl.text.trim(); if (remarks.isEmpty) { setState(() => _error = 'Remarks are required'); return; } setState(() { _loading = true; _error = null; }); try { await widget.onConfirm(remarks, _selected.toList()); if (mounted) Navigator.of(context).pop(true); } catch (e) { if (mounted) setState(() { _loading = false; _error = e.toString(); }); } } @override Widget build(BuildContext context) { final tt = Theme.of(context).textTheme; final cs = Theme.of(context).colorScheme; return AlertDialog( title: const Text('Disapprove'), content: SizedBox( width: 360, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( controller: _remarksCtrl, maxLines: 3, decoration: InputDecoration( labelText: 'Remarks', hintText: 'Explain what needs to change', errorText: _error, border: const OutlineInputBorder(), ), onChanged: (_) { if (_error != null) setState(() => _error = null); }, ), if (widget.flaggedRows.isNotEmpty) ...[ const SizedBox(height: 16), Text( 'Flagged tasks', style: tt.labelMedium?.copyWith( color: cs.onSurfaceVariant, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 4), for (final row in widget.flaggedRows) CheckboxListTile( dense: true, contentPadding: EdgeInsets.zero, title: Text(row.title, style: tt.bodySmall), value: _selected.contains(row.taskId), onChanged: (v) { setState(() { if (v == true) { _selected.add(row.taskId); } else { _selected.remove(row.taskId); } }); }, ), ], ], ), ), ), actions: [ TextButton( onPressed: _loading ? null : () => Navigator.of(context).pop(null), child: const Text('Cancel'), ), FilledButton( onPressed: _loading ? null : _submit, child: _loading ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Disapprove'), ), ], ); } } // --------------------------------------------------------------------------- // showJustifyDialog // --------------------------------------------------------------------------- /// Shows an AlertDialog for the programmer to write a justification before /// resubmitting. Executes [onConfirm] while showing a loading state; handles /// errors inline. Returns true if the action completed, or null if cancelled. Future showJustifyDialog( BuildContext context, { required Future Function(String body) onConfirm, }) { return showDialog( context: context, barrierDismissible: false, builder: (ctx) => _JustifyDialog(onConfirm: onConfirm), ); } class _JustifyDialog extends StatefulWidget { const _JustifyDialog({required this.onConfirm}); final Future Function(String body) onConfirm; @override State<_JustifyDialog> createState() => _JustifyDialogState(); } class _JustifyDialogState extends State<_JustifyDialog> { final _bodyCtrl = TextEditingController(); String? _error; bool _loading = false; @override void dispose() { _bodyCtrl.dispose(); super.dispose(); } Future _submit() async { final body = _bodyCtrl.text.trim(); if (body.isEmpty) { setState(() => _error = 'Please write a justification'); return; } setState(() { _loading = true; _error = null; }); try { await widget.onConfirm(body); if (mounted) Navigator.of(context).pop(true); } catch (e) { if (mounted) setState(() { _loading = false; _error = e.toString(); }); } } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Justify & Resubmit'), content: SizedBox( width: 360, child: TextField( controller: _bodyCtrl, maxLines: 4, enabled: !_loading, decoration: InputDecoration( labelText: 'Justification', hintText: 'Explain what changed or why the sheet is correct', errorText: _error, border: const OutlineInputBorder(), ), onChanged: (_) { if (_error != null) setState(() => _error = null); }, ), ), actions: [ TextButton( onPressed: _loading ? null : () => Navigator.of(context).pop(null), child: const Text('Cancel'), ), FilledButton( onPressed: _loading ? null : _submit, child: _loading ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Submit'), ), ], ); } }