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 '../../core/theme/app_theme.dart'; import '../../core/widgets/status_badge.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/error_view.dart'; import '../../shared/models/contract.dart'; import 'contracts_provider.dart'; class ContractListScreen extends ConsumerStatefulWidget { const ContractListScreen({super.key}); @override ConsumerState createState() => _ContractListScreenState(); } class _ContractListScreenState extends ConsumerState { String _filter = 'all'; final _searchCtrl = TextEditingController(); @override void initState() { super.initState(); _searchCtrl.addListener(_reload); } @override void dispose() { _searchCtrl.removeListener(_reload); _searchCtrl.dispose(); super.dispose(); } void _reload() { ref.read(contractListProvider.notifier).load(filter: _filter, search: _searchCtrl.text); } void _setFilter(String f) { setState(() => _filter = f); ref.read(contractListProvider.notifier).load(filter: f, search: _searchCtrl.text); } @override Widget build(BuildContext context) { final state = ref.watch(contractListProvider); return Scaffold( appBar: AppBar(title: const Text('Contracts')), body: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), child: TextField( controller: _searchCtrl, decoration: const InputDecoration( hintText: 'Search contracts...', prefixIcon: Icon(Icons.search, color: AppColors.textSub), ), ), ), SizedBox( height: 40, child: ListView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), children: [ for (final (val, label) in [('all', 'All'), ('active', 'Active'), ('upcoming', 'Upcoming'), ('expired', 'Expired')]) Padding( padding: const EdgeInsets.only(right: 8), child: ChoiceChip( label: Text(label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _filter == val ? Colors.white : AppColors.textSub, )), selected: _filter == val, selectedColor: AppColors.primary, backgroundColor: AppColors.surface, side: BorderSide(color: _filter == val ? AppColors.primary : AppColors.border), onSelected: (_) => _setFilter(val), ), ), ], ), ), const SizedBox(height: 8), Expanded( child: state.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => ErrorView( message: e.toString(), onRetry: () => ref.read(contractListProvider.notifier).load(), ), data: (paged) { final expiring = paged.items.where((c) => c.isExpiringSoon && c.status == 'Active').toList(); return RefreshIndicator( color: AppColors.primary, onRefresh: () async => ref.read(contractListProvider.notifier).load( filter: _filter, search: _searchCtrl.text), child: CustomScrollView( slivers: [ if (expiring.isNotEmpty) SliverToBoxAdapter( child: Container( margin: const EdgeInsets.fromLTRB(16, 4, 16, 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.warningLight, borderRadius: BorderRadius.circular(10), border: Border.all(color: AppColors.warning.withOpacity(0.5)), ), child: Row(children: [ const Icon(Icons.warning_amber_rounded, color: AppColors.warning, size: 18), const SizedBox(width: 8), Text('${expiring.length} contract(s) expiring soon (≤90 days)', style: const TextStyle(fontSize: 13, color: Color(0xFFB45309), fontWeight: FontWeight.w600)), ]), ), ), if (paged.items.isEmpty) SliverFillRemaining(child: EmptyState( message: 'No contracts', icon: Icons.description_outlined, actionLabel: 'Upload Contract', onAction: () => context.push('/contracts/upload'), )) else SliverPadding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), sliver: SliverList( delegate: SliverChildBuilderDelegate( (_, i) => Padding( padding: const EdgeInsets.only(bottom: 8), child: _ContractCard(contract: paged.items[i]), ), childCount: paged.items.length, ), ), ), ], ), ); }, ), ), ], ), floatingActionButton: FloatingActionButton( onPressed: () async { await context.push('/contracts/upload'); if (mounted) ref.read(contractListProvider.notifier).load(filter: _filter); }, child: const Icon(Icons.add), ), ); } } class _ContractCard extends StatelessWidget { final Contract contract; const _ContractCard({required this.contract}); @override Widget build(BuildContext context) { return GestureDetector( onTap: () => context.push('/contracts/${contract.id}'), child: Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(12), border: Border.all(color: contract.isExpiringSoon ? AppColors.warningLight : AppColors.border), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Expanded(child: Text(contract.title, style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14, color: AppColors.text), maxLines: 2, overflow: TextOverflow.ellipsis)), const SizedBox(width: 8), StatusBadge(contract.status, small: true), ]), const SizedBox(height: 8), if (contract.periodStart != null && contract.periodEnd != null) Text( '${DateFormat('d MMM yyyy', 'id').format(contract.periodStart!)} — ${DateFormat('d MMM yyyy', 'id').format(contract.periodEnd!)}', style: const TextStyle(fontSize: 12, color: AppColors.textSub), ), if (contract.companyName != null || contract.vendorName != null) ...[ const SizedBox(height: 4), Row(children: [ if (contract.companyName != null) ...[ const Icon(Icons.business_outlined, size: 11, color: AppColors.textSub), const SizedBox(width: 3), Text(contract.companyName!, style: const TextStyle(fontSize: 11, color: AppColors.textSub)), ], if (contract.companyName != null && contract.vendorName != null) const Text(' · ', style: TextStyle(fontSize: 11, color: AppColors.textSub)), if (contract.vendorName != null) ...[ const Icon(Icons.local_shipping_outlined, size: 11, color: AppColors.textSub), const SizedBox(width: 3), Text(contract.vendorName!, style: const TextStyle(fontSize: 11, color: AppColors.textSub)), ], ]), ], if (contract.isExpiringSoon && contract.status == 'Active') ...[ const SizedBox(height: 6), Row(children: [ const Icon(Icons.warning_amber_rounded, size: 13, color: AppColors.warning), const SizedBox(width: 4), Text('Expires in ${contract.daysUntilExpiry} days', style: const TextStyle(fontSize: 11, color: AppColors.warning, fontWeight: FontWeight.w600)), ]), ], if (contract.status == 'Expired' && contract.successors.isNotEmpty) ...[ const SizedBox(height: 6), Row(children: [ const Icon(Icons.check_circle_outline, size: 13, color: AppColors.primary), const SizedBox(width: 4), const Text('Digantikan', style: TextStyle(fontSize: 11, color: AppColors.primary, fontWeight: FontWeight.w600)), ]), ], ], ), ), ); } }