feat(programmer-tasks): loading states, confirmation & success dialogs
Day sheet dialogs (disapprove, justify): moved the async controller call inside each dialog so loading state is owned there — button shows a spinner and is disabled while the RPC is in flight; errors display inline instead of via a snackbar after dialog close. Detail screen (approve): added _approvingSheet bool; Approve button shows CircularProgressIndicator while in flight. On success shows an AlertDialog "Sheet Approved" with a check icon instead of silently updating state. Approvals tab (bulk approve): upgraded post-approve feedback from a snackbar to a success AlertDialog showing the count of approved sheets and any skipped ones. Day sheets tab (submit): added confirmation AlertDialog before the existing submit flow so programmers don't accidentally submit mid-day. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -21,24 +21,30 @@ List<DaySheetRow> 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<String?> 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<bool?> showDisapproveDialog(
|
||||
BuildContext context, {
|
||||
required List<DaySheetRow> flaggedRows,
|
||||
required Future<void> Function(
|
||||
String remarks, List<String> flaggedTaskIds)
|
||||
onConfirm,
|
||||
}) {
|
||||
return showDialog<String>(
|
||||
return showDialog<bool>(
|
||||
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<DaySheetRow> flaggedRows;
|
||||
final Future<void> Function(String remarks, List<String> 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<String> _selected;
|
||||
|
||||
@override
|
||||
@@ -61,13 +68,19 @@ class _DisapproveDialogState extends State<_DisapproveDialog> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
Future<void> _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<String?> showJustifyDialog(BuildContext context) {
|
||||
return showDialog<String>(
|
||||
/// resubmitting. Executes [onConfirm] while showing a loading state; handles
|
||||
/// errors inline. Returns true if the action completed, or null if cancelled.
|
||||
Future<bool?> showJustifyDialog(
|
||||
BuildContext context, {
|
||||
required Future<void> Function(String body) onConfirm,
|
||||
}) {
|
||||
return showDialog<bool>(
|
||||
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<void> 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<void> _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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user