import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../config/app_config.dart'; import '../../models/document_model.dart'; import '../../main.dart'; import 'pending_documents_provider.dart'; import '../editor/pdf_service.dart'; import '../../core/network/api_client.dart'; import 'pending_document_card.dart'; import 'document_card.dart'; import '../camera/scan_mode_provider.dart'; import 'documents_tab_switcher.dart'; import 'documents_empty_state.dart'; import 'document_sync_merge.dart'; class DocumentsScreen extends ConsumerStatefulWidget { const DocumentsScreen({super.key}); @override ConsumerState createState() => _DocumentsScreenState(); } class _DocumentsScreenState extends ConsumerState { final _searchController = TextEditingController(); List _allDocuments = []; List _filteredDocuments = []; Set _unsyncedDocIds = {}; // _selectedTab removed in task 8.1 -- scanModeProvider is the single source // of truth for the active tab, shared with CameraDrawer / CameraDrawerModeToggle. // Caching it locally (and seeding from the provider in initState) caused mode // desync when the camera drawer changed the mode after this screen was open. // See docs/api-contract-map.md G11, plans/next-enhancements.md §8.1. @override void initState() { super.initState(); _loadDocuments(); _searchController.addListener(_onSearchChanged); } Future _loadDocuments() async { final localStorage = ref.read(localStorageProvider); final docs = localStorage.getAllDocuments(); setState(() { _allDocuments = docs; _filteredDocuments = docs; }); try { final apiClient = ref.read(apiClientProvider); final response = await apiClient.client.get(AppConfig.fetchDocumentsEndpoint); final List serverData = response.data['data'] ?? []; final List serverDocs = serverData .map((json) => DocumentModel.fromJson(Map.from(json))) .toList(); // A document whose editor save PUT failed (syncFailed) still has its // corrected version saved locally - don't let the server's stale // pre-edit copy silently overwrite it on refresh (see // docs/api-contract-map.md G6). final unsyncedOverrides = { for (final pending in ref.read(pendingDocumentsProvider)) if (pending.status == PendingDocumentStatus.syncFailed && pending.document != null) pending.document!.id: pending.document!, }; final merged = mergeDocumentsWithUnsyncedOverrides( serverDocs: serverDocs, unsyncedOverrides: unsyncedOverrides, ); await localStorage.documentBox.clear(); for (var doc in merged.docs) { await localStorage.saveDocument(doc); } final updatedDocs = localStorage.getAllDocuments(); if (mounted) { setState(() { _allDocuments = updatedDocs; _filteredDocuments = updatedDocs; _unsyncedDocIds = merged.unsyncedIds; }); } } catch (e) { debugPrint('Offline or failed fetching: $e'); } } void _onSearchChanged() { final query = _searchController.text.toLowerCase(); setState(() { _filteredDocuments = _allDocuments.where((doc) { return doc.noDo.toLowerCase().contains(query) || doc.noPo.toString().contains(query) || doc.noSo.toString().contains(query) || doc.tanggal.toLowerCase().contains(query) || doc.kepadaYth.toLowerCase().contains(query); }).toList(); }); } @override void dispose() { _searchController.dispose(); super.dispose(); } Future _printReceipt(DocumentModel doc) async { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Membuat PDF...'), duration: Duration(seconds: 1), backgroundColor: AppConfig.secondaryColor), ); await PdfService.generateAndPrintReceipt(doc); } @override Widget build(BuildContext context) { final pendingDocs = ref.watch(pendingDocumentsProvider); // Single source of truth for the active tab -- shared with CameraDrawer // via scanModeProvider (task 8.1, docs/api-contract-map.md G11). final selectedTab = ref.watch(scanModeProvider); final tabFilteredPendingDocs = pendingDocs.where((doc) { final isProduct = doc.scanMode == 'Product'; if (selectedTab == 'DO') return !isProduct; if (selectedTab == 'Product') return isProduct; return true; }).toList(); final tabFilteredDocs = _filteredDocuments.where((doc) { final isProduct = doc.docType == 'Product'; if (selectedTab == 'DO') return !isProduct; if (selectedTab == 'Product') return isProduct; return true; }).toList(); final Map> groupedDocs = {}; for (var doc in tabFilteredDocs) { final date = doc.createdAt; final monthYearStr = DateFormat('MMMM yyyy').format(date); if (!groupedDocs.containsKey(monthYearStr)) { groupedDocs[monthYearStr] = []; } groupedDocs[monthYearStr]!.add(doc); } final List listItems = []; if (tabFilteredPendingDocs.isNotEmpty) { listItems.add( Padding( padding: const EdgeInsets.only(bottom: AppConfig.spaceSmall), child: Text('Tertunda & Diproses', style: AppConfig.h3), ), ); listItems.addAll(tabFilteredPendingDocs.map((doc) => PendingDocumentCard( doc: doc, onReload: () { if (mounted) _loadDocuments(); }, ))); listItems.add(const SizedBox(height: AppConfig.spaceLarge)); listItems.add(const Divider()); listItems.add(const SizedBox(height: AppConfig.spaceLarge)); } if (groupedDocs.isNotEmpty) { groupedDocs.forEach((monthYear, docs) { listItems.add( Padding( padding: const EdgeInsets.only(top: AppConfig.spaceLarge, bottom: AppConfig.spaceSmall), child: Text( monthYear, style: AppConfig.bodyMedium.copyWith( fontWeight: FontWeight.bold, color: AppConfig.textSecondary, ), ), ), ); listItems.addAll(docs.map((doc) => DocumentCard( doc: doc, onPrint: _printReceipt, isUnsynced: _unsyncedDocIds.contains(doc.id), ))); }); } return Scaffold( appBar: AppBar( title: const Text('Dokumen'), leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => context.go('/camera')), ), body: Column( children: [ Container( padding: AppConfig.paddingLarge, color: AppConfig.surfaceColor, child: TextField( controller: _searchController, decoration: const InputDecoration( hintText: 'Cari berdasarkan No PO, No SO, DO, Tanggal...', prefixIcon: Icon(Icons.search), ), ), ), DocumentsTabSwitcher( selectedTab: selectedTab, onTabChanged: (tab) => ref.read(scanModeProvider.notifier).state = tab, ), Expanded( child: (tabFilteredPendingDocs.isEmpty && tabFilteredDocs.isEmpty) ? const DocumentsEmptyState() : ListView( padding: const EdgeInsets.symmetric(horizontal: AppConfig.spaceLarge), children: listItems, ), ), ], ), ); } }