836 lines
32 KiB
Dart
836 lines
32 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:device_preview/device_preview.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'login/login.dart';
|
|
import '../api/LoginApi.dart';
|
|
import '../absensi/absensi.dart';
|
|
import '../api/AbsensiApi.dart';
|
|
import 'models/Absensi_model.dart';
|
|
import 'tugas/Penugasan.dart';
|
|
import 'tugas/Progress.dart';
|
|
import '../api/PenugasanApi.dart';
|
|
import 'models/Penugasan_model.dart';
|
|
import 'keuangan/FinanceMain.dart';
|
|
import 'profil/Profil.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
import 'package:intl/date_symbol_data_local.dart';
|
|
|
|
// ── Palette (konsisten dengan login screen) ──────────────────────────────────
|
|
const _bg = Color(0xFFF9FAFB);
|
|
const _bg1 = Color(0xFFFFFFFF);
|
|
const _bg2 = Color(0xFFF3F4F6);
|
|
const _green = Color(0xFF10B981);
|
|
const _greenDim = Color(0x1A10B981);
|
|
const _greenGlow = Color(0x4D10B981);
|
|
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 _violet = Color(0xFF8B5CF6);
|
|
const _violetDim = Color(0x1A8B5CF6);
|
|
const _t1 = Color(0xFF111827);
|
|
const _t2 = Color(0xFF6B7280);
|
|
const _t3 = Color(0xFF9CA3AF);
|
|
const _line2 = Color(0xFFE5E7EB);
|
|
|
|
void main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
await initializeDateFormatting('id_ID', null);
|
|
runApp(
|
|
DevicePreview(
|
|
enabled: !kReleaseMode,
|
|
builder: (context) => const MyApp(),
|
|
),
|
|
);
|
|
}
|
|
|
|
class MyApp extends StatelessWidget {
|
|
const MyApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
useInheritedMediaQuery: true,
|
|
builder: DevicePreview.appBuilder,
|
|
locale: DevicePreview.locale(context),
|
|
debugShowCheckedModeBanner: false,
|
|
title: 'PDAM Teknisi',
|
|
theme: ThemeData(
|
|
fontFamily: 'Roboto',
|
|
brightness: Brightness.light,
|
|
scaffoldBackgroundColor: _bg,
|
|
primarySwatch: Colors.blue,
|
|
colorScheme: ColorScheme.fromSeed(
|
|
seedColor: _green,
|
|
brightness: Brightness.light,
|
|
primary: _green,
|
|
surface: _bg1,
|
|
background: _bg,
|
|
),
|
|
useMaterial3: true,
|
|
),
|
|
initialRoute: '/',
|
|
routes: {
|
|
'/': (context) => const LoginScreen(),
|
|
'/dashboard': (context) => const DashboardScreen(),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class DashboardScreen extends StatefulWidget {
|
|
const DashboardScreen({super.key});
|
|
|
|
@override
|
|
State<DashboardScreen> createState() => _DashboardScreenState();
|
|
}
|
|
|
|
class _DashboardScreenState extends State<DashboardScreen> {
|
|
int _selectedIndex = 0;
|
|
final ApiService _apiService = ApiService();
|
|
Map<String, dynamic>? _userData;
|
|
Map<String, dynamic>? _dashboardData;
|
|
bool _isLoadingProfile = false;
|
|
bool _isLoadingDashboard = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadUserData();
|
|
_loadDashboardData();
|
|
}
|
|
|
|
Future<void> _loadUserData() async {
|
|
setState(() { _isLoadingProfile = true; });
|
|
final localUserData = await _apiService.getUserData();
|
|
if (localUserData != null) {
|
|
setState(() { _userData = localUserData; });
|
|
}
|
|
final result = await _apiService.getProfile();
|
|
setState(() { _isLoadingProfile = false; });
|
|
if (result['success'] == true && result['data'] != null) {
|
|
setState(() { _userData = result['data']; });
|
|
} else if (result['logout'] == true) {
|
|
if (mounted) {
|
|
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _loadDashboardData() async {
|
|
setState(() { _isLoadingDashboard = true; });
|
|
final result = await _apiService.getDashboardData();
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoadingDashboard = false;
|
|
if (result['success'] == true) {
|
|
_dashboardData = result['data'];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
List<Widget> get _pages => [
|
|
HomePage(
|
|
userData: _userData,
|
|
dashboardData: _dashboardData,
|
|
isLoading: _isLoadingProfile || _isLoadingDashboard,
|
|
onNavigate: _onItemTapped,
|
|
),
|
|
const AbsensiScreen(),
|
|
PenugasanScreen(),
|
|
ProgressScreen(),
|
|
const FinanceMainScreen(),
|
|
];
|
|
|
|
void _onItemTapped(int index) {
|
|
setState(() {
|
|
_selectedIndex = index;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: _bg,
|
|
body: _pages[_selectedIndex],
|
|
bottomNavigationBar: _buildBottomNav(),
|
|
);
|
|
}
|
|
|
|
Widget _buildBottomNav() {
|
|
final items = [
|
|
{'icon': Icons.grid_view_rounded, 'label': 'Dashboard'},
|
|
{'icon': Icons.fingerprint_rounded, 'label': 'Absensi'},
|
|
{'icon': Icons.assignment_rounded, 'label': 'Tugas'},
|
|
{'icon': Icons.trending_up_rounded, 'label': 'Progress'},
|
|
{'icon': Icons.account_balance_wallet_rounded, 'label': 'Gaji'},
|
|
];
|
|
|
|
return Container(
|
|
decoration: const BoxDecoration(
|
|
color: _bg1,
|
|
border: Border(top: BorderSide(color: _line2, width: 1)),
|
|
),
|
|
child: SafeArea(
|
|
top: false,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: List.generate(items.length, (i) {
|
|
final active = i == _selectedIndex;
|
|
return GestureDetector(
|
|
onTap: () => _onItemTapped(i),
|
|
child: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 200),
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 14, vertical: 6),
|
|
decoration: active
|
|
? BoxDecoration(
|
|
color: _greenDim,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: _green.withOpacity(0.3)),
|
|
)
|
|
: null,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
items[i]['icon'] as IconData,
|
|
size: 22,
|
|
color: active ? _green : _t3,
|
|
),
|
|
const SizedBox(height: 3),
|
|
Text(
|
|
items[i]['label'] as String,
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: active
|
|
? FontWeight.w700 : FontWeight.w400,
|
|
color: active ? _green : _t3,
|
|
letterSpacing: active ? 0.3 : 0,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class HomePage extends StatelessWidget {
|
|
final Map<String, dynamic>? userData;
|
|
final Map<String, dynamic>? dashboardData;
|
|
final bool isLoading;
|
|
final Function(int) onNavigate;
|
|
|
|
const HomePage({
|
|
super.key,
|
|
this.userData,
|
|
this.dashboardData,
|
|
this.isLoading = false,
|
|
required this.onNavigate,
|
|
});
|
|
|
|
Future<void> _handleLogout(BuildContext context) async {
|
|
final confirmed = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => Dialog(
|
|
backgroundColor: _bg1,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20),
|
|
side: const BorderSide(color: _line2),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 52, height: 52,
|
|
decoration: BoxDecoration(
|
|
color: _roseDim,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: _rose.withOpacity(0.3)),
|
|
),
|
|
child: const Icon(Icons.logout_rounded,
|
|
color: _rose, size: 24),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text('Konfirmasi Logout',
|
|
style: TextStyle(
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w700,
|
|
color: _t1)),
|
|
const SizedBox(height: 8),
|
|
const Text('Apakah Anda yakin ingin keluar?',
|
|
style: TextStyle(fontSize: 13, color: _t2),
|
|
textAlign: TextAlign.center),
|
|
const SizedBox(height: 24),
|
|
Row(children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: _t2,
|
|
side: const BorderSide(color: _line2),
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
child: const Text('Batal'),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: ElevatedButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: _rose,
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
child: const Text('Logout',
|
|
style: TextStyle(fontWeight: FontWeight.w700)),
|
|
),
|
|
),
|
|
]),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
if (confirmed == true && context.mounted) {
|
|
final apiService = ApiService();
|
|
await apiService.logout();
|
|
if (context.mounted) {
|
|
Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final teknisi = userData?['teknisi'];
|
|
final namaTeknisi = (teknisi?['nama'] ?? 'Teknisi') as String;
|
|
final idTeknisi = teknisi?['id_teknisi'] ?? '-';
|
|
final username = userData?['username'] ?? '-';
|
|
final initial = namaTeknisi.isNotEmpty
|
|
? namaTeknisi[0].toUpperCase() : 'T';
|
|
|
|
return Scaffold(
|
|
backgroundColor: _bg,
|
|
appBar: AppBar(
|
|
elevation: 0,
|
|
backgroundColor: _bg1,
|
|
title: Row(children: [
|
|
Container(
|
|
width: 30, height: 30,
|
|
decoration: BoxDecoration(
|
|
color: _greenDim,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: _green.withOpacity(0.35)),
|
|
),
|
|
child: const Icon(Icons.water_drop_rounded,
|
|
color: _green, size: 15),
|
|
),
|
|
const SizedBox(width: 10),
|
|
const Text('PDAM Teknisi',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w800,
|
|
color: _t1,
|
|
letterSpacing: 0.2)),
|
|
]),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.account_circle_rounded, color: _t2, size: 24),
|
|
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ProfilScreen())),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.logout_rounded, color: _rose, size: 22),
|
|
onPressed: () => _handleLogout(context),
|
|
),
|
|
const SizedBox(width: 8),
|
|
],
|
|
),
|
|
body: RefreshIndicator(
|
|
color: _green,
|
|
backgroundColor: _bg1,
|
|
onRefresh: () async {
|
|
final apiService = ApiService();
|
|
await apiService.getProfile();
|
|
},
|
|
child: SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 32),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// New Premium Header with Attendance Status
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Halo, $namaTeknisi!',
|
|
style: const TextStyle(fontSize: 26, fontWeight: FontWeight.w900, color: _t1, letterSpacing: -1)),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: [
|
|
Container(width: 8, height: 8, decoration: const BoxDecoration(color: _green, shape: BoxShape.circle)),
|
|
const SizedBox(width: 8),
|
|
const Text('Online & Siap Bertugas', style: TextStyle(fontSize: 12, color: _t2, fontWeight: FontWeight.w600)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 32),
|
|
|
|
// Wallet & Earnings Section (Horizontal Scroll or Compact)
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: _bg1,
|
|
borderRadius: BorderRadius.circular(28),
|
|
border: Border.all(color: _line2),
|
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 20, offset: const Offset(0, 10))],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text('RINGKASAN PENDAPATAN', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w800, color: _t3, letterSpacing: 1.5)),
|
|
Icon(Icons.account_balance_wallet_rounded, color: _green.withOpacity(0.5), size: 18),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('Estimasi Gaji', style: TextStyle(fontSize: 11, color: _t2, fontWeight: FontWeight.w600)),
|
|
const SizedBox(height: 4),
|
|
Text(_formatCurrency(dashboardData?['statistik']?['estimasi_gaji'] ?? 0),
|
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: _green)),
|
|
],
|
|
),
|
|
),
|
|
Container(width: 1, height: 40, color: _line2),
|
|
const SizedBox(width: 20),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('Kasbon Aktif', style: TextStyle(fontSize: 11, color: _t2, fontWeight: FontWeight.w600)),
|
|
const SizedBox(height: 4),
|
|
Text(_formatCurrency(dashboardData?['statistik']?['total_kasbon'] ?? 0),
|
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w900, color: _rose)),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 28),
|
|
|
|
// New Performance & Statistics Section
|
|
_sectionLabel('RINGKASAN KINERJA BULAN INI'),
|
|
const SizedBox(height: 16),
|
|
Container(
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
color: _bg1,
|
|
borderRadius: BorderRadius.circular(32),
|
|
border: Border.all(color: _line2),
|
|
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.2), blurRadius: 30, offset: const Offset(0, 15))],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
// Circular Progress
|
|
Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
SizedBox(
|
|
width: 86, height: 86,
|
|
child: CircularProgressIndicator(
|
|
value: ((dashboardData?['statistik']?['kehadiran'] ?? 0) as num) / 100.0,
|
|
strokeWidth: 9,
|
|
backgroundColor: _green.withOpacity(0.08),
|
|
valueColor: const AlwaysStoppedAnimation<Color>(_green),
|
|
strokeCap: StrokeCap.round,
|
|
),
|
|
),
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text('${dashboardData?['statistik']?['kehadiran'] ?? 0}%', style: const TextStyle(fontSize: 19, fontWeight: FontWeight.w900, color: _t1, letterSpacing: -0.5)),
|
|
Text('Hadir', style: TextStyle(fontSize: 9, color: _green.withOpacity(0.6), fontWeight: FontWeight.w800, letterSpacing: 0.5)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(width: 24),
|
|
// Stats Details
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_miniRow(Icons.check_circle_outline_rounded, _green, 'Tugas Selesai', '${dashboardData?['statistik']?['tugas_selesai'] ?? 0} Item'),
|
|
const SizedBox(height: 12),
|
|
_miniRow(Icons.timer_outlined, _amber, 'Jam Kerja', '${dashboardData?['statistik']?['jam_kerja'] ?? 0} Jam'),
|
|
const SizedBox(height: 12),
|
|
_miniRow(Icons.trending_up_rounded, _cyan, 'Efisiensi', '+${dashboardData?['statistik']?['efisiensi'] ?? 0}%'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 28),
|
|
|
|
// Quick Actions or Status
|
|
_sectionLabel('AKTIVITAS TERAKHIR'),
|
|
const SizedBox(height: 16),
|
|
_buildModernStatCard(
|
|
'Gaji Terakhir Diterima',
|
|
_formatCurrency(dashboardData?['statistik']?['gaji_terakhir'] ?? 0),
|
|
Icons.history_rounded, _cyan,
|
|
isWide: true
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _miniRow(IconData icon, Color color, String label, String value) {
|
|
return Row(
|
|
children: [
|
|
Icon(icon, color: color, size: 14),
|
|
const SizedBox(width: 8),
|
|
Text(label, style: const TextStyle(fontSize: 11, color: _t2, fontWeight: FontWeight.w500)),
|
|
const Spacer(),
|
|
Text(value, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w800, color: _t1)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildModernStatCard(String title, String value, IconData icon, Color color, {bool isWide = false}) {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: _bg1,
|
|
borderRadius: BorderRadius.circular(24),
|
|
border: Border.all(color: _line2),
|
|
boxShadow: [
|
|
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 10))
|
|
],
|
|
),
|
|
child: isWide
|
|
? Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(14)),
|
|
child: Icon(icon, color: color, size: 22),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(title, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: _t3, letterSpacing: 0.5)),
|
|
const SizedBox(height: 4),
|
|
Text(value, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: color)),
|
|
],
|
|
),
|
|
],
|
|
)
|
|
: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12)),
|
|
child: Icon(icon, color: color, size: 20),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Text(value, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w900, color: color)),
|
|
const SizedBox(height: 4),
|
|
Text(title, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: _t3)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _pill(String text, Color bg, Color fg) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: bg,
|
|
borderRadius: BorderRadius.circular(99),
|
|
border: Border.all(color: fg.withOpacity(0.25)),
|
|
),
|
|
child: Text(text, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: fg)),
|
|
);
|
|
}
|
|
|
|
Widget _sectionLabel(String label) {
|
|
return Text(label, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: _t3, letterSpacing: 1.4));
|
|
}
|
|
|
|
Widget _buildStatCard(String title, String value, IconData icon, Color color, Color dimColor) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: _bg1,
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: _line2),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
width: 34, height: 34,
|
|
decoration: BoxDecoration(
|
|
color: dimColor,
|
|
borderRadius: BorderRadius.circular(9),
|
|
border: Border.all(color: color.withOpacity(0.25)),
|
|
),
|
|
child: Icon(icon, color: color, size: 17),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(value, style: TextStyle(fontSize: 22, fontWeight: FontWeight.w800, color: color, letterSpacing: -0.5)),
|
|
const SizedBox(height: 2),
|
|
Text(title, style: const TextStyle(fontSize: 11, color: _t3, fontWeight: FontWeight.w500)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMenuCard(String title, IconData icon, Color color, Color dimColor, VoidCallback onTap) {
|
|
return Material(
|
|
color: _bg1, borderRadius: BorderRadius.circular(14),
|
|
child: InkWell(
|
|
onTap: onTap, borderRadius: BorderRadius.circular(14),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(14), border: Border.all(color: _line2)),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Container(
|
|
width: 36, height: 36,
|
|
decoration: BoxDecoration(color: dimColor, borderRadius: BorderRadius.circular(10), border: Border.all(color: color.withOpacity(0.25))),
|
|
child: Icon(icon, color: color, size: 18),
|
|
),
|
|
Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: _t1)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
String _formatCurrency(dynamic amount) {
|
|
final formatter = NumberFormat.currency(locale: 'id_ID', symbol: 'Rp ', decimalDigits: 0);
|
|
return formatter.format(amount ?? 0);
|
|
}
|
|
}
|
|
|
|
class SalaryHistoryPage extends StatefulWidget {
|
|
const SalaryHistoryPage({super.key});
|
|
|
|
@override
|
|
State<SalaryHistoryPage> createState() => _SalaryHistoryPageState();
|
|
}
|
|
|
|
class _SalaryHistoryPageState extends State<SalaryHistoryPage> {
|
|
final ApiService _apiService = ApiService();
|
|
List<dynamic> _riwayatGaji = [];
|
|
bool _isLoading = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_fetchRiwayat();
|
|
}
|
|
|
|
Future<void> _fetchRiwayat() async {
|
|
setState(() => _isLoading = true);
|
|
final res = await _apiService.getGajiRiwayat();
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
if (res['success'] == true) {
|
|
_riwayatGaji = res['data'] ?? [];
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: _bg,
|
|
appBar: AppBar(
|
|
elevation: 0, backgroundColor: _bg1,
|
|
title: const Row(children: [
|
|
Icon(Icons.account_balance_wallet_rounded, color: _amber, size: 18),
|
|
SizedBox(width: 8),
|
|
Text('Riwayat Gaji', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: _t1)),
|
|
]),
|
|
actions: [
|
|
IconButton(onPressed: _fetchRiwayat, icon: const Icon(Icons.refresh, color: _t2)),
|
|
],
|
|
),
|
|
body: _isLoading
|
|
? const Center(child: CircularProgressIndicator(color: _amber))
|
|
: _riwayatGaji.isEmpty
|
|
? const Center(child: Text('Belum ada riwayat gaji', style: TextStyle(color: _t2)))
|
|
: ListView.builder(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: _riwayatGaji.length,
|
|
itemBuilder: (context, i) {
|
|
final item = _riwayatGaji[i];
|
|
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(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(color: _amberDim, borderRadius: BorderRadius.circular(12)),
|
|
child: const Icon(Icons.receipt_long_rounded, color: _amber, size: 20),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Text('Periode: ${item['periode']}', style: const TextStyle(color: _t1, fontWeight: FontWeight.bold)),
|
|
Text('Tanggal Bayar: ${item['tanggal_bayar'] ?? '-'}', style: const TextStyle(color: _t2, fontSize: 12)),
|
|
]),
|
|
),
|
|
Text('Rp${item['gaji_bersih']}', style: const TextStyle(color: _green, fontWeight: FontWeight.bold, fontSize: 16)),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class KasbonHistoryPage extends StatefulWidget {
|
|
const KasbonHistoryPage({super.key});
|
|
|
|
@override
|
|
State<KasbonHistoryPage> createState() => _KasbonHistoryPageState();
|
|
}
|
|
|
|
class _KasbonHistoryPageState extends State<KasbonHistoryPage> {
|
|
final ApiService _apiService = ApiService();
|
|
List<dynamic> _riwayat = [];
|
|
bool _isLoading = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_fetch();
|
|
}
|
|
|
|
Future<void> _fetch() async {
|
|
setState(() => _isLoading = true);
|
|
final res = await _apiService.getKasbonRiwayat();
|
|
if (mounted) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
if (res['success'] == true) _riwayat = res['data'] ?? [];
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF9FAFB),
|
|
appBar: AppBar(
|
|
elevation: 0, backgroundColor: const Color(0xFFFFFFFF),
|
|
title: const Text('Riwayat Kasbon', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF111827))),
|
|
),
|
|
body: _isLoading
|
|
? const Center(child: CircularProgressIndicator(color: Color(0xFFF59E0B)))
|
|
: _riwayat.isEmpty
|
|
? const Center(child: Text('Belum ada riwayat kasbon', style: TextStyle(color: Color(0xFF6B7280))))
|
|
: ListView.builder(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: _riwayat.length,
|
|
itemBuilder: (context, i) {
|
|
final item = _riwayat[i];
|
|
final bool isLunas = item['status'] == 'Lunas';
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(color: const Color(0xFFFFFFFF), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFE5E7EB))),
|
|
child: Row(children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
decoration: BoxDecoration(color: isLunas ? const Color(0x1A10B981) : const Color(0x1AEF4444), borderRadius: BorderRadius.circular(12)),
|
|
child: Icon(isLunas ? Icons.check_circle_rounded : Icons.pending_rounded, color: isLunas ? const Color(0xFF10B981) : const Color(0xFFEF4444), size: 20),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
|
Text('Rp${item['jumlah']}', style: const TextStyle(color: Color(0xFF111827), fontWeight: FontWeight.bold)),
|
|
Text(item['keterangan'] ?? 'Tanpa keterangan', style: const TextStyle(color: Color(0xFF6B7280), fontSize: 12)),
|
|
Text(item['tanggal'] ?? '-', style: const TextStyle(color: Color(0xFF9CA3AF), fontSize: 11)),
|
|
])),
|
|
_statusBadge(item['status']),
|
|
]),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _statusBadge(String status) {
|
|
final bool isLunas = status == 'Lunas';
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(color: isLunas ? const Color(0x1A10B981) : const Color(0x1AEF4444), borderRadius: BorderRadius.circular(8), border: Border.all(color: isLunas ? const Color(0xFF10B981).withOpacity(0.3) : const Color(0xFFEF4444).withOpacity(0.3))),
|
|
child: Text(status, style: TextStyle(color: isLunas ? const Color(0xFF10B981) : const Color(0xFFEF4444), fontSize: 10, fontWeight: FontWeight.bold)),
|
|
);
|
|
}
|
|
} |