import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:penyaluran_app/app/data/models/pengaduan_model.dart'; import 'package:penyaluran_app/app/data/models/tindakan_pengaduan_model.dart'; import 'package:penyaluran_app/app/modules/warga/controllers/warga_dashboard_controller.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/utils/format_helper.dart'; import 'package:timeline_tile/timeline_tile.dart'; import 'package:image_picker/image_picker.dart'; import 'dart:io'; import 'package:penyaluran_app/app/widgets/widgets.dart'; class WargaDetailPengaduanView extends GetView { const WargaDetailPengaduanView({super.key}); @override Widget build(BuildContext context) { final Map args = Get.arguments ?? {}; final String pengaduanId = args['id'] ?? ''; if (pengaduanId.isEmpty) { return Scaffold( appBar: AppBar( title: const Text('Detail Pengaduan'), ), body: const Center( child: Text('ID Pengaduan tidak valid'), ), ); } return Scaffold( appBar: AppBar( title: const Text('Detail Pengaduan'), elevation: 0, ), body: RefreshIndicator( onRefresh: () async { await controller.getDetailPengaduan(pengaduanId); }, color: AppTheme.primaryColor, child: FutureBuilder>( future: controller.getDetailPengaduan(pengaduanId), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(AppTheme.primaryColor), ), ); } if (snapshot.hasError) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.error_outline, size: 60, color: Colors.red.shade300, ), const SizedBox(height: 16), Text( 'Error: ${snapshot.error}', style: const TextStyle( fontSize: 16, color: Colors.red, ), ), const SizedBox(height: 24), ElevatedButton.icon( onPressed: () { controller.getDetailPengaduan(pengaduanId); }, icon: const Icon(Icons.refresh), label: const Text('Coba Lagi'), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryColor, padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 12), ), ), ], ), ); } final data = snapshot.data; if (data == null || data['pengaduan'] == null) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.search_off, size: 60, color: Colors.grey.shade400, ), const SizedBox(height: 16), Text( 'Data pengaduan tidak ditemukan', style: TextStyle( fontSize: 16, color: Colors.grey.shade700, ), ), const SizedBox(height: 8), Text( 'Pengaduan mungkin telah dihapus atau tidak tersedia', style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), ], ), ); } final pengaduan = PengaduanModel.fromJson(data['pengaduan']); final List tindakanList = (data['tindakan'] as List) .map((item) => TindakanPengaduanModel.fromJson(item)) .toList(); return _buildDetailContent(context, pengaduan, tindakanList); }, ), ), floatingActionButton: FutureBuilder>( future: controller.getDetailPengaduan(pengaduanId), builder: (context, snapshot) { if (!snapshot.hasData) return const SizedBox(); final data = snapshot.data; if (data == null || data['pengaduan'] == null) { return const SizedBox(); } final pengaduan = PengaduanModel.fromJson(data['pengaduan']); // Tampilkan tombol feedback hanya jika status pengaduan SELESAI // dan belum ada feedback atau rating if (pengaduan.status?.toUpperCase() == 'SELESAI' && (pengaduan.feedbackWarga == null || pengaduan.ratingWarga == null)) { return FloatingActionButton.extended( onPressed: () { _showFeedbackDialog(context, pengaduan); }, backgroundColor: AppTheme.primaryColor, icon: const Icon(Icons.star, color: Colors.white), label: const Text('Beri Rating', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold)), elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), ); } return const SizedBox(); }, ), ); } Widget _buildDetailContent( BuildContext context, PengaduanModel pengaduan, List tindakanList, ) { // Tentukan status dan warna Color statusColor; String statusText; switch (pengaduan.status?.toUpperCase()) { case 'MENUNGGU': statusColor = Colors.orange; statusText = 'Menunggu'; break; case 'TINDAKAN': statusColor = Colors.blue; statusText = 'Tindakan'; break; case 'SELESAI': statusColor = Colors.green; statusText = 'Selesai'; break; default: statusColor = Colors.grey; statusText = pengaduan.status ?? 'Tidak Diketahui'; } return Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.white, Colors.grey.shade50], ), ), child: SingleChildScrollView( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header dengan status _buildHeaderWithStatus(context, pengaduan, statusColor, statusText), const SizedBox(height: 24), // Informasi penyaluran yang diadukan if (pengaduan.penerimaPenyaluran != null) _buildPenyaluranInfo(context, pengaduan), const SizedBox(height: 24), // Tampilkan feedback dan rating jika sudah ada if (pengaduan.status?.toUpperCase() == 'SELESAI' && (pengaduan.feedbackWarga != null || pengaduan.ratingWarga != null)) _buildFeedbackSection(context, pengaduan), if (pengaduan.status?.toUpperCase() == 'SELESAI' && (pengaduan.feedbackWarga != null || pengaduan.ratingWarga != null)) const SizedBox(height: 24), // Timeline tindakan _buildTindakanTimeline(context, tindakanList), // Padding di bagian bawah untuk memberikan space saat ada floating action button const SizedBox(height: 80), ], ), ), ); } // Widget untuk menampilkan feedback dan rating Widget _buildFeedbackSection(BuildContext context, PengaduanModel pengaduan) { return Card( elevation: 3, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.white, Colors.amber.shade50], ), ), padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header section Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.amber.shade100, shape: BoxShape.circle, ), child: Icon( Icons.feedback, color: Colors.amber.shade700, size: 20, ), ), const SizedBox(width: 12), Text( 'Feedback Anda', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.amber.shade700, ), ), ], ), // Divider Padding( padding: const EdgeInsets.only(top: 16), child: Divider( color: Colors.amber.shade200, thickness: 1, ), ), // Rating display if (pengaduan.ratingWarga != null) Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.amber.shade100, borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: List.generate(5, (index) { return Icon( index < (pengaduan.ratingWarga ?? 0) ? Icons.star : Icons.star_border, color: Colors.amber.shade700, size: 24, ); }), ), ), ), const SizedBox(height: 8), // Feedback content or placeholder if (pengaduan.feedbackWarga != null && pengaduan.feedbackWarga!.isNotEmpty) Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.amber.shade200), boxShadow: [ BoxShadow( color: Colors.amber.shade100.withOpacity(0.5), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.format_quote, size: 18, color: Colors.amber.shade400, ), const SizedBox(width: 8), Text( 'Komentar Anda:', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, color: Colors.amber.shade800, ), ), ], ), const SizedBox(height: 8), Text( pengaduan.feedbackWarga!, style: TextStyle( fontSize: 15, color: Colors.grey.shade800, height: 1.4, fontStyle: FontStyle.italic, ), ), ], ), ) else Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade200), ), child: Row( children: [ Icon( Icons.info_outline, size: 18, color: Colors.grey.shade600, ), const SizedBox(width: 12), Text( 'Anda belum memberikan komentar', style: TextStyle( fontSize: 14, color: Colors.grey.shade600, fontStyle: FontStyle.italic, ), ), ], ), ), ], ), ), ); } // Dialog untuk menambahkan atau mengedit feedback void _showFeedbackDialog(BuildContext context, PengaduanModel pengaduan) { final formKey = GlobalKey(); final feedbackController = TextEditingController(text: pengaduan.feedbackWarga); int selectedRating = pengaduan.ratingWarga ?? 0; showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setState) { return AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), title: Column( children: [ Icon( Icons.rate_review, color: AppTheme.primaryColor, size: 40, ), const SizedBox(height: 10), Text( 'Beri Feedback Pelayanan', textAlign: TextAlign.center, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( 'Bantu kami meningkatkan layanan dengan memberi rating dan komentar', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), ], ), content: Form( key: formKey, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Rating', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric( vertical: 10, horizontal: 8), decoration: BoxDecoration( color: Colors.amber.shade50, borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(5, (index) { return IconButton( onPressed: () { setState(() { selectedRating = index + 1; }); }, icon: Icon( index < selectedRating ? Icons.star : Icons.star_border, color: Colors.amber, size: 32, ), padding: EdgeInsets.zero, constraints: BoxConstraints(), ); }), ), ), const SizedBox(height: 20), const Text( 'Komentar', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 12), TextFormField( controller: feedbackController, maxLines: 4, decoration: InputDecoration( hintText: 'Tulis komentar Anda di sini...', filled: true, fillColor: Colors.grey.shade50, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade300), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: AppTheme.primaryColor), ), ), validator: (value) { if (selectedRating > 0 && (value == null || value.isEmpty)) { return 'Mohon berikan komentar'; } return null; }, ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text( 'Batal', style: TextStyle( color: Colors.grey.shade600, ), ), ), ElevatedButton( onPressed: () async { if (selectedRating == 0) { Get.snackbar( 'Peringatan', 'Mohon berikan rating terlebih dahulu', snackPosition: SnackPosition.TOP, backgroundColor: Colors.orange, colorText: Colors.white, ); return; } if (formKey.currentState!.validate()) { Navigator.pop(context); await controller.addPengaduanFeedback( pengaduan.id!, feedbackController.text, selectedRating, ); // Refresh data pengaduan await controller.getDetailPengaduan(pengaduan.id!); } }, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), elevation: 2, padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12), ), child: Text( 'Kirim Feedback', style: TextStyle( fontWeight: FontWeight.bold, ), ), ), ], ); }, ), ); } Widget _buildHeaderWithStatus( BuildContext context, PengaduanModel pengaduan, Color statusColor, String statusText, ) { return Card( elevation: 3, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.white, Colors.grey.shade50], ), ), padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( pengaduan.judul ?? 'Pengaduan', style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, letterSpacing: 0.3, ), ), ), _getStatusPill(pengaduan.status), ], ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey.shade200), ), child: Text( pengaduan.deskripsi ?? '', style: TextStyle( fontSize: 15, color: Colors.grey.shade800, height: 1.4, ), ), ), // Menampilkan foto pengaduan jika ada if (pengaduan.fotoPengaduan != null && pengaduan.fotoPengaduan!.isNotEmpty) ...[ const SizedBox(height: 16), Text( 'Foto Pengaduan:', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, color: Colors.grey.shade800, ), ), const SizedBox(height: 8), Container( height: 120, decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(8), ), child: SingleChildScrollView( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), child: Row( children: pengaduan.fotoPengaduan!.map((foto) { return GestureDetector( onTap: () => _showFullScreenImage(context, foto), child: Container( width: 120, height: 120, margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), image: DecorationImage( image: foto.startsWith('http') ? NetworkImage(foto) : FileImage(File(foto)) as ImageProvider, fit: BoxFit.cover, ), ), child: Stack( alignment: Alignment.bottomRight, children: [ Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.black.withOpacity(0.5), borderRadius: const BorderRadius.only( topLeft: Radius.circular(8), bottomRight: Radius.circular(8), ), ), child: const Icon( Icons.zoom_in, color: Colors.white, size: 16, ), ), ], ), ), ); }).toList(), ), ), ), ], const SizedBox(height: 16), Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue.shade50, shape: BoxShape.circle, ), child: Icon( Icons.calendar_today, size: 16, color: Colors.blue.shade700, ), ), const SizedBox(width: 12), Text( pengaduan.tanggalPengaduan != null ? FormatHelper.formatDateTime(pengaduan.tanggalPengaduan!) : '-', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: Colors.grey.shade800, ), ), ], ), const SizedBox(height: 20), // Panel status pengaduan _buildStatusPanel(context, pengaduan), ], ), ), ); } // Helper method untuk mendapatkan StatusPill berdasarkan status StatusPill _getStatusPill(String? status) { switch (status?.toUpperCase()) { case 'MENUNGGU': return StatusPill( status: 'Menunggu', backgroundColor: Colors.orange, textColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), ); case 'TINDAKAN': return StatusPill( status: 'Tindakan', backgroundColor: Colors.blue, textColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), ); case 'SELESAI': return StatusPill.completed(status: 'Selesai'); default: return StatusPill( status: status ?? 'Tidak Diketahui', backgroundColor: Colors.grey, textColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), ); } } Widget _buildStatusPanel(BuildContext context, PengaduanModel pengaduan) { final status = pengaduan.status?.toUpperCase() ?? ''; return Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: _getStatusColor(status).withOpacity(0.05), borderRadius: BorderRadius.circular(12), border: Border.all(color: _getStatusColor(status).withOpacity(0.3)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.info_outline, color: _getStatusColor(status), size: 20, ), const SizedBox(width: 10), Text( 'Status Pengaduan', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: _getStatusColor(status), ), ), ], ), const SizedBox(height: 12), _buildStatusGuideItem( status, _getStatusDescription(status), _getStatusColor(status), _getStatusIcon(status), ), ], ), ); } Widget _buildStatusGuideItem( String status, String description, Color color, IconData icon, ) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: color.withOpacity(0.2), shape: BoxShape.circle, ), child: Center( child: Icon( icon, color: color, size: 20, ), ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ StatusPill( status: _getStatusText(status), backgroundColor: color, textColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), ), const SizedBox(height: 8), Text( description, style: TextStyle( fontSize: 14, color: Colors.grey.shade700, height: 1.4, ), ), ], ), ), ], ); } String _getStatusText(String status) { switch (status) { case 'MENUNGGU': return 'Menunggu'; case 'TINDAKAN': return 'Tindakan'; case 'SELESAI': return 'Selesai'; default: return status; } } String _getStatusDescription(String status) { switch (status) { case 'MENUNGGU': return 'Pengaduan Anda sedang menunggu tindakan dari petugas'; case 'TINDAKAN': return 'Pengaduan Anda sedang dalam proses penanganan'; case 'SELESAI': return 'Pengaduan Anda telah selesai ditangani'; default: return 'Status pengaduan tidak diketahui'; } } Color _getStatusColor(String status) { switch (status) { case 'MENUNGGU': return Colors.orange; case 'TINDAKAN': return Colors.blue; case 'SELESAI': return Colors.green; default: return Colors.grey; } } IconData _getStatusIcon(String status) { switch (status) { case 'MENUNGGU': return Icons.hourglass_empty; case 'TINDAKAN': return Icons.engineering; case 'SELESAI': return Icons.check_circle; default: return Icons.help_outline; } } Widget _buildPenyaluranInfo(BuildContext context, PengaduanModel pengaduan) { return Card( elevation: 3, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.white, Colors.blue.shade50], ), ), padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppTheme.primaryColor.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon( Icons.inventory_2_rounded, color: AppTheme.primaryColor, size: 20, ), ), const SizedBox(width: 12), const Text( 'Informasi Penyaluran', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AppTheme.primaryColor, ), ), ], ), ], ), Padding( padding: const EdgeInsets.symmetric(vertical: 16), child: Divider( color: Colors.blue.shade100, thickness: 1, ), ), _buildInfoItem( 'Nama Penyaluran', pengaduan.namaPenyaluran, Icons.assignment), _buildInfoItem( 'Jenis Bantuan', pengaduan.jenisBantuan, Icons.category), _buildInfoItem( 'Jumlah Bantuan', pengaduan.isUang == true ? FormatHelper.formatRupiah( double.tryParse(pengaduan.jumlahBantuan ?? '0')) : pengaduan.jumlahBantuan, Icons.shopping_basket), _buildInfoItem( 'Deskripsi', pengaduan.deskripsiPenyaluran, Icons.description), ], ), ), ); } Widget _buildInfoItem(String label, String value, IconData icon) { return Padding( padding: const EdgeInsets.only(bottom: 16.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.grey.shade100, borderRadius: BorderRadius.circular(8), ), child: Icon( icon, size: 18, color: AppTheme.primaryColor, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), const SizedBox(height: 4), Text( value, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w500, ), ), ], ), ), ], ), ); } Widget _buildTindakanTimeline( BuildContext context, List tindakanList, ) { if (tindakanList.isEmpty) { return Card( elevation: 3, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Container( padding: const EdgeInsets.all(20), child: InfoCard( title: 'Belum Ada Tindakan', description: 'Pengaduan Anda sedang menunggu tindakan dari petugas', icon: Icons.hourglass_empty, backgroundColor: Colors.orange.shade50, iconColor: Colors.orange, ), ), ); } return Card( elevation: 3, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Colors.white, Colors.grey.shade50], ), ), padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue.shade100, shape: BoxShape.circle, ), child: Icon( Icons.timeline, color: Colors.blue.shade700, size: 20, ), ), const SizedBox(width: 12), Text( 'Riwayat Tindakan', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.blue.shade700, ), ), ], ), const SizedBox(height: 16), Container( padding: const EdgeInsets.symmetric(vertical: 8), child: ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: tindakanList.length, itemBuilder: (context, index) { final tindakan = tindakanList[index]; final bool isFirst = index == 0; final bool isLast = index == tindakanList.length - 1; return _buildTimelineTile( context, tindakan, isFirst, isLast, ); }, ), ), ], ), ), ); } Widget _buildTimelineTile( BuildContext context, TindakanPengaduanModel tindakan, bool isFirst, bool isLast, ) { Color dotColor; switch (tindakan.statusTindakan) { case 'SELESAI': dotColor = Colors.green; break; case 'PROSES': dotColor = Colors.blue; break; default: dotColor = Colors.grey; } return TimelineTile( alignment: TimelineAlign.start, isFirst: isFirst, isLast: isLast, indicatorStyle: IndicatorStyle( width: 24, height: 24, color: dotColor, padding: const EdgeInsets.symmetric(vertical: 2), iconStyle: IconStyle( color: Colors.white, iconData: tindakan.statusTindakan == 'SELESAI' ? Icons.check : Icons.sync, fontSize: 14, ), ), beforeLineStyle: LineStyle( color: Colors.grey.shade300, thickness: 2, ), afterLineStyle: LineStyle( color: Colors.grey.shade300, thickness: 2, ), endChild: Container( margin: const EdgeInsets.only(left: 20, bottom: 30), child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.1), spreadRadius: 1, blurRadius: 4, offset: const Offset(0, 2), ), ], border: Border.all( color: Colors.grey.shade200, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header dengan kategori dan status Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: dotColor.withOpacity(0.1), borderRadius: const BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( tindakan.kategoriTindakanText, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: dotColor, ), ), ), StatusPill( status: tindakan.statusTindakanText, backgroundColor: dotColor, textColor: Colors.white, ), ], ), ), // Content Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Deskripsi tindakan Text( tindakan.tindakan ?? '', style: TextStyle( fontSize: 15, color: Colors.grey.shade800, height: 1.4, ), ), // Catatan tindakan (jika ada) if (tindakan.catatan != null && tindakan.catatan!.isNotEmpty) ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade200), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.note, size: 16, color: Colors.grey.shade700, ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Catatan:', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 13, color: Colors.grey.shade700, ), ), const SizedBox(height: 4), Text( tindakan.catatan!, style: TextStyle( fontSize: 13, color: Colors.grey.shade700, fontStyle: FontStyle.italic, height: 1.4, ), ), ], ), ), ], ), ), ], // Hasil tindakan (jika ada) if (tindakan.hasilTindakan != null && tindakan.hasilTindakan!.isNotEmpty) ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.shade100), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.check_circle_outline, size: 16, color: Colors.blue.shade700, ), const SizedBox(width: 8), Text( 'Hasil Tindakan:', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 13, color: Colors.blue.shade700, ), ), ], ), const SizedBox(height: 6), Text( tindakan.hasilTindakan!, style: TextStyle( fontSize: 13, color: Colors.blue.shade900, height: 1.4, ), ), ], ), ), ], // Bukti tindakan (jika ada) if (tindakan.buktiTindakan != null && tindakan.buktiTindakan!.isNotEmpty) ...[ const SizedBox(height: 16), Text( 'Bukti Tindakan:', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, color: Colors.grey.shade800, ), ), const SizedBox(height: 8), Container( height: 100, decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(8), ), child: SingleChildScrollView( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), child: Row( children: tindakan.buktiTindakan!.map((bukti) { return GestureDetector( onTap: () => ShowImageDialog.showFullScreen( context, bukti), child: Container( width: 100, height: 100, margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade300), image: DecorationImage( image: bukti.startsWith('http') ? NetworkImage(bukti) : FileImage(File(bukti)) as ImageProvider, fit: BoxFit.cover, ), ), child: Stack( alignment: Alignment.bottomRight, children: [ Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.black.withOpacity(0.5), borderRadius: const BorderRadius.only( topLeft: Radius.circular(8), bottomRight: Radius.circular(8), ), ), child: const Icon( Icons.zoom_in, color: Colors.white, size: 16, ), ), ], ), ), ); }).toList(), ), ), ), ], ], ), ), // Footer dengan info petugas dan tanggal Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(12), bottomRight: Radius.circular(12), ), border: Border( top: BorderSide(color: Colors.grey.shade200), ), ), child: Column( children: [ Row( children: [ Icon( Icons.person, size: 14, color: Colors.grey.shade600, ), const SizedBox(width: 6), Expanded( child: Text( 'Oleh: ${tindakan.namaPetugas}', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, overflow: TextOverflow.ellipsis, ), ), ), ], ), const SizedBox(height: 6), Row( children: [ Icon( Icons.access_time, size: 14, color: Colors.grey.shade600, ), const SizedBox(width: 6), Expanded( child: Text( tindakan.tanggalTindakan != null ? FormatHelper.formatDateTime( tindakan.tanggalTindakan!) : '-', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, overflow: TextOverflow.ellipsis, ), ), ), ], ), ], ), ), ], ), ), ), ); } void _showFullScreenImage(BuildContext context, String imagePath) { ShowImageDialog.showFullScreen(context, imagePath); } } class TambahTindakanPengaduanView extends StatefulWidget { final String pengaduanId; const TambahTindakanPengaduanView({super.key, required this.pengaduanId}); @override State createState() => _TambahTindakanPengaduanViewState(); } class _TambahTindakanPengaduanViewState extends State { final formKey = GlobalKey(); final tindakanController = TextEditingController(); final catatanController = TextEditingController(); String? selectedKategori; String? selectedPrioritas; // List untuk menyimpan path file lokal final List buktiTindakanPaths = []; bool isUploading = false; final List kategoriOptions = [ 'VERIFIKASI_DATA', 'KUNJUNGAN_LAPANGAN', 'KOORDINASI_LINTAS_INSTANSI', 'PERBAIKAN_DATA_PENERIMA', 'PENYALURAN_ULANG', 'PENGGANTIAN_BANTUAN', 'MEDIASI', 'KLARIFIKASI', 'PENYESUAIAN_JUMLAH_BANTUAN', 'PEMERIKSAAN_KUALITAS_BANTUAN', 'PERBAIKAN_PROSES_DISTRIBUSI', 'EDUKASI_PENERIMA', 'PENYELESAIAN_ADMINISTRATIF', 'INVESTIGASI_PENYALAHGUNAAN', 'PELAPORAN_KE_PIHAK_BERWENANG', ]; final List prioritasOptions = [ 'RENDAH', 'SEDANG', 'TINGGI', ]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Tambah Tindakan Pengaduan'), elevation: 0, ), body: Form( key: formKey, child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Kategori Tindakan Text( 'Kategori Tindakan', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 8), DropdownButtonFormField( decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), hint: const Text('Pilih kategori tindakan'), value: selectedKategori, items: kategoriOptions.map((kategori) { return DropdownMenuItem( value: kategori, child: Text( kategori .split('_') .map((word) => word[0].toUpperCase() + word.substring(1).toLowerCase()) .join(' '), style: const TextStyle(fontSize: 14), ), ); }).toList(), onChanged: (value) { setState(() { selectedKategori = value; }); }, validator: (value) { if (value == null || value.isEmpty) { return 'Pilih kategori tindakan'; } return null; }, ), const SizedBox(height: 16), // Prioritas Text( 'Prioritas', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 8), DropdownButtonFormField( decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), hint: const Text('Pilih prioritas'), value: selectedPrioritas, items: prioritasOptions.map((prioritas) { return DropdownMenuItem( value: prioritas, child: Text( prioritas[0].toUpperCase() + prioritas.substring(1).toLowerCase(), style: const TextStyle(fontSize: 14), ), ); }).toList(), onChanged: (value) { setState(() { selectedPrioritas = value; }); }, validator: (value) { if (value == null || value.isEmpty) { return 'Pilih prioritas tindakan'; } return null; }, ), const SizedBox(height: 16), // Deskripsi Tindakan Text( 'Deskripsi Tindakan', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 8), TextFormField( controller: tindakanController, decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), hintText: 'Jelaskan tindakan yang dilakukan', contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), maxLines: 3, validator: (value) { if (value == null || value.isEmpty) { return 'Deskripsi tindakan tidak boleh kosong'; } return null; }, ), const SizedBox(height: 16), // Catatan (opsional) Text( 'Catatan (opsional)', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 8), TextFormField( controller: catatanController, decoration: InputDecoration( border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), hintText: 'Tambahkan catatan jika diperlukan', contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), maxLines: 2, ), const SizedBox(height: 16), // Bukti Tindakan Text( 'Bukti Tindakan', style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 8), // Area upload bukti tindakan if (buktiTindakanPaths.isEmpty) InkWell( onTap: () => _showPilihSumberFoto(context), child: Container( height: 150, width: double.infinity, decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade400), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.camera_alt, size: 48, color: Colors.grey.shade600, ), const SizedBox(height: 8), Text( 'Tambah Bukti Tindakan', style: TextStyle( color: Colors.grey.shade600, fontWeight: FontWeight.bold, ), ), ], ), ), ) else Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 100, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: buktiTindakanPaths.length + 1, // +1 untuk tombol tambah itemBuilder: (context, index) { if (index == buktiTindakanPaths.length) { // Tombol tambah foto return InkWell( onTap: () => _showPilihSumberFoto(context), child: Container( width: 100, margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade400), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.add_photo_alternate, size: 32, color: Colors.grey.shade600, ), const SizedBox(height: 4), Text( 'Tambah', style: TextStyle( color: Colors.grey.shade600, fontSize: 12, ), ), ], ), ), ); } // Tampilkan foto yang sudah diambil return Stack( children: [ GestureDetector( onTap: () => _showFullScreenImage( context, buktiTindakanPaths[index]), child: Container( width: 100, height: 100, margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), image: DecorationImage( image: FileImage( File(buktiTindakanPaths[index])), fit: BoxFit.cover, ), ), ), ), Positioned( top: 4, right: 12, child: GestureDetector( onTap: () { setState(() { buktiTindakanPaths.removeAt(index); }); }, child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.black.withOpacity(0.5), shape: BoxShape.circle, ), child: const Icon( Icons.close, color: Colors.white, size: 16, ), ), ), ), ], ); }, ), ), ], ), ], ), ), ), bottomNavigationBar: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.2), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, -1), ), ], ), child: Row( children: [ Expanded( child: TextButton( onPressed: () => Get.back(), child: const Text('Batal'), ), ), const SizedBox(width: 16), Expanded( child: ElevatedButton( onPressed: isUploading ? null : _simpanTindakan, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryColor, padding: const EdgeInsets.symmetric(vertical: 12), ), child: isUploading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Text('Simpan'), ), ), ], ), ), ); } void _showPilihSumberFoto(BuildContext context) { Get.bottomSheet( Container( padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Pilih Sumber Foto', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), ListTile( leading: const Icon(Icons.camera_alt), title: const Text('Kamera'), onTap: () { Get.back(); _pickBuktiTindakan(true); }, ), ListTile( leading: const Icon(Icons.photo_library), title: const Text('Galeri'), onTap: () { Get.back(); _pickBuktiTindakan(false); }, ), ], ), ), ); } Future _pickBuktiTindakan(bool fromCamera) async { try { final ImagePicker picker = ImagePicker(); final XFile? pickedFile = await picker.pickImage( source: fromCamera ? ImageSource.camera : ImageSource.gallery, imageQuality: 70, maxWidth: 1000, ); if (pickedFile != null) { setState(() { buktiTindakanPaths.add(pickedFile.path); }); } } catch (e) { print('Error picking image: $e'); Get.snackbar( 'Error', 'Gagal mengambil gambar: ${e.toString()}', snackPosition: SnackPosition.TOP, backgroundColor: Colors.red, colorText: Colors.white, ); } } void _showFullScreenImage(BuildContext context, String imagePath) { ShowImageDialog.showFullScreen(context, imagePath); } Future _simpanTindakan() async { if (formKey.currentState!.validate()) { if (buktiTindakanPaths.isEmpty) { Get.snackbar( 'Error', 'Bukti tindakan harus diupload', snackPosition: SnackPosition.TOP, backgroundColor: Colors.red, colorText: Colors.white, ); return; } setState(() { isUploading = true; }); try { // Tampilkan pesan sukses Get.back(); // Kembali ke halaman sebelumnya Get.snackbar( 'Sukses', 'Tindakan berhasil disimpan', snackPosition: SnackPosition.TOP, backgroundColor: Colors.green, colorText: Colors.white, ); } catch (e) { print('Error saving tindakan: $e'); Get.snackbar( 'Error', 'Gagal menyimpan tindakan: ${e.toString()}', snackPosition: SnackPosition.TOP, backgroundColor: Colors.red, colorText: Colors.white, ); } finally { setState(() { isUploading = false; }); } } } }