261 lines
9.1 KiB
Dart
261 lines
9.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../api/KasbonApi.dart';
|
|
|
|
const _bg = Color(0xFFF9FAFB);
|
|
const _bg1 = Color(0xFFFFFFFF);
|
|
const _bg2 = Color(0xFFF3F4F6);
|
|
const _green = Color(0xFF10B981);
|
|
const _greenDim = Color(0x1A10B981);
|
|
const _cyan = Color(0xFF06B6D4);
|
|
const _cyanDim = Color(0x1A06B6D4);
|
|
const _amber = Color(0xFFF59E0B);
|
|
const _amberDim = Color(0x1AF59E0B);
|
|
const _rose = Color(0xFFEF4444);
|
|
const _roseDim = Color(0x1AEF4444);
|
|
const _t1 = Color(0xFF111827);
|
|
const _t2 = Color(0xFF6B7280);
|
|
const _t3 = Color(0xFF9CA3AF);
|
|
const _line2 = Color(0xFFE5E7EB);
|
|
|
|
class KasbonRiwayatScreen extends StatefulWidget {
|
|
const KasbonRiwayatScreen({super.key});
|
|
|
|
@override
|
|
State<KasbonRiwayatScreen> createState() => _KasbonRiwayatScreenState();
|
|
}
|
|
|
|
class _KasbonRiwayatScreenState extends State<KasbonRiwayatScreen> {
|
|
final KasbonApi _apiService = KasbonApi();
|
|
List<dynamic> _riwayat = [];
|
|
Map<String, dynamic>? _statistik;
|
|
bool _isLoading = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_fetchData();
|
|
}
|
|
|
|
Future<void> _fetchData() async {
|
|
setState(() => _isLoading = true);
|
|
try {
|
|
final results = await Future.wait([
|
|
_apiService.getRiwayat(),
|
|
_fetchStatistik(),
|
|
]);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
final resRiwayat = results[0] as Map<String, dynamic>?;
|
|
final resStat = results[1] as Map<String, dynamic>?;
|
|
|
|
if (resRiwayat != null && resRiwayat['success'] == true) {
|
|
// Laravel paginate structure
|
|
final rawData = resRiwayat['data'];
|
|
if (rawData is Map && rawData.containsKey('data')) {
|
|
_riwayat = rawData['data'] ?? [];
|
|
} else {
|
|
_riwayat = rawData ?? [];
|
|
}
|
|
}
|
|
if (resStat != null) {
|
|
_statistik = resStat;
|
|
}
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
|
|
Future<Map<String, dynamic>?> _fetchStatistik() async {
|
|
// API Route: /kasbon/statistik
|
|
try {
|
|
final response = await _apiService.getStatistik(); // Use the new method
|
|
return response['data'];
|
|
} catch (e) { return null; }
|
|
}
|
|
|
|
String _formatCurrency(dynamic amount) {
|
|
final formatter = NumberFormat.currency(locale: 'id_ID', symbol: 'Rp ', decimalDigits: 0);
|
|
return formatter.format(amount ?? 0);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return _isLoading
|
|
? const Center(child: CircularProgressIndicator(color: _rose))
|
|
: RefreshIndicator(
|
|
onRefresh: _fetchData,
|
|
color: _rose,
|
|
backgroundColor: _bg1,
|
|
child: CustomScrollView(
|
|
slivers: [
|
|
// STATS SECTION
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('RINGKASAN KASBON',
|
|
style: TextStyle(color: _t3, fontSize: 10, fontWeight: FontWeight.w900, letterSpacing: 1.5)),
|
|
const SizedBox(height: 12),
|
|
_buildTotalKasbonCard(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// LIST SECTION
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text('RIWAYAT PINJAMAN',
|
|
style: TextStyle(color: _t3, fontSize: 10, fontWeight: FontWeight.w900, letterSpacing: 1.5)),
|
|
Text('${_riwayat.length} Transaksi',
|
|
style: const TextStyle(color: _t2, fontSize: 10)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
const SliverPadding(padding: EdgeInsets.only(top: 12)),
|
|
|
|
_riwayat.isEmpty
|
|
? SliverToBoxAdapter(child: _buildEmptyState())
|
|
: SliverList(
|
|
delegate: SliverChildBuilderDelegate(
|
|
(context, i) => Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: _buildKasbonCard(_riwayat[i]),
|
|
),
|
|
childCount: _riwayat.length,
|
|
),
|
|
),
|
|
|
|
const SliverPadding(padding: EdgeInsets.only(bottom: 32)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTotalKasbonCard() {
|
|
final outstanding = _statistik?['total_hutang'] ?? _statistik?['outstanding_kasbon'] ?? 0;
|
|
return Container(
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [_rose, _rose.withOpacity(0.7)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(color: _rose.withOpacity(0.3), blurRadius: 15, offset: const Offset(0, 8)),
|
|
],
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('Sisa Hutang Kasbon',
|
|
style: TextStyle(color: Colors.white70, fontSize: 12, fontWeight: FontWeight.w500)),
|
|
const SizedBox(height: 6),
|
|
Text(_formatCurrency(outstanding),
|
|
style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w900, letterSpacing: -0.5)),
|
|
],
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: const Icon(Icons.money_off_rounded, color: Colors.white, size: 28),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEmptyState() {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 40),
|
|
child: Center(
|
|
child: Column(
|
|
children: [
|
|
Icon(Icons.history_rounded, size: 48, color: _t3),
|
|
const SizedBox(height: 12),
|
|
const Text('Belum ada riwayat kasbon', style: TextStyle(color: _t2, fontSize: 13)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildKasbonCard(dynamic item) {
|
|
final bool isLunas = item['status'] == 'Lunas';
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: _bg1,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: _line2),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 44, height: 44,
|
|
decoration: BoxDecoration(
|
|
color: isLunas ? _greenDim : _roseDim,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: (isLunas ? _green : _rose).withOpacity(0.3)),
|
|
),
|
|
child: Icon(isLunas ? Icons.check_circle_rounded : Icons.pending_rounded,
|
|
color: isLunas ? _green : _rose, size: 20),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(_formatCurrency(item['nominal']),
|
|
style: const TextStyle(color: _t1, fontWeight: FontWeight.w800, fontSize: 15)),
|
|
const SizedBox(height: 4),
|
|
Text(item['keperluan'] ?? 'Tanpa keterangan',
|
|
style: const TextStyle(color: _t2, fontSize: 12), maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
const SizedBox(height: 4),
|
|
Text(item['tanggal'] ?? '-',
|
|
style: const TextStyle(color: _t3, fontSize: 10)),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: isLunas ? _greenDim : _roseDim,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: (isLunas ? _green : _rose).withOpacity(0.25)),
|
|
),
|
|
child: Text(item['status_label']?.toUpperCase() ?? '-',
|
|
style: TextStyle(
|
|
color: isLunas ? _green : _rose,
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 0.5)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|