diff --git a/lib/screens/programmer_tasks/day_sheets/approvals_tab.dart b/lib/screens/programmer_tasks/day_sheets/approvals_tab.dart index 376b6643..b02eed2e 100644 --- a/lib/screens/programmer_tasks/day_sheets/approvals_tab.dart +++ b/lib/screens/programmer_tasks/day_sheets/approvals_tab.dart @@ -87,12 +87,27 @@ class _ApprovalsTabState extends ConsumerState { _approving = false; }); - final buf = StringBuffer('Approved ${result.approvedIds.length}'); - if (result.skippedIds.isNotEmpty) { - buf.write(', ${result.skippedIds.length} already reviewed'); - } - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(buf.toString()))); + final count = result.approvedIds.length; + final skipped = result.skippedIds.length; + await showDialog( + context: context, + builder: (ctx) => AlertDialog( + icon: const Icon(Icons.check_circle_outline, size: 48), + title: Text('Approved $count ${count == 1 ? 'Sheet' : 'Sheets'}'), + content: skipped > 0 + ? Text( + '$skipped ${skipped == 1 ? 'sheet was' : 'sheets were'} ' + 'already reviewed and skipped.', + ) + : null, + actions: [ + FilledButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Done'), + ), + ], + ), + ); } on DaySheetActionException catch (e) { if (!mounted) return; setState(() => _approving = false); diff --git a/lib/screens/programmer_tasks/day_sheets/day_sheet_detail_screen.dart b/lib/screens/programmer_tasks/day_sheets/day_sheet_detail_screen.dart index bd1b5bd1..f0ff85e8 100644 --- a/lib/screens/programmer_tasks/day_sheets/day_sheet_detail_screen.dart +++ b/lib/screens/programmer_tasks/day_sheets/day_sheet_detail_screen.dart @@ -35,6 +35,7 @@ class DaySheetDetailScreen extends ConsumerStatefulWidget { class _DaySheetDetailScreenState extends ConsumerState { Set _selectedTaskIds = {}; + bool _approvingSheet = false; // --------------------------------------------------------------------------- // Helpers @@ -95,16 +96,16 @@ class _DaySheetDetailScreenState extends ConsumerState { } Future _onJustify(String sheetId) async { - final body = await showJustifyDialog(context); - if (body == null || !mounted) return; - try { - await ref + final done = await showJustifyDialog( + context, + onConfirm: (body) => ref .read(daySheetsControllerProvider) - .justify(sheetId: sheetId, body: body); - } on DaySheetActionException catch (e) { - _showError(e.message); - } catch (e) { - _showError(e.toString()); + .justify(sheetId: sheetId, body: body), + ); + if (done == true && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Justification submitted')), + ); } } @@ -118,20 +119,21 @@ class _DaySheetDetailScreenState extends ConsumerState { ) async { final flaggedRows = rows.where((r) => _selectedTaskIds.contains(r.taskId)).toList(); - final remarks = - await showDisapproveDialog(context, flaggedRows: flaggedRows); - if (remarks == null || !mounted) return; - try { - await ref.read(daySheetsControllerProvider).disapprove( - sheetId: sheetId, - remarks: remarks, - flaggedTaskIds: flaggedRows.map((r) => r.taskId).toList(), - ); + final done = await showDisapproveDialog( + context, + flaggedRows: flaggedRows, + onConfirm: (remarks, flaggedTaskIds) => + ref.read(daySheetsControllerProvider).disapprove( + sheetId: sheetId, + remarks: remarks, + flaggedTaskIds: flaggedTaskIds, + ), + ); + if (done == true && mounted) { setState(() => _selectedTaskIds = {}); - } on DaySheetActionException catch (e) { - _showError(e.message); - } catch (e) { - _showError(e.toString()); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Sheet disapproved')), + ); } } @@ -178,15 +180,33 @@ class _DaySheetDetailScreenState extends ConsumerState { } } + setState(() => _approvingSheet = true); final snapshot = DaySheetSnapshot.fromRows(rows); try { await ref.read(daySheetsControllerProvider).approve( [(sheetId: sheet.id, snapshot: snapshot)], ); - setState(() => _selectedTaskIds = {}); + if (!mounted) return; + setState(() { _approvingSheet = false; _selectedTaskIds = {}; }); + await showDialog( + context: context, + builder: (ctx) => AlertDialog( + icon: const Icon(Icons.check_circle_outline, size: 48), + title: const Text('Sheet Approved'), + content: Text("$programmerName's day sheet has been approved."), + actions: [ + FilledButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Done'), + ), + ], + ), + ); } on DaySheetActionException catch (e) { + if (mounted) setState(() => _approvingSheet = false); _showError(e.message); } catch (e) { + if (mounted) setState(() => _approvingSheet = false); _showError(e.toString()); } } @@ -382,15 +402,22 @@ class _DaySheetDetailScreenState extends ConsumerState { const SizedBox(width: 12), Expanded( child: FilledButton.icon( - icon: const Icon(Icons.check_circle_outline), + icon: _approvingSheet + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2), + ) + : const Icon(Icons.check_circle_outline), label: const Text('Approve'), - onPressed: isOnline - ? () => _onApprove( + onPressed: (_approvingSheet || !isOnline) + ? null + : () => _onApprove( sheet, rows, programmerName, - ) - : null, + ), ), ), ], diff --git a/lib/screens/programmer_tasks/day_sheets/day_sheet_dialogs.dart b/lib/screens/programmer_tasks/day_sheets/day_sheet_dialogs.dart index 58f6bc31..70cd192f 100644 --- a/lib/screens/programmer_tasks/day_sheets/day_sheet_dialogs.dart +++ b/lib/screens/programmer_tasks/day_sheets/day_sheet_dialogs.dart @@ -21,24 +21,30 @@ List rowsForSheet( // --------------------------------------------------------------------------- /// Shows an AlertDialog collecting disapproval remarks and optionally -/// toggling which rows are flagged. -/// -/// Returns the non-empty remarks string on confirm, or `null` on cancel. -Future showDisapproveDialog( +/// 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( + return showDialog( context: context, barrierDismissible: false, - builder: (ctx) => _DisapproveDialog(flaggedRows: flaggedRows), + builder: (ctx) => + _DisapproveDialog(flaggedRows: flaggedRows, onConfirm: onConfirm), ); } class _DisapproveDialog extends StatefulWidget { - const _DisapproveDialog({required this.flaggedRows}); + const _DisapproveDialog({required this.flaggedRows, required this.onConfirm}); final List flaggedRows; + final Future Function(String remarks, List flaggedTaskIds) + onConfirm; @override State<_DisapproveDialog> createState() => _DisapproveDialogState(); @@ -47,6 +53,7 @@ class _DisapproveDialog extends StatefulWidget { class _DisapproveDialogState extends State<_DisapproveDialog> { final _remarksCtrl = TextEditingController(); String? _error; + bool _loading = false; late Set _selected; @override @@ -61,13 +68,19 @@ class _DisapproveDialogState extends State<_DisapproveDialog> { super.dispose(); } - void _submit() { + Future _submit() async { final remarks = _remarksCtrl.text.trim(); if (remarks.isEmpty) { setState(() => _error = 'Remarks are required'); return; } - Navigator.of(context).pop(remarks); + 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 @@ -130,12 +143,18 @@ class _DisapproveDialogState extends State<_DisapproveDialog> { ), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(null), + onPressed: _loading ? null : () => Navigator.of(context).pop(null), child: const Text('Cancel'), ), FilledButton( - onPressed: _submit, - child: const Text('Disapprove'), + onPressed: _loading ? null : _submit, + child: _loading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Disapprove'), ), ], ); @@ -147,19 +166,23 @@ class _DisapproveDialogState extends State<_DisapproveDialog> { // --------------------------------------------------------------------------- /// Shows an AlertDialog for the programmer to write a justification before -/// resubmitting. -/// -/// Returns the non-empty justification string on confirm, or `null` on cancel. -Future showJustifyDialog(BuildContext context) { - return showDialog( +/// 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) => const _JustifyDialog(), + builder: (ctx) => _JustifyDialog(onConfirm: onConfirm), ); } class _JustifyDialog extends StatefulWidget { - const _JustifyDialog(); + const _JustifyDialog({required this.onConfirm}); + + final Future Function(String body) onConfirm; @override State<_JustifyDialog> createState() => _JustifyDialogState(); @@ -168,6 +191,7 @@ class _JustifyDialog extends StatefulWidget { class _JustifyDialogState extends State<_JustifyDialog> { final _bodyCtrl = TextEditingController(); String? _error; + bool _loading = false; @override void dispose() { @@ -175,13 +199,19 @@ class _JustifyDialogState extends State<_JustifyDialog> { super.dispose(); } - void _submit() { + Future _submit() async { final body = _bodyCtrl.text.trim(); if (body.isEmpty) { setState(() => _error = 'Please write a justification'); return; } - Navigator.of(context).pop(body); + 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 @@ -193,6 +223,7 @@ class _JustifyDialogState extends State<_JustifyDialog> { child: TextField( controller: _bodyCtrl, maxLines: 4, + enabled: !_loading, decoration: InputDecoration( labelText: 'Justification', hintText: 'Explain what changed or why the sheet is correct', @@ -206,12 +237,18 @@ class _JustifyDialogState extends State<_JustifyDialog> { ), actions: [ TextButton( - onPressed: () => Navigator.of(context).pop(null), + onPressed: _loading ? null : () => Navigator.of(context).pop(null), child: const Text('Cancel'), ), FilledButton( - onPressed: _submit, - child: const Text('Submit'), + onPressed: _loading ? null : _submit, + child: _loading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Submit'), ), ], ); diff --git a/lib/screens/programmer_tasks/day_sheets/day_sheets_tab.dart b/lib/screens/programmer_tasks/day_sheets/day_sheets_tab.dart index 0bf2b4e9..49d2c12a 100644 --- a/lib/screens/programmer_tasks/day_sheets/day_sheets_tab.dart +++ b/lib/screens/programmer_tasks/day_sheets/day_sheets_tab.dart @@ -43,6 +43,26 @@ class _DaySheetsTabState extends ConsumerState { // --------------------------------------------------------------------------- Future _submit(DateTime today) async { + final ok = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text("Submit today's work?"), + content: const Text( + "This will send your day sheet to admin for review."), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.of(ctx).pop(true), + child: const Text('Submit'), + ), + ], + ), + ); + if (ok != true) return; + setState(() => _submitting = true); try { await ref.read(daySheetsControllerProvider).submit(today);