Perbarui model dan tampilan untuk mendukung fitur tanda tangan dan pembatalan penyaluran. Modifikasi PenerimaPenyaluranModel untuk menambahkan properti tandaTangan. Ubah PenyaluranBantuanModel dengan mengganti alasanPenolakan menjadi alasanPembatalan dan menambahkan tanggalPembatalan serta tanggalSelesai. Perbarui DetailPenyaluranController untuk menangani data penyaluran dan penerima dengan lebih baik. Tambahkan logika baru di DetailPenyaluranPage untuk menampilkan informasi pembatalan dan tanda tangan. Perbarui tampilan KonfirmasiPenerimaPage untuk menyertakan fitur tanda tangan saat konfirmasi penerimaan.

This commit is contained in:
Khafidh Fuadi
2025-03-16 08:42:51 +07:00
parent da06611c3a
commit 49b60f3195
155 changed files with 21110 additions and 769 deletions

View File

@ -1,12 +1,15 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/penerima_penyaluran_model.dart';
import 'package:penyaluran_app/app/data/models/bentuk_bantuan_model.dart';
import 'package:penyaluran_app/app/modules/penyaluran/detail_penyaluran_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:image_picker/image_picker.dart';
import 'package:signature/signature.dart';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:penyaluran_app/app/utils/date_formatter.dart';
class KonfirmasiPenerimaPage extends StatefulWidget {
final PenerimaPenyaluranModel penerima;
@ -15,12 +18,12 @@ class KonfirmasiPenerimaPage extends StatefulWidget {
final DateTime? tanggalPenyaluran;
const KonfirmasiPenerimaPage({
Key? key,
super.key,
required this.penerima,
this.bentukBantuan,
this.jumlahBantuan,
this.tanggalPenyaluran,
}) : super(key: key);
});
@override
State<KonfirmasiPenerimaPage> createState() => _KonfirmasiPenerimaPageState();
@ -34,11 +37,26 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
bool _setujuPenggunaan = false;
bool _isLoading = false;
// Controller untuk tanda tangan
final SignatureController _signatureController = SignatureController(
penStrokeWidth: 3,
penColor: AppTheme.primaryColor,
exportBackgroundColor: Colors.white,
);
// Untuk menyimpan gambar tanda tangan
Uint8List? _signatureImage;
@override
void dispose() {
// Pastikan controller signature dibersihkan
_signatureController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final warga = widget.penerima.warga;
final dateFormat = DateFormat('dd MMMM yyyy', 'id_ID');
final timeFormat = DateFormat('HH:mm', 'id_ID');
return Scaffold(
appBar: AppBar(
@ -49,29 +67,40 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
onPressed: () => Get.back(),
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
body: Obx(
() => controller.isProcessing.value || _isLoading
? const Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildDetailPenerimaSection(warga),
const SizedBox(height: 16),
_buildDetailBantuanSection(),
const SizedBox(height: 16),
_buildFotoBuktiSection(),
const SizedBox(height: 16),
_buildTandaTanganSection(),
const SizedBox(height: 16),
_buildFormPersetujuanSection(),
const SizedBox(height: 24),
_buildKonfirmasiButton(),
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Sedang memproses konfirmasi...'),
],
),
)
: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailPenerimaSection(warga),
const SizedBox(height: 16),
_buildDetailBantuanSection(),
const SizedBox(height: 16),
_buildFotoBuktiSection(),
const SizedBox(height: 16),
_buildTandaTanganSection(),
const SizedBox(height: 16),
_buildFormPersetujuanSection(),
const SizedBox(height: 24),
_buildKonfirmasiButton(),
],
),
),
),
),
),
);
}
@ -158,7 +187,7 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
'Tempat, Tanggal Lahir',
warga?['tempat_lahir'] != null &&
warga?['tanggal_lahir'] != null
? '${warga!['tempat_lahir']}, ${DateFormat('d MMMM yyyy').format(DateTime.parse(warga['tanggal_lahir']))}'
? '${warga!['tempat_lahir']}, ${DateFormatter.formatDate(DateTime.parse(warga['tanggal_lahir']), format: 'd MMMM yyyy')}'
: 'Bogor, 2 Juni 1990'),
const Divider(),
@ -183,9 +212,6 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
}
Widget _buildDetailBantuanSection() {
final dateFormat = DateFormat('dd MMMM yyyy', 'id_ID');
final timeFormat = DateFormat('HH:mm', 'id_ID');
// Tentukan satuan berdasarkan data yang tersedia
String satuan = '';
if (widget.bentukBantuan?.satuan != null) {
@ -197,10 +223,10 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
String tanggalWaktuPenyaluran = '';
if (widget.tanggalPenyaluran != null) {
final tanggal = dateFormat.format(widget.tanggalPenyaluran!);
final waktuMulai = timeFormat.format(widget.tanggalPenyaluran!);
final waktuSelesai = timeFormat
.format(widget.tanggalPenyaluran!.add(const Duration(hours: 1)));
final tanggal = DateFormatter.formatDate(widget.tanggalPenyaluran!);
final waktuMulai = DateFormatter.formatTime(widget.tanggalPenyaluran!);
final waktuSelesai = DateFormatter.formatTime(
widget.tanggalPenyaluran!.add(const Duration(hours: 1)));
tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai';
} else {
tanggalWaktuPenyaluran = '09 April 2025 13:00-14:00';
@ -328,39 +354,65 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
),
),
const SizedBox(height: 16),
// Area tanda tangan
Container(
width: double.infinity,
height: 100,
height: 200,
decoration: BoxDecoration(
color: Colors.grey[100],
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[300]!),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Tanda Tangan Digital',
style: TextStyle(
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Image.asset(
'assets/images/signature_placeholder.png',
height: 50,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Image.network(
'https://i.imgur.com/JMoZ0nR.png',
height: 50,
child: _signatureImage != null
? ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.memory(
_signatureImage!,
fit: BoxFit.contain,
);
},
),
)
: Signature(
controller: _signatureController,
backgroundColor: Colors.white,
height: 200,
width: double.infinity,
),
),
const SizedBox(height: 12),
// Tombol aksi untuk tanda tangan
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Tombol hapus tanda tangan
ElevatedButton.icon(
onPressed: () {
setState(() {
_signatureController.clear();
_signatureImage = null;
});
},
icon: const Icon(Icons.delete_outline),
label: const Text('Hapus'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[100],
foregroundColor: Colors.red[800],
),
],
),
),
// Tombol simpan tanda tangan
ElevatedButton.icon(
onPressed: _saveSignature,
icon: const Icon(Icons.check),
label: const Text('Simpan'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[100],
foregroundColor: Colors.green[800],
),
),
],
),
],
),
@ -531,6 +583,36 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
}
}
Future<void> _saveSignature() async {
if (_signatureController.isEmpty) {
Get.snackbar(
'Perhatian',
'Tanda tangan tidak boleh kosong',
backgroundColor: Colors.orange,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
return;
}
// Mendapatkan data tanda tangan
final signature = await _signatureController.toPngBytes();
if (signature != null) {
setState(() {
_signatureImage = signature;
});
Get.snackbar(
'Sukses',
'Tanda tangan berhasil disimpan',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
}
}
Future<void> _konfirmasiPenerimaan() async {
if (!_setujuPenerimaan || !_setujuPenggunaan) {
Get.snackbar(
@ -543,35 +625,98 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
return;
}
if (_signatureImage == null) {
Get.snackbar(
'Perhatian',
'Tanda tangan digital diperlukan',
backgroundColor: Colors.orange,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
return;
}
if (_buktiPenerimaan == null) {
Get.snackbar(
'Perhatian',
'Foto bukti penerimaan diperlukan',
backgroundColor: Colors.orange,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
return;
}
setState(() {
_isLoading = true;
});
try {
String? imageUrl;
Directory? tempDir;
File? signatureFile;
if (_buktiPenerimaan != null) {
// Upload bukti penerimaan
imageUrl =
await controller.uploadBuktiPenerimaan(_buktiPenerimaan!.path);
}
try {
String imageUrl;
String signatureUrl;
// Upload bukti penerimaan
imageUrl = await controller.uploadBuktiPenerimaan(_buktiPenerimaan!.path);
// Simpan tanda tangan ke file sementara dan upload
tempDir = await Directory.systemTemp.createTemp('signature');
signatureFile = File('${tempDir.path}/signature.png');
await signatureFile.writeAsBytes(_signatureImage!);
print('Signature file path: ${signatureFile.path}');
print('Signature file exists: ${signatureFile.existsSync()}');
print('Signature file size: ${signatureFile.lengthSync()} bytes');
signatureUrl = await controller.uploadBuktiPenerimaan(
signatureFile.path,
isTandaTangan: true,
);
// Konfirmasi penerimaan
await controller.konfirmasiPenerimaan(
widget.penerima,
buktiPenerimaan: imageUrl,
tandaTangan: signatureUrl,
);
// Hapus file sementara sebelum navigasi
try {
if (signatureFile.existsSync()) {
await signatureFile.delete();
}
if (tempDir.existsSync()) {
await tempDir.delete();
}
} catch (e) {
print('Error saat menghapus file sementara: $e');
}
// Tutup semua snackbar yang mungkin masih terbuka
if (Get.isSnackbarOpen) {
Get.closeAllSnackbars();
}
// Kembali ke halaman sebelumnya dengan hasil true (berhasil)
// Gunakan Get.back(result: true) untuk kembali ke halaman detail penyaluran
// dengan membawa hasil bahwa konfirmasi berhasil
Get.back(result: true);
Get.snackbar(
'Sukses',
'Konfirmasi penerimaan bantuan berhasil',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
// Tampilkan snackbar sukses di halaman detail penyaluran
Future.delayed(const Duration(milliseconds: 300), () {
Get.snackbar(
'Sukses',
'Konfirmasi penerimaan bantuan berhasil',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 2),
);
});
} catch (e) {
// Tampilkan pesan error
Get.snackbar(
'Error',
'Terjadi kesalahan: $e',
@ -580,6 +725,18 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
snackPosition: SnackPosition.BOTTOM,
);
} finally {
// Hapus file sementara jika belum dihapus
try {
if (signatureFile != null && signatureFile.existsSync()) {
await signatureFile.delete();
}
if (tempDir != null && tempDir.existsSync()) {
await tempDir.delete();
}
} catch (e) {
print('Error saat menghapus file sementara: $e');
}
setState(() {
_isLoading = false;
});