import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../../models/profile.model.dart'; import '../../../models/programmer_day_sheet.model.dart'; import '../../../providers/profile_provider.dart'; import '../../../providers/programmer_day_sheets_provider.dart'; import '../../../providers/reports_provider.dart'; import '../../../theme/app_status_colors.dart'; import '../../../utils/app_time.dart'; import '../../../utils/programmer_task_ledger.dart'; import '../../../widgets/app_state_view.dart'; import '../../../widgets/app_status_summary.dart'; import '../../../widgets/tech_chip.dart'; import '../../reports/report_date_filter.dart'; import 'day_sheet_snapshot_builder.dart'; import '../report/accomplishment_report_dialog.dart'; /// Admin tab for reviewing and bulk-approving programmer day sheets. /// /// Features: /// * Status filter chips: Pending (default) / Awaiting justification / Approved. /// * Programmer dropdown: All programmers or a specific person. /// * Controlled date range filter (default: this month). /// * Cards grouped by work-date; pending cards have selection checkboxes. /// * "Approve selected (n)" button that calls [buildSnapshotForSheet] for each /// selected sheet before calling the bulk-approve RPC. class ApprovalsTab extends ConsumerStatefulWidget { const ApprovalsTab({super.key}); @override ConsumerState createState() => _ApprovalsTabState(); } class _ApprovalsTabState extends ConsumerState { /// Active status filter — one of the [DaySheetStatus] constants. String _filter = DaySheetStatus.pending; /// Selected programmer ID, or null for "all programmers". String? _programmerFilter; late ReportDateRange _dateRange; /// IDs of pending sheets selected for bulk approval. final Set _selected = {}; bool _approving = false; @override void initState() { super.initState(); _dateRange = thisMonthRange(AppTime.now()); } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- bool _inDateRange(DateTime workDate) => !workDate.isBefore(_dateRange.start) && workDate.isBefore(_dateRange.end); // --------------------------------------------------------------------------- // Actions // --------------------------------------------------------------------------- Future _onApprove(List allSheets) async { setState(() => _approving = true); try { final toApprove = allSheets .where((s) => _selected.contains(s.id) && s.status == DaySheetStatus.pending) .toList(); final items = <({String sheetId, DaySheetSnapshot snapshot})>[]; for (final sheet in toApprove) { final snapshot = await buildSnapshotForSheet(ref, sheet); items.add((sheetId: sheet.id, snapshot: snapshot)); } final result = await ref.read(daySheetsControllerProvider).approve(items); if (!mounted) return; setState(() { _selected.clear(); _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()))); } on DaySheetActionException catch (e) { if (!mounted) return; setState(() => _approving = false); ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(e.message))); } catch (e) { if (!mounted) return; setState(() => _approving = false); ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(e.toString()))); } } // --------------------------------------------------------------------------- // Build // --------------------------------------------------------------------------- @override Widget build(BuildContext context) { final allSheets = ref.watch(allDaySheetsProvider).valueOrNull ?? const []; final profiles = ref.watch(profilesProvider).valueOrNull ?? const []; final profileById = {for (final p in profiles) p.id: p}; final programmerProfiles = profiles.where((p) => p.role == 'programmer').toList() ..sort((a, b) => a.fullName.compareTo(b.fullName)); // Sheets passing the date + programmer filters (used for chip counts). final dateAndProgFiltered = allSheets.where((s) { return _inDateRange(s.workDate) && (_programmerFilter == null || s.programmerId == _programmerFilter); }).toList(); final counts = approvalCounts(dateAndProgFiltered); // Sheets also matching the status filter (displayed in the list). final filtered = dateAndProgFiltered.where((s) => s.status == _filter).toList(); final grouped = groupSheetsByDate(filtered); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ── Status filter chips ──────────────────────────────────────────── Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: AppStatusSummaryRow( items: [ AppStatusSummaryItem( label: 'Pending', count: counts.pending, kind: StatusKind.info, selected: _filter == DaySheetStatus.pending, onTap: () => setState(() { _filter = DaySheetStatus.pending; _selected.clear(); }), ), AppStatusSummaryItem( label: 'Awaiting justification', count: counts.awaiting, kind: StatusKind.warning, selected: _filter == DaySheetStatus.disapproved, onTap: () => setState(() { _filter = DaySheetStatus.disapproved; _selected.clear(); }), ), AppStatusSummaryItem( label: 'Approved', count: counts.approved, kind: StatusKind.success, selected: _filter == DaySheetStatus.approved, onTap: () => setState(() { _filter = DaySheetStatus.approved; _selected.clear(); }), ), ], ), ), const SizedBox(height: 8), // ── Programmer dropdown + date filter ────────────────────────────── Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ DropdownButton( value: _programmerFilter, hint: const Text('All programmers'), underline: const SizedBox.shrink(), items: [ const DropdownMenuItem( value: null, child: Text('All programmers'), ), for (final p in programmerProfiles) DropdownMenuItem( value: p.id, child: Text(p.fullName), ), ], onChanged: (v) => setState(() { _programmerFilter = v; _selected.clear(); }), ), const SizedBox(width: 8), Expanded( child: ReportDateFilter.controlled( value: _dateRange, onChanged: (r) => setState(() { _dateRange = r; _selected.clear(); }), ), ), const SizedBox(width: 4), IconButton( icon: const Icon(Icons.description_outlined), tooltip: 'Accomplishment Report', onPressed: () => showAccomplishmentReportDialog( context, ref, programmerId: _programmerFilter, ), ), ], ), ), const SizedBox(height: 8), // ── Card list ────────────────────────────────────────────────────── Expanded( child: filtered.isEmpty ? AppEmptyView( icon: Icons.check_circle_outline, title: _filter == DaySheetStatus.pending ? 'No pending sheets' : 'Nothing here', subtitle: 'All caught up!', ) : ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), itemCount: grouped.length, itemBuilder: (context, groupIdx) { final dateKey = grouped.keys.elementAt(groupIdx); final groupSheets = grouped[dateKey]!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(top: 8, bottom: 4), child: Text( AppTime.formatDate(dateKey), style: Theme.of(context) .textTheme .labelMedium ?.copyWith( color: Theme.of(context) .colorScheme .onSurfaceVariant, ), ), ), for (final sheet in groupSheets) _SheetApprovalCard( sheet: sheet, profile: profileById[sheet.programmerId], showCheckbox: _filter == DaySheetStatus.pending, selected: _selected.contains(sheet.id), totalSecs: sheet.approvedSnapshot?.totalSeconds, taskCount: sheet.approvedSnapshot?.rows.length, onToggle: _filter == DaySheetStatus.pending ? (v) => setState(() { if (v == true) { _selected.add(sheet.id); } else { _selected.remove(sheet.id); } }) : null, onTap: () => context.pushNamed( 'daySheetDetail', pathParameters: {'id': sheet.id}, ), ), ], ); }, ), ), // ── Approve button ───────────────────────────────────────────────── if (_selected.isNotEmpty) SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: FilledButton( onPressed: _approving ? null : () => _onApprove(allSheets), child: _approving ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : Text('Approve selected (${_selected.length})'), ), ), ), ], ); } } // --------------------------------------------------------------------------- // Card widget // --------------------------------------------------------------------------- class _SheetApprovalCard extends StatelessWidget { const _SheetApprovalCard({ required this.sheet, required this.profile, required this.showCheckbox, required this.selected, required this.onToggle, required this.onTap, this.totalSecs, this.taskCount, }); final ProgrammerDaySheet sheet; final Profile? profile; final bool showCheckbox; final bool selected; final ValueChanged? onToggle; final VoidCallback onTap; /// Null when no approved snapshot exists yet (pending/disapproved sheets). final int? totalSecs; final int? taskCount; @override Widget build(BuildContext context) { final roundNum = sheet.resubmissions + 1; return Card( margin: const EdgeInsets.only(bottom: 8), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ if (showCheckbox && onToggle != null) ...[ Checkbox(value: selected, onChanged: onToggle), const SizedBox(width: 4), ], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( profile?.fullName ?? sheet.programmerId, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 6), Wrap( spacing: 6, runSpacing: 4, children: [ TechChip( totalSecs != null ? formatDurationHm(Duration(seconds: totalSecs!)) : '—', ), TechChip( taskCount != null ? '$taskCount tasks' : '—', ), if (sheet.autoSubmitted) const TechChip('Auto', status: StatusKind.info) else const TechChip('Early', status: StatusKind.neutral), if (roundNum > 1) TechChip( 'Round $roundNum', status: StatusKind.warning, ), ], ), ], ), ), Icon( Icons.chevron_right, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ], ), ), ), ); } }