Files
tasq/lib/screens/programmer_tasks/day_sheets/day_sheet_dialogs.dart
T
2026-09-28 14:21:22 +08:00

220 lines
6.1 KiB
Dart

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<DaySheetRow> rowsForSheet(
ProgrammerDaySheet sheet,
List<DaySheetRow> 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.
///
/// Returns the non-empty remarks string on confirm, or `null` on cancel.
Future<String?> showDisapproveDialog(
BuildContext context, {
required List<DaySheetRow> flaggedRows,
}) {
return showDialog<String>(
context: context,
barrierDismissible: false,
builder: (ctx) => _DisapproveDialog(flaggedRows: flaggedRows),
);
}
class _DisapproveDialog extends StatefulWidget {
const _DisapproveDialog({required this.flaggedRows});
final List<DaySheetRow> flaggedRows;
@override
State<_DisapproveDialog> createState() => _DisapproveDialogState();
}
class _DisapproveDialogState extends State<_DisapproveDialog> {
final _remarksCtrl = TextEditingController();
String? _error;
late Set<String> _selected;
@override
void initState() {
super.initState();
_selected = {for (final r in widget.flaggedRows) r.taskId};
}
@override
void dispose() {
_remarksCtrl.dispose();
super.dispose();
}
void _submit() {
final remarks = _remarksCtrl.text.trim();
if (remarks.isEmpty) {
setState(() => _error = 'Remarks are required');
return;
}
Navigator.of(context).pop(remarks);
}
@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: () => Navigator.of(context).pop(null),
child: const Text('Cancel'),
),
FilledButton(
onPressed: _submit,
child: const Text('Disapprove'),
),
],
);
}
}
// ---------------------------------------------------------------------------
// showJustifyDialog
// ---------------------------------------------------------------------------
/// 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>(
context: context,
barrierDismissible: false,
builder: (ctx) => const _JustifyDialog(),
);
}
class _JustifyDialog extends StatefulWidget {
const _JustifyDialog();
@override
State<_JustifyDialog> createState() => _JustifyDialogState();
}
class _JustifyDialogState extends State<_JustifyDialog> {
final _bodyCtrl = TextEditingController();
String? _error;
@override
void dispose() {
_bodyCtrl.dispose();
super.dispose();
}
void _submit() {
final body = _bodyCtrl.text.trim();
if (body.isEmpty) {
setState(() => _error = 'Please write a justification');
return;
}
Navigator.of(context).pop(body);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Justify & Resubmit'),
content: SizedBox(
width: 360,
child: TextField(
controller: _bodyCtrl,
maxLines: 4,
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: () => Navigator.of(context).pop(null),
child: const Text('Cancel'),
),
FilledButton(
onPressed: _submit,
child: const Text('Submit'),
),
],
);
}
}