337 lines
12 KiB
Dart
337 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../api/GajiApi.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 GajiRiwayatScreen extends StatefulWidget {
|
|
const GajiRiwayatScreen({super.key});
|
|
|
|
@override
|
|
State<GajiRiwayatScreen> createState() => _GajiRiwayatScreenState();
|
|
}
|
|
|
|
class _GajiRiwayatScreenState extends State<GajiRiwayatScreen> {
|
|
final GajiApi _apiService = GajiApi();
|
|
List<dynamic> _riwayatGaji = [];
|
|
bool _isLoading = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_fetchRiwayat();
|
|
}
|
|
|
|
Future<void> _fetchRiwayat() async {
|
|
setState(() => _isLoading = true);
|
|
final res = await _apiService.getRiwayat();
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
if (res['success'] == true) {
|
|
// Laravel paginate structure is res['data']['data']
|
|
final rawData = res['data'];
|
|
if (rawData is Map && rawData.containsKey('data')) {
|
|
_riwayatGaji = rawData['data'] ?? [];
|
|
} else {
|
|
_riwayatGaji = rawData ?? [];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
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: _amber))
|
|
: RefreshIndicator(
|
|
onRefresh: _fetchRiwayat,
|
|
color: _amber,
|
|
backgroundColor: _bg1,
|
|
child: _riwayatGaji.isEmpty
|
|
? _buildEmptyState()
|
|
: ListView.builder(
|
|
padding: const EdgeInsets.all(20),
|
|
itemCount: _riwayatGaji.length,
|
|
itemBuilder: (context, i) => _buildGajiCard(_riwayatGaji[i]),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEmptyState() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: _bg1,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: _line2),
|
|
),
|
|
child: const Icon(Icons.receipt_long_rounded, size: 40, color: _t3),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text('Belum Ada Riwayat',
|
|
style: TextStyle(color: _t1, fontSize: 16, fontWeight: FontWeight.w700)),
|
|
const SizedBox(height: 4),
|
|
const Text('Slip gaji Anda akan muncul di sini',
|
|
style: TextStyle(color: _t2, fontSize: 13)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildGajiCard(dynamic item) {
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 16),
|
|
decoration: BoxDecoration(
|
|
color: _bg1,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: _line2),
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: () => _showGajiDetail(item['id_penggajian']),
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 48, height: 48,
|
|
decoration: BoxDecoration(
|
|
color: _amberDim,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: _amber.withOpacity(0.3)),
|
|
),
|
|
child: const Icon(Icons.receipt_long_rounded, color: _amber, size: 22),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Periode: ${item['nama_bulan'] ?? ''} ${item['periode_tahun'] ?? ''}',
|
|
style: const TextStyle(
|
|
color: _t1,
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 14)),
|
|
const SizedBox(height: 4),
|
|
Text('Dibayar: ${item['tanggal_bayar'] ?? '-'}',
|
|
style: const TextStyle(color: _t2, fontSize: 11)),
|
|
],
|
|
),
|
|
),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Text(_formatCurrency(item['gaji_bersih']),
|
|
style: const TextStyle(
|
|
color: _green,
|
|
fontWeight: FontWeight.w800,
|
|
fontSize: 15)),
|
|
const SizedBox(height: 4),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: (item['is_paid'] ?? false) ? _greenDim : _amberDim,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text((item['is_paid'] ?? false) ? 'LUNAS' : 'BELUM DIBAYAR',
|
|
style: TextStyle(
|
|
color: (item['is_paid'] ?? false) ? _green : _amber,
|
|
fontSize: 9,
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 0.5)),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showGajiDetail(int id) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (context) => _GajiDetailSheet(id: id, apiService: _apiService),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _GajiDetailSheet extends StatefulWidget {
|
|
final int id;
|
|
final GajiApi apiService;
|
|
const _GajiDetailSheet({required this.id, required this.apiService});
|
|
|
|
@override
|
|
State<_GajiDetailSheet> createState() => _GajiDetailSheetState();
|
|
}
|
|
|
|
class _GajiDetailSheetState extends State<_GajiDetailSheet> {
|
|
bool _isLoading = true;
|
|
dynamic _detail;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_fetchDetail();
|
|
}
|
|
|
|
Future<void> _fetchDetail() async {
|
|
final res = await widget.apiService.getDetail(widget.id);
|
|
if (mounted) {
|
|
setState(() {
|
|
_detail = res['data']?['header']; // We use 'header' for top summary
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
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 DraggableScrollableSheet(
|
|
initialChildSize: 0.8,
|
|
maxChildSize: 0.95,
|
|
minChildSize: 0.5,
|
|
builder: (_, controller) => Container(
|
|
decoration: const BoxDecoration(
|
|
color: _bg1,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
|
),
|
|
child: _isLoading
|
|
? const Center(child: CircularProgressIndicator(color: _amber))
|
|
: Column(
|
|
children: [
|
|
const SizedBox(height: 12),
|
|
Container(width: 40, height: 4, decoration: BoxDecoration(color: _line2, borderRadius: BorderRadius.circular(2))),
|
|
const SizedBox(height: 24),
|
|
Expanded(
|
|
child: ListView(
|
|
controller: controller,
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
children: [
|
|
Center(
|
|
child: Column(
|
|
children: [
|
|
const Text('SLIP GAJI TEKNISI',
|
|
style: TextStyle(color: _t2, fontSize: 11, fontWeight: FontWeight.w800, letterSpacing: 2)),
|
|
const SizedBox(height: 8),
|
|
Text(_detail['periode'] ?? '-',
|
|
style: const TextStyle(color: _t1, fontSize: 18, fontWeight: FontWeight.w800)),
|
|
const SizedBox(height: 24),
|
|
],
|
|
),
|
|
),
|
|
|
|
_buildInfoSection('RINCIAN PENDAPATAN', [
|
|
_buildDetailRow('Gaji Pokok', _formatCurrency(_detail['gaji_pokok'] ?? 0)),
|
|
_buildDetailRow('Uang Makan', _formatCurrency(_detail['potongan_makan'] ?? 0)), // It's usually a static or dynamic field
|
|
_buildDetailRow('Insentif Pekerjaan', _formatCurrency(_detail['gaji_kotor'] ?? 0), isBold: true),
|
|
]),
|
|
|
|
const SizedBox(height: 20),
|
|
_buildInfoSection('POTONGAN', [
|
|
_buildDetailRow('Potongan Kasbon', '- ${_formatCurrency(_detail['potongan_kasbon'])}', color: _rose),
|
|
_buildDetailRow('Potongan Absensi', '- ${_formatCurrency(_detail['potongan_absensi'])}', color: _rose),
|
|
]),
|
|
|
|
const Divider(height: 48, color: _line2),
|
|
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text('TOTAL DITERIMA',
|
|
style: TextStyle(color: _t1, fontSize: 14, fontWeight: FontWeight.w800)),
|
|
Text(_formatCurrency(_detail['gaji_bersih']),
|
|
style: const TextStyle(color: _green, fontSize: 22, fontWeight: FontWeight.w900)),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 32),
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: _bg2,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: _line2),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('CATATAN PEKERJAAN',
|
|
style: TextStyle(color: _t2, fontSize: 10, fontWeight: FontWeight.w800, letterSpacing: 1)),
|
|
const SizedBox(height: 8),
|
|
Text(_detail['rincian_pekerjaan'] ?? 'Tidak ada rincian',
|
|
style: const TextStyle(color: _t1, fontSize: 12, height: 1.5)),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 40),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInfoSection(String title, List<Widget> children) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title, style: const TextStyle(color: _t3, fontSize: 10, fontWeight: FontWeight.w900, letterSpacing: 1.2)),
|
|
const SizedBox(height: 12),
|
|
...children,
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildDetailRow(String label, String value, {bool isBold = false, Color? color}) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(label, style: TextStyle(color: _t2, fontSize: 13, fontWeight: isBold ? FontWeight.w700 : FontWeight.w400)),
|
|
Text(value, style: TextStyle(color: color ?? _t1, fontSize: 13, fontWeight: isBold ? FontWeight.w800 : FontWeight.w600)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|