Files
pfm-ocr/lib/features/documents/documents_screen.dart
T

299 lines
9.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.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';
class DocumentsScreen extends ConsumerStatefulWidget {
const DocumentsScreen({super.key});
@override
ConsumerState<DocumentsScreen> createState() => _DocumentsScreenState();
}
class _DocumentsScreenState extends ConsumerState<DocumentsScreen> {
final _searchController = TextEditingController();
List<DocumentModel> _allDocuments = [];
List<DocumentModel> _filteredDocuments = [];
String _selectedTab = 'DO'; // 'DO' | 'Product'
@override
void initState() {
super.initState();
_loadDocuments();
_searchController.addListener(_onSearchChanged);
}
Future<void> _loadDocuments() async {
final localStorage = ref.read(localStorageProvider);
var docs = localStorage.getAllDocuments();
final prefs = await SharedPreferences.getInstance();
final currentStoreName = prefs.getString('nama_toko') ?? 'PM KELAPA DUA KARAWACI';
bool databaseUpdated = false;
final hasProductScan = docs.any((doc) => doc.orderUntuk == 'PRODUCT SCAN' && doc.kepadaYth == currentStoreName);
if (!hasProductScan) {
final mockProdDoc = DocumentModel(
id: 'MOCK-PROD-SCAN-1',
tanggal: DateFormat('dd MMMM yyyy').format(DateTime.now().subtract(const Duration(days: 1))),
noPo: 'PO-PROD-12010119',
noSo: '1002003004',
noDo: 'Batch 1',
kepadaYth: currentStoreName,
orderUntuk: 'PRODUCT SCAN',
alamat: 'Product Scan: Expired 15/12/2026',
platTruk: 'B 1234 PFM',
namaDriver: 'PRODUCT SCAN',
namaPenerima: 'STORE STAFF',
items: [
DocumentItem(
nomorSku: '12010119',
namaBarang: 'FIESTA SPICY CHICKEN NUGGET 500 GR',
banyak: '1',
jumlah: '1',
)
],
createdAt: DateTime.now().subtract(const Duration(days: 1)),
);
await localStorage.saveDocument(mockProdDoc);
databaseUpdated = true;
}
final hasDoScan = docs.any((doc) => doc.orderUntuk != 'PRODUCT SCAN' && doc.kepadaYth == currentStoreName);
if (!hasDoScan) {
final mockDoDoc = DocumentModel(
id: 'MOCK-DO-SCAN-1',
tanggal: DateFormat('dd MMMM yyyy').format(DateTime.now().subtract(const Duration(days: 2))),
noPo: 'PO-9923488',
noSo: '1002008892',
noDo: 'DO-9988223',
kepadaYth: currentStoreName,
orderUntuk: 'DELIVERY ORDER',
alamat: 'Jl. Boulevard Raya No. 12, Karawaci',
platTruk: 'B 9876 PFM',
namaDriver: 'Budi Santoso',
namaPenerima: 'Staff Toko',
items: [
DocumentItem(
nomorSku: '12010119',
namaBarang: 'FIESTA SPICY CHICKEN NUGGET 500 GR',
banyak: '10',
jumlah: '10',
)
],
createdAt: DateTime.now().subtract(const Duration(days: 2)),
);
await localStorage.saveDocument(mockDoDoc);
databaseUpdated = true;
}
if (databaseUpdated) {
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<DocumentModel> serverDocs = serverData
.map((json) => DocumentModel.fromJson(Map<String, dynamic>.from(json)))
.toList();
for (var doc in serverDocs) {
await localStorage.saveDocument(doc);
}
final updatedDocs = localStorage.getAllDocuments();
if (mounted) {
setState(() {
_allDocuments = updatedDocs;
_filteredDocuments = updatedDocs;
});
}
} 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<void> _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);
final tabFilteredDocs = _filteredDocuments.where((doc) {
final isProduct = doc.orderUntuk == 'PRODUCT SCAN';
if (_selectedTab == 'DO') return !isProduct;
if (_selectedTab == 'Product') return isProduct;
return true;
}).toList();
final Map<String, List<DocumentModel>> 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<Widget> listItems = [];
if (pendingDocs.isNotEmpty) {
listItems.add(
Padding(
padding: const EdgeInsets.only(bottom: AppConfig.spaceSmall),
child: Text('Tertunda & Diproses', style: AppConfig.h3),
),
);
listItems.addAll(pendingDocs.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,
)));
});
}
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),
),
),
),
_buildTabSwitcher(),
Expanded(
child: (pendingDocs.isEmpty && tabFilteredDocs.isEmpty)
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.folder_open, size: 64, color: AppConfig.textSecondary.withOpacity(0.5)),
const SizedBox(height: AppConfig.spaceLarge),
Text('Tidak ada dokumen ditemukan', style: AppConfig.bodyLarge),
],
),
)
: ListView(
padding: const EdgeInsets.symmetric(horizontal: AppConfig.spaceLarge),
children: listItems,
),
),
],
),
);
}
Widget _buildTabSwitcher() {
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppConfig.spaceLarge, vertical: AppConfig.spaceSmall),
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(AppConfig.borderRadiusMedium)),
child: Row(
children: [
_buildTabItem('DO', 'DO Scan'),
_buildTabItem('Product', 'Product Scan'),
],
),
);
}
Widget _buildTabItem(String value, String label) {
final isActive = _selectedTab == value;
return Expanded(
child: GestureDetector(
onTap: () => setState(() { _selectedTab = value; }),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: isActive ? Colors.white : Colors.transparent,
borderRadius: BorderRadius.circular(AppConfig.borderRadiusSmall),
boxShadow: isActive ? [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2))] : null,
),
child: Center(
child: Text(
label,
style: TextStyle(
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
color: isActive ? AppConfig.primaryColor : AppConfig.textSecondary,
),
),
),
),
),
);
}
}