last63

zakat_page.dart

Nov 16th, 2025
79
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 20.20 KB | None | 0 0
  1. // ================================================================
  2. // Perhitungan zakat penghasilan
  3. // berdasarkan harga emas dunia (GoldAPI) & kurs USD/IDR (OpenERAPI)
  4. // ================================================================
  5.  
  6. /// ๐Ÿ”ท Kelas utama halaman zakat
  7. /// Mewarisi `StatefulWidget` agar bisa memperbarui UI secara dinamis
  8. class ZakatPage extends StatefulWidget {
  9. const ZakatPage({Key? key}) : super(key: key);
  10.  
  11. @override
  12. State<ZakatPage> createState() => _ZakatPageState();
  13. }
  14.  
  15. /// ๐Ÿ”ท State untuk menyimpan data dan logika utama
  16. class _ZakatPageState extends State<ZakatPage> {
  17. // ---------------------------------------------------------------
  18. // ๐Ÿ”ท Controller untuk input user
  19. // ---------------------------------------------------------------
  20. final TextEditingController _incomeController = TextEditingController();
  21. final TextEditingController _expenseController = TextEditingController();
  22.  
  23. // ---------------------------------------------------------------
  24. // ๐Ÿ”ท Variabel utama logika zakat
  25. // ---------------------------------------------------------------
  26. double? _zakatAmount;
  27. double? _nisabValue;
  28. bool _isAboveNisab = false;
  29. bool _isLoading = false;
  30. List<double> _zakatHistory = [];
  31.  
  32. // ---------------------------------------------------------------
  33. // ๐Ÿ”ท Variabel untuk status dan cache
  34. // ---------------------------------------------------------------
  35. String? _lastUpdate; // waktu update terakhir dari API
  36. bool _isOffline = false; // apakah sedang pakai cache
  37.  
  38. // ๐Ÿ”ท Formatter untuk mata uang Rupiah (tanpa desimal)
  39. final NumberFormat _currencyFormat = NumberFormat.currency(
  40. locale: 'id_ID',
  41. symbol: 'Rp ',
  42. decimalDigits: 0,
  43. );
  44.  
  45. // ---------------------------------------------------------------
  46. // ๐Ÿ”ท Lifecycle method: dijalankan sekali saat halaman dimuat
  47. // ---------------------------------------------------------------
  48. @override
  49. void initState() {
  50. super.initState();
  51. _fetchGoldPrice(); // ambil data harga emas dan kurs
  52. }
  53.  
  54. // ================================================================
  55. // ๐Ÿ”ท Ambil data harga emas dari GoldAPI + kurs USD/IDR
  56. // ================================================================
  57. Future<void> _fetchGoldPrice() async {
  58. setState(() => _isLoading = true);
  59.  
  60. final prefs = await SharedPreferences.getInstance();
  61. const fallbackPricePerGram = 2000000.0; // fallback realistis (Rp 2 jt/gram)
  62. double? pricePerGram;
  63. double? usdToIdr;
  64.  
  65. try {
  66. print("๐ŸŒ [LOG] Mengambil harga emas & kurs USDโ†’IDR...");
  67.  
  68. // -------------------------------------------------------------
  69. // Ambil kurs USD โ†’ IDR dari open.er-api.com
  70. // -------------------------------------------------------------
  71. final exchangeRes = await http.get(
  72. Uri.parse('https://open.er-api.com/v6/latest/USD'),
  73. );
  74.  
  75. if (exchangeRes.statusCode == 200) {
  76. final exData = json.decode(exchangeRes.body);
  77. usdToIdr = (exData['rates']?['IDR'] ?? 15000).toDouble();
  78. print("๐Ÿ’ต Kurs USD โ†’ IDR: Rp ${usdToIdr?.toStringAsFixed(2)}");
  79. } else {
  80. usdToIdr = 15000;
  81. print("โš ๏ธ Gagal ambil kurs, fallback Rp $usdToIdr");
  82. }
  83.  
  84. // -------------------------------------------------------------
  85. // Ambil harga emas per gram 24K dari GoldAPI.io
  86. // -------------------------------------------------------------
  87. final goldRes = await http.get(
  88. Uri.parse('https://www.goldapi.io/api/XAU/USD'),
  89. headers: {
  90. 'x-access-token': 'goldapi-1wwsrsmhk0ch86-io',
  91. 'Content-Type': 'application/json',
  92. },
  93. );
  94.  
  95. if (goldRes.statusCode == 200) {
  96. final goldData = json.decode(goldRes.body);
  97. final double usdPerGram = (goldData['price_gram_24k'] ?? 0).toDouble();
  98. print("๐Ÿช™ Harga emas (USD/gram 24K): $usdPerGram");
  99.  
  100. // ๐Ÿ”น Konversi ke Rupiah
  101. pricePerGram = usdPerGram * (usdToIdr ?? 15000);
  102. print("๐Ÿ’ฐ Harga emas (IDR/gram): Rp ${pricePerGram?.toStringAsFixed(0)}");
  103.  
  104. // ๐Ÿ”น Validasi harga agar realistis
  105. if (pricePerGram! < 1800000 || pricePerGram > 2600000) {
  106. print("โš ๏ธ Harga emas tidak realistis ($pricePerGram). Gunakan fallback.");
  107. pricePerGram = fallbackPricePerGram;
  108. }
  109.  
  110. // ๐Ÿ”น Simpan hasil ke cache
  111. await prefs.setDouble('last_gold_price', pricePerGram);
  112. await prefs.setString('last_update', DateTime.now().toIso8601String());
  113. _isOffline = false;
  114. } else {
  115. // ๐Ÿ”น Fallback ke cache
  116. pricePerGram = prefs.getDouble('last_gold_price') ?? fallbackPricePerGram;
  117. _isOffline = true;
  118. }
  119. } catch (e) {
  120. // ๐Ÿ”น Jika tidak konek API
  121. print("๐Ÿ“ด Tidak bisa konek API: $e");
  122. pricePerGram = prefs.getDouble('last_gold_price') ?? fallbackPricePerGram;
  123. _isOffline = true;
  124. }
  125.  
  126. // -------------------------------------------------------------
  127. // Hitung nilai nisab (85 gram emas)
  128. // -------------------------------------------------------------
  129. setState(() {
  130. _nisabValue = 85 * (pricePerGram ?? fallbackPricePerGram);
  131. _lastUpdate = prefs.getString('last_update');
  132. _isLoading = false;
  133. });
  134.  
  135. print("โœ… Nisab final (85 gram): Rp ${_nisabValue?.toStringAsFixed(0)}");
  136. }
  137.  
  138. // ================================================================
  139. // ๐Ÿ”ท Logika perhitungan zakat penghasilan
  140. // ================================================================
  141. void _calculateZakat() {
  142. /// Langkah-langkah logika:
  143. /// 1๏ธโƒฃ Ambil input gaji & pengeluaran
  144. /// 2๏ธโƒฃ Bersihkan format (hilangkan titik, koma, .00)
  145. /// 3๏ธโƒฃ Validasi input
  146. /// 4๏ธโƒฃ Hitung zakat (2.5% dari penghasilan bersih)
  147. /// 5๏ธโƒฃ Bandingkan dengan nisab untuk menentukan wajib/tidak
  148.  
  149. String incomeText = _incomeController.text.trim();
  150. String expenseText = _expenseController.text.trim();
  151.  
  152. String cleanIncome = toNumericString(incomeText, allowPeriod: false);
  153. String cleanExpense = toNumericString(expenseText, allowPeriod: false);
  154.  
  155. cleanIncome = cleanIncome.replaceAll(RegExp(r'\.00$'), '');
  156. cleanExpense = cleanExpense.replaceAll(RegExp(r'\.00$'), '');
  157.  
  158. if (cleanIncome.isEmpty || cleanExpense.isEmpty) {
  159. ScaffoldMessenger.of(context).showSnackBar(
  160. const SnackBar(
  161. content: Text('Mohon isi semua kolom dengan angka yang valid.'),
  162. backgroundColor: Colors.redAccent,
  163. ),
  164. );
  165. return;
  166. }
  167.  
  168. final double? income = double.tryParse(cleanIncome);
  169. final double? expenses = double.tryParse(cleanExpense);
  170.  
  171. if (income == null || expenses == null) {
  172. ScaffoldMessenger.of(context).showSnackBar(
  173. const SnackBar(
  174. content: Text('Angka tidak valid. Pastikan format tanpa huruf.'),
  175. backgroundColor: Colors.redAccent,
  176. ),
  177. );
  178. return;
  179. }
  180.  
  181. final double netIncome = (income - expenses).clamp(0, double.infinity);
  182. final double annualIncome = income * 12;
  183. final double zakat = netIncome * 0.025;
  184.  
  185. setState(() {
  186. _zakatAmount = zakat;
  187. _isAboveNisab = _nisabValue != null && annualIncome >= _nisabValue!;
  188. _zakatHistory.add(zakat);
  189. if (_zakatHistory.length > 6) _zakatHistory.removeAt(0);
  190. });
  191.  
  192. print("๐Ÿ“Š Net Income: $netIncome | Annual: $annualIncome | Zakat: $zakat");
  193. }
  194.  
  195. // ================================================================
  196. // ๐Ÿ”ท UI utama halaman zakat
  197. // ================================================================
  198. @override
  199. Widget build(BuildContext context) {
  200. return Scaffold(
  201. backgroundColor: Colors.grey[100],
  202. appBar: AppBar(
  203. backgroundColor: Colors.amber,
  204. elevation: 0,
  205. title: const Text(
  206. "Zakat Penghasilan",
  207. style: TextStyle(fontFamily: 'PoppinsSemiBold', color: Colors.white),
  208. ),
  209. ),
  210.  
  211. body: _isLoading
  212. ? const Center(child: CircularProgressIndicator(color: Colors.amber))
  213. : SingleChildScrollView(
  214. padding: const EdgeInsets.all(20),
  215. child: Column(
  216. crossAxisAlignment: CrossAxisAlignment.start,
  217. children: [
  218. // ======================================================
  219. // INFORMASI HARGA EMAS DUNIA + STATUS KONEKSI
  220. // ======================================================
  221. if (!_isLoading && _nisabValue != null)
  222. Container(
  223. width: double.infinity,
  224. padding: const EdgeInsets.all(14),
  225. margin: const EdgeInsets.only(bottom: 16),
  226. decoration: BoxDecoration(
  227. color: Colors.white,
  228. borderRadius: BorderRadius.circular(12),
  229. boxShadow: [
  230. BoxShadow(
  231. color: Colors.black.withOpacity(0.05),
  232. blurRadius: 5,
  233. offset: const Offset(0, 2),
  234. ),
  235. ],
  236. ),
  237.  
  238. /// Layout:
  239. /// [๐Ÿ“ˆ Icon] [Info Emas + Update] [๐ŸŸข Online / ๐Ÿ”ด Offline]
  240. child: Row(
  241. crossAxisAlignment: CrossAxisAlignment.start,
  242. children: [
  243. const Icon(Icons.trending_up,
  244. color: Colors.amber, size: 28),
  245. const SizedBox(width: 10),
  246.  
  247. // ๐Ÿ”น Info harga & waktu update
  248. Expanded(
  249. child: Column(
  250. crossAxisAlignment: CrossAxisAlignment.start,
  251. children: [
  252. const Text(
  253. "๐Ÿ“ˆ Harga emas dunia:",
  254. style: TextStyle(
  255. fontFamily: 'PoppinsSemiBold',
  256. fontSize: 14,
  257. color: Colors.black87,
  258. ),
  259. ),
  260. const SizedBox(height: 4),
  261. Text(
  262. "${_currencyFormat.format(_nisabValue! / 85)} / gram",
  263. style: const TextStyle(
  264. fontFamily: 'PoppinsBold',
  265. fontSize: 16,
  266. color: Colors.green,
  267. ),
  268. ),
  269. if (_lastUpdate != null)
  270. Text(
  271. "Update: ${DateFormat('dd MMM yyyy, HH:mm').format(DateTime.parse(_lastUpdate!))}",
  272. style: const TextStyle(
  273. fontSize: 12, color: Colors.grey),
  274. ),
  275. const Text(
  276. "Sumber: GoldAPI.io + open.er-api.com",
  277. style: TextStyle(
  278. fontSize: 11, color: Colors.grey),
  279. ),
  280. ],
  281. ),
  282. ),
  283.  
  284. // ๐Ÿ”น Status koneksi API (Online / Offline)
  285. Row(
  286. children: [
  287. Icon(Icons.circle,
  288. color: _isOffline
  289. ? Colors.redAccent
  290. : Colors.green,
  291. size: 10),
  292. const SizedBox(width: 4),
  293. Text(
  294. _isOffline ? "Offline (Cache)" : "Online",
  295. style: TextStyle(
  296. fontSize: 11,
  297. color: _isOffline
  298. ? Colors.redAccent
  299. : Colors.green,
  300. ),
  301. ),
  302. ],
  303. ),
  304. ],
  305. ),
  306. ),
  307.  
  308. // ======================================================
  309. // INFORMASI NISAB SAAT INI
  310. // ======================================================
  311. Container(
  312. width: double.infinity,
  313. padding: const EdgeInsets.all(16),
  314. decoration: BoxDecoration(
  315. color: Colors.amber.shade100,
  316. borderRadius: BorderRadius.circular(16),
  317. ),
  318. child: Column(
  319. crossAxisAlignment: CrossAxisAlignment.start,
  320. children: [
  321. Text(
  322. _nisabValue != null
  323. ? "๐Ÿ’ฐ Nisab saat ini (85 gram emas): ${_currencyFormat.format(_nisabValue)} per tahun."
  324. : "Mengambil data harga emas...",
  325. style: const TextStyle(fontSize: 14),
  326. ),
  327. if (_lastUpdate != null)
  328. Text(
  329. "๐Ÿ•“ Terakhir diperbarui: ${DateFormat('dd MMM yyyy, HH:mm').format(DateTime.parse(_lastUpdate!))}",
  330. style: const TextStyle(
  331. fontSize: 12, color: Colors.grey),
  332. ),
  333. if (_isOffline)
  334. const Text(
  335. "โš ๏ธ Mode offline: data diambil dari cache lokal",
  336. style: TextStyle(
  337. fontSize: 12, color: Colors.redAccent),
  338. ),
  339. ],
  340. ),
  341. ),
  342. const SizedBox(height: 20),
  343.  
  344. // ======================================================
  345. // INPUT: GAJI & PENGELUARAN
  346. // ======================================================
  347. const Text("Gaji Per Bulan",
  348. style: TextStyle(fontFamily: 'PoppinsSemiBold')),
  349. const SizedBox(height: 8),
  350. TextField(
  351. controller: _incomeController,
  352. keyboardType: TextInputType.number,
  353. inputFormatters: [
  354. CurrencyInputFormatter(
  355. leadingSymbol: '',
  356. useSymbolPadding: false,
  357. mantissaLength: 0,
  358. thousandSeparator: ThousandSeparator.Period,
  359. ),
  360. ],
  361. decoration: InputDecoration(
  362. hintText: "Masukkan gaji per bulan",
  363. prefixIcon:
  364. const Icon(Icons.monetization_on_outlined),
  365. prefixText: 'Rp ',
  366. filled: true,
  367. fillColor: Colors.white,
  368. border: OutlineInputBorder(
  369. borderRadius: BorderRadius.circular(12),
  370. borderSide: BorderSide.none,
  371. ),
  372. ),
  373. ),
  374. const SizedBox(height: 16),
  375.  
  376. const Text("Pengeluaran Pokok Per Bulan",
  377. style: TextStyle(fontFamily: 'PoppinsSemiBold')),
  378. const SizedBox(height: 8),
  379. TextField(
  380. controller: _expenseController,
  381. keyboardType: TextInputType.number,
  382. inputFormatters: [
  383. CurrencyInputFormatter(
  384. leadingSymbol: '',
  385. useSymbolPadding: false,
  386. mantissaLength: 0,
  387. thousandSeparator: ThousandSeparator.Period,
  388. ),
  389. ],
  390. decoration: InputDecoration(
  391. hintText: "Masukkan pengeluaran pokok",
  392. prefixIcon:
  393. const Icon(Icons.shopping_cart_outlined),
  394. prefixText: 'Rp ',
  395. filled: true,
  396. fillColor: Colors.white,
  397. border: OutlineInputBorder(
  398. borderRadius: BorderRadius.circular(12),
  399. borderSide: BorderSide.none,
  400. ),
  401. ),
  402. ),
  403. const SizedBox(height: 20),
  404.  
  405. // ======================================================
  406. // TOMBOL HITUNG ZAKAT
  407. // ======================================================
  408. SizedBox(
  409. width: double.infinity,
  410. child: ElevatedButton.icon(
  411. style: ElevatedButton.styleFrom(
  412. backgroundColor: Colors.amber,
  413. shape: RoundedRectangleBorder(
  414. borderRadius: BorderRadius.circular(12),
  415. ),
  416. padding: const EdgeInsets.symmetric(vertical: 14),
  417. ),
  418. onPressed: _calculateZakat,
  419. icon: const Icon(Icons.calculate, color: Colors.white),
  420. label: const Text("Hitung Zakat",
  421. style: TextStyle(
  422. fontFamily: 'PoppinsBold', fontSize: 16)),
  423. ),
  424. ),
  425. const SizedBox(height: 30),
  426.  
  427. // ======================================================
  428. // HASIL PERHITUNGAN ZAKAT
  429. // ======================================================
  430. if (_zakatAmount != null)
  431. Container(
  432. width: double.infinity,
  433. padding: const EdgeInsets.all(24),
  434. decoration: BoxDecoration(
  435. color: Colors.white,
  436. borderRadius: BorderRadius.circular(16),
  437. boxShadow: [
  438. BoxShadow(
  439. color: Colors.black.withOpacity(0.05),
  440. blurRadius: 6,
  441. offset: const Offset(0, 3),
  442. ),
  443. ],
  444. ),
  445. child: Column(
  446. children: [
  447. Text(
  448. _isAboveNisab
  449. ? "โœ… Anda WAJIB membayar zakat penghasilan."
  450. : "โ„น๏ธ Anda BELUM mencapai nisab zakat.",
  451. style: TextStyle(
  452. fontFamily: 'PoppinsSemiBold',
  453. color: _isAboveNisab
  454. ? Colors.green
  455. : Colors.redAccent,
  456. ),
  457. ),
  458. const SizedBox(height: 8),
  459. Text(
  460. _currencyFormat.format(_zakatAmount),
  461. style: const TextStyle(
  462. fontFamily: 'PoppinsBold',
  463. fontSize: 28,
  464. color: Colors.green),
  465. ),
  466. ],
  467. ),
  468. ),
  469. ],
  470. ),
  471. ),
  472. );
  473. }
  474. }
  475.  
Add Comment
Please, Sign In to add comment