import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/Penugasan_model.dart'; import '../api/PenugasanApi.dart'; const _bg = Color(0xFFF9FAFB); const _bg1 = Color(0xFFFFFFFF); const _bg2 = Color(0xFFF3F4F6); const _green = Color(0xFF10B981); const _greenDim = Color(0x1A10B981); const _cyan = Color(0xFF06B6D4); const _cyanDim = Color(0x1A06B6D4); const _amber = Color(0xFFF59E0B); const _amberDim = Color(0x1AF59E0B); const _rose = Color(0xFFEF4444); const _roseDim = Color(0x1AEF4444); const _t1 = Color(0xFF111827); const _t2 = Color(0xFF6B7280); const _t3 = Color(0xFF9CA3AF); const _line2 = Color(0xFFE5E7EB); class ProgressScreen extends StatefulWidget { @override _ProgressScreenState createState() => _ProgressScreenState(); } class _ProgressScreenState extends State with SingleTickerProviderStateMixin { final _api = PenugasanApi(); List activeList = []; List historyList = []; bool isLoading = false; String? errorMessage; late TabController _tabController; bool? _pakaiPipaBesi; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _loadData(); } @override void dispose() { _tabController.dispose(); super.dispose(); } Future _loadData() async { setState(() { isLoading = true; errorMessage = null; }); try { final results = await Future.wait([ _fetchAllPages(status: 'dalam_proses'), _fetchAllPages(status: 'selesai'), ]); if (mounted) { setState(() { activeList = results[0]; historyList = results[1]; isLoading = false; }); } } catch (e) { if (mounted) { setState(() { errorMessage = 'Gagal terhubung ke server.'; isLoading = false; }); } } } Future> _fetchAllPages({required String status}) async { final List all = []; int page = 1; while (true) { final result = await _api.getPenugasanList(status: status, page: page); final data = result['data']; final items = (data['data'] as List? ?? []); all.addAll(items.map((e) => PenugasanModel.fromJson(e))); final int currentPage = data['current_page'] ?? 1; final int lastPage = data['last_page'] ?? 1; if (currentPage >= lastPage) break; page++; } return all; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: _bg, appBar: AppBar( title: Text('Daftar Tugas', style: TextStyle(fontWeight: FontWeight.w800, fontSize: 16)), backgroundColor: _bg1, foregroundColor: _t1, elevation: 0, bottom: TabBar( controller: _tabController, indicatorColor: _green, labelColor: _green, unselectedLabelColor: _t2, indicatorWeight: 3, dividerColor: _line2, tabs: [ Tab(text: 'Progres Aktif'), Tab(text: 'Riwayat Selesai'), ], ), ), body: TabBarView( controller: _tabController, children: [ _buildList(activeList, 'Tidak ada pekerjaan berjalan', Icons.trending_up_rounded), _buildList(historyList, 'Belum ada riwayat tugas', Icons.history_rounded), ], ), ); } Widget _buildList(List list, String emptyMsg, IconData emptyIcon) { if (isLoading) return Center(child: CircularProgressIndicator(color: _green)); if (errorMessage != null) return Center(child: Text(errorMessage!, style: TextStyle(color: _t1))); if (list.isEmpty) return Center( child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(emptyIcon, size: 60, color: _t3), SizedBox(height: 12), Text(emptyMsg, style: TextStyle(fontSize: 14, color: _t2)), ])); return RefreshIndicator( onRefresh: _loadData, color: _green, child: ListView.builder( padding: EdgeInsets.all(16), itemCount: list.length, itemBuilder: (context, index) => _buildProgressCard(list[index]), ), ); } Widget _buildProgressCard(PenugasanModel item) { final bool isDone = item.statusPekerjaan == 'selesai'; // ── VALIDASI FOTO: cek apakah foto sebelum & sesudah sudah ada ── final bool sudahFotoSebelum = item.fotoSebelumUrl != null && item.fotoSebelumUrl!.isNotEmpty; final bool sudahFotoSesudah = item.fotoSesudahUrl != null && item.fotoSesudahUrl!.isNotEmpty; final bool bolehSelesai = sudahFotoSebelum && sudahFotoSesudah; return Container( margin: EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: _bg1, borderRadius: BorderRadius.circular(16), border: Border.all( color: isDone ? _green.withOpacity(0.3) : _amber.withOpacity(0.3), ), ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Header ── Container( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: isDone ? _greenDim : _amberDim, borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row(children: [ Icon( isDone ? Icons.check_circle_outline : Icons.timer_outlined, size: 14, color: isDone ? _green : _amber, ), SizedBox(width: 6), Text( isDone ? 'SELESAI' : 'SEDANG DIKERJAKAN', style: TextStyle( color: isDone ? _green : _amber, fontSize: 10, fontWeight: FontWeight.w900, letterSpacing: 1), ), ]), Text( DateFormat('dd MMM yyyy').format(DateTime.parse(item.tanggalDiberikan).toLocal()), style: TextStyle( color: (isDone ? _green : _amber).withOpacity(0.7), fontSize: 11), ), ]), ), Padding( padding: EdgeInsets.all(16), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item.labelJenisPekerjaanFallback, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w800, color: _t1)), SizedBox(height: 8), if (item.teknisi != null) Row(children: [ Icon(Icons.person_outline, size: 14, color: _t2), SizedBox(width: 6), Expanded( child: Text(item.namaTim, style: TextStyle(fontSize: 13, color: _t2))), ]), SizedBox(height: 8), if (item.alamatLokasi != null) Container( padding: EdgeInsets.all(10), decoration: BoxDecoration( color: _bg2, borderRadius: BorderRadius.circular(10), border: Border.all(color: _line2), ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(Icons.location_on_outlined, size: 14, color: _green), SizedBox(width: 6), Expanded( child: Text(item.alamatLokasi!, style: TextStyle( fontSize: 13, color: _t1, fontWeight: FontWeight.w600))), ]), if (item.namaPelanggan != null || item.noSambungan != null) ...[ SizedBox(height: 6), Row(children: [ Icon(Icons.person, size: 13, color: _t2), SizedBox(width: 6), Text(item.namaPelanggan ?? '-', style: TextStyle(fontSize: 12, color: _t2)), if (item.noSambungan != null) ...[ SizedBox(width: 10), Icon(Icons.tag, size: 13, color: _t2), SizedBox(width: 4), Text(item.noSambungan!, style: TextStyle(fontSize: 12, color: _t2)), ], ]), ], ]), ), if (item.totalNilaiFormatted != 'Rp 0') ...[ SizedBox(height: 10), Row(children: [ Icon(Icons.monetization_on_outlined, size: 14, color: _green), SizedBox(width: 6), Text(item.totalNilaiFormatted, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w800, color: _green)), ]), ], // ── INDIKATOR STATUS FOTO (hanya untuk tugas aktif) ── if (!isDone) ...[ SizedBox(height: 10), Row(children: [ _fotoStatusChip( label: 'Foto Sebelum', sudah: sudahFotoSebelum, ), SizedBox(width: 8), _fotoStatusChip( label: 'Foto Sesudah', sudah: sudahFotoSesudah, ), ]), ], SizedBox(height: 14), Divider(height: 1, color: _line2), SizedBox(height: 14), if (!isDone) Row(children: [ Expanded( child: _aksiBtn( label: 'Foto Sebelum', icon: Icons.camera_outlined, color: _cyan, onTap: () => _showUploadFotoSheet(item, tipeFoto: 'sebelum'), )), SizedBox(width: 8), Expanded( child: _aksiBtn( label: 'Gali Urug', icon: Icons.construction_rounded, color: _cyan, onTap: () => _showAddGaliUrugDialog(item), )), SizedBox(width: 8), Expanded( child: _aksiBtn( label: 'Foto Sesudah', icon: Icons.camera_alt_outlined, color: _amber, onTap: () => _showUploadFotoSheet(item, tipeFoto: 'sesudah'), )), SizedBox(width: 8), // ── TOMBOL SELESAI: disabled jika foto belum lengkap ── Expanded( child: _aksiBtn( label: 'Selesai', icon: Icons.check_circle_outline, color: _green, disabled: !bolehSelesai, onTap: () { if (!sudahFotoSebelum) { _showFotoWarningSnackbar('Foto sebelum belum diupload!'); return; } if (!sudahFotoSesudah) { _showFotoWarningSnackbar('Foto sesudah belum diupload!'); return; } _pakaiPipaBesi = null; _openFinishSheet(item); }, )), ]) else Row(children: [ Expanded( child: _aksiBtn( label: 'Lihat Detail', icon: Icons.info_outline, color: _green, onTap: () => _openDetailSheet(item), )), SizedBox(width: 8), Expanded( child: _aksiBtn( label: 'Edit', icon: Icons.edit_outlined, color: _cyan, onTap: () => _openDetailSheet(item, startEditing: true), )), ]), ]), ), ]), ); } // ── Widget chip kecil indikator status foto ── Widget _fotoStatusChip({required String label, required bool sudah}) { return Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: sudah ? _greenDim : _roseDim, borderRadius: BorderRadius.circular(20), border: Border.all( color: sudah ? _green.withOpacity(0.4) : _rose.withOpacity(0.4), ), ), child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon( sudah ? Icons.check_circle : Icons.radio_button_unchecked, size: 11, color: sudah ? _green : _rose, ), SizedBox(width: 4), Text( label, style: TextStyle( fontSize: 10, fontWeight: FontWeight.w700, color: sudah ? _green : _rose, ), ), ]), ); } // ── Snackbar peringatan foto belum diupload ── void _showFotoWarningSnackbar(String pesan) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Row(children: [ Icon(Icons.warning_amber_rounded, color: _rose, size: 18), SizedBox(width: 8), Expanded(child: Text(pesan, style: TextStyle(color: _t1, fontSize: 13))), ]), backgroundColor: _bg1, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide(color: _rose.withOpacity(0.4)), ), behavior: SnackBarBehavior.floating, duration: Duration(seconds: 3), )); } void _openFinishSheet(PenugasanModel item) { final dimensiCtrl = TextEditingController(); final jarakCtrl = TextEditingController(); final unitCtrl = TextEditingController(); final detailCtrl = TextEditingController(); bool? localPakaiPipaBesi; showModalBottomSheet( context: context, backgroundColor: _bg1, isScrollControlled: true, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20))), builder: (sheetCtx) { return StatefulBuilder( builder: (ctx, setSheet) { return Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(ctx).viewInsets.bottom), child: SingleChildScrollView( child: Padding( padding: EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 40, height: 4, decoration: BoxDecoration( color: _line2, borderRadius: BorderRadius.circular(10)), margin: EdgeInsets.only(bottom: 20)), ), Row(children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: _greenDim, borderRadius: BorderRadius.circular(12), border: Border.all( color: _green.withOpacity(0.3))), child: Icon(Icons.check_circle_rounded, size: 22, color: _green), ), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Selesaikan Pekerjaan', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w800, color: _t1)), Text(item.labelJenisPekerjaanFallback, style: TextStyle( fontSize: 12, color: _t2)), ]), ), ]), SizedBox(height: 16), // ── Info: foto sudah lengkap ── Container( padding: EdgeInsets.symmetric( horizontal: 14, vertical: 10), decoration: BoxDecoration( color: _greenDim, borderRadius: BorderRadius.circular(10), border: Border.all( color: _green.withOpacity(0.3))), child: Row(children: [ Icon(Icons.check_circle_outline, size: 15, color: _green), SizedBox(width: 8), Expanded( child: Text( 'Foto sebelum & sesudah sudah terupload.', style: TextStyle( fontSize: 12, color: _green))), ]), ), SizedBox(height: 20), Text('Isi Detail Pekerjaan', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: _t1)), SizedBox(height: 12), if (['sr', 'pengembangan_jaringan_pipa', 'pemasangan_gate_valve', 'perbaikan_jaringan_pipa', 'pengecatan_pipa_besi', 'penyempurnaan_jaringan_pipa'].contains(item.jenisPekerjaan)) ...[ _sheetField( label: 'Dimensi Pipa', controller: dimensiCtrl, hint: 'Cth: 2, 3, 4, 6'), SizedBox(height: 10), ], if (['pengembangan_jaringan_pipa', 'penyempurnaan_jaringan_pipa'].contains(item.jenisPekerjaan)) ...[ _sheetField( label: 'Jarak (meter)', controller: jarakCtrl, hint: '0.00', keyboardType: TextInputType.number), SizedBox(height: 10), ], if (['sr', 'pengangkatan', 'perbaikan_jaringan_pipa'].contains(item.jenisPekerjaan)) ...[ _sheetField( label: 'Jumlah Unit', controller: unitCtrl, hint: '0', keyboardType: TextInputType.number), SizedBox(height: 10), ], if (item.jenisPekerjaan == 'pemasangan_gate_valve') Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Pakai Pipa Besi?', style: TextStyle( fontSize: 12, color: _t2, fontWeight: FontWeight.w600)), SizedBox(height: 8), Row(children: [ Expanded( child: GestureDetector( onTap: () => setSheet(() => localPakaiPipaBesi = true), child: Container( padding: EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: localPakaiPipaBesi == true ? _greenDim : _bg2, borderRadius: BorderRadius.circular(10), border: Border.all( color: localPakaiPipaBesi == true ? _green : _line2, width: localPakaiPipaBesi == true ? 1.5 : 1), ), child: Text('Ya (Rp 200.000)', textAlign: TextAlign.center, style: TextStyle( color: localPakaiPipaBesi == true ? _green : _t2, fontSize: 12, fontWeight: localPakaiPipaBesi == true ? FontWeight.w700 : FontWeight.w400)), ), ), ), SizedBox(width: 10), Expanded( child: GestureDetector( onTap: () => setSheet(() => localPakaiPipaBesi = false), child: Container( padding: EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: localPakaiPipaBesi == false ? _cyanDim : _bg2, borderRadius: BorderRadius.circular(10), border: Border.all( color: localPakaiPipaBesi == false ? _cyan : _line2, width: localPakaiPipaBesi == false ? 1.5 : 1), ), child: Text('Tidak (Gratis)', textAlign: TextAlign.center, style: TextStyle( color: localPakaiPipaBesi == false ? _cyan : _t2, fontSize: 12, fontWeight: localPakaiPipaBesi == false ? FontWeight.w700 : FontWeight.w400)), ), ), ), ]), SizedBox(height: 10), ], ), _sheetField( label: 'Detail Pekerjaan', controller: detailCtrl, hint: 'Deskripsi pekerjaan...', maxLines: 3), SizedBox(height: 20), Row(children: [ Expanded( child: OutlinedButton( onPressed: () => Navigator.pop(sheetCtx), child: Text('Batal'), style: OutlinedButton.styleFrom( foregroundColor: _t2, side: BorderSide(color: _line2), padding: EdgeInsets.symmetric(vertical: 14)), )), SizedBox(width: 12), Expanded( child: ElevatedButton( onPressed: () async { Navigator.pop(sheetCtx); setState(() => isLoading = true); try { final bool adaDetail = detailCtrl.text.isNotEmpty || jarakCtrl.text.isNotEmpty || unitCtrl.text.isNotEmpty || dimensiCtrl.text.isNotEmpty || localPakaiPipaBesi != null; // STEP 1: Simpan detail dulu (jika ada) if (adaDetail) { await _api.lengkapiDetail( idPenugasan: item.idPenugasan, items: [ { 'jenis_pekerjaan': item.jenisPekerjaan ?? '', 'dimensi_pipa': dimensiCtrl.text.isEmpty ? null : dimensiCtrl.text, 'jarak_meter': jarakCtrl.text.isEmpty ? null : double.tryParse( jarakCtrl.text), 'jumlah_unit': unitCtrl.text.isEmpty ? null : int.tryParse(unitCtrl.text), 'pakai_pipa_besi': localPakaiPipaBesi, } ], tanggalMulai: item.tanggalDiberikan, detailPekerjaan: detailCtrl.text, ); } // STEP 2: Update status ke selesai await _api.updateStatus( idPenugasan: item.idPenugasan, statusPekerjaan: 'selesai', tanggalDiselesaikan: DateTime.now().toIso8601String(), ); // STEP 3: Reload data await _loadData(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Pekerjaan berhasil diselesaikan! ✓'), backgroundColor: _bg1, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide( color: _green.withOpacity(0.4))), behavior: SnackBarBehavior.floating, ), ); _tabController.animateTo(1); } } catch (e) { if (mounted) { setState(() => isLoading = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Gagal: $e'), backgroundColor: _bg1, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide( color: _rose.withOpacity(0.4))), behavior: SnackBarBehavior.floating, ), ); } } finally { dimensiCtrl.dispose(); jarakCtrl.dispose(); unitCtrl.dispose(); detailCtrl.dispose(); } }, child: Text('Ya, Selesai', style: TextStyle( fontWeight: FontWeight.w800)), style: ElevatedButton.styleFrom( backgroundColor: _green, foregroundColor: Colors.black, padding: EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), ), )), ]), SizedBox(height: 8), ]), ), ), ); }, ); }, ); } Widget _sheetField({ required String label, required TextEditingController controller, String? hint, int maxLines = 1, TextInputType keyboardType = TextInputType.text, }) { return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle( fontSize: 12, color: _t2, fontWeight: FontWeight.w600)), SizedBox(height: 6), TextField( controller: controller, keyboardType: keyboardType, maxLines: maxLines, style: TextStyle(color: _t1, fontSize: 13), decoration: InputDecoration( hintText: hint, hintStyle: TextStyle(color: _t3), contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: _line2), borderRadius: BorderRadius.circular(8)), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: _green), borderRadius: BorderRadius.circular(8)), filled: true, fillColor: _bg2, ), ), ]); } Future _openDetailSheet(PenugasanModel item, {bool startEditing = false}) async { await showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: _bg1, builder: (context) => DetailPenugasanSheet( penugasan: item, api: _api, startEditing: startEditing, onUpdated: _loadData, ), ); } // ── _aksiBtn: sekarang mendukung parameter disabled ── Widget _aksiBtn({ required String label, required IconData icon, required Color color, required VoidCallback onTap, bool disabled = false, }) { final effectiveColor = disabled ? _t3 : color; return GestureDetector( onTap: disabled ? onTap : onTap, // tetap bisa tap agar snackbar muncul child: Container( padding: EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: effectiveColor.withOpacity(0.08), borderRadius: BorderRadius.circular(10), border: Border.all(color: effectiveColor.withOpacity(0.3)), ), child: Column(children: [ Icon(icon, size: 18, color: effectiveColor), SizedBox(height: 4), Text(label, style: TextStyle( fontSize: 10, color: effectiveColor, fontWeight: FontWeight.w700)), ]), ), ); } void _showUploadFotoSheet(PenugasanModel item, {String tipeFoto = 'sesudah'}) { showModalBottomSheet( context: context, backgroundColor: Colors.transparent, isScrollControlled: true, builder: (_) => _UploadFotoSheet( item: item, api: _api, tipeFoto: tipeFoto, onSuccess: () { Navigator.pop(context); _loadData(); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( 'Foto ${tipeFoto == 'sebelum' ? 'sebelum' : 'sesudah'} berhasil diupload!'), backgroundColor: _bg1, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide(color: _green.withOpacity(0.4))), behavior: SnackBarBehavior.floating, )); }, onFail: (err) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('Gagal: $err'), backgroundColor: _bg1, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide(color: _rose.withOpacity(0.4))), behavior: SnackBarBehavior.floating, )); }, ), ); } void _showAddGaliUrugDialog(PenugasanModel item) { final ctrl = TextEditingController(text: '1'); showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: _bg2, title: Text('Tambah Titik Gali Urug', style: TextStyle( color: _t1, fontSize: 16, fontWeight: FontWeight.w800)), content: Column(mainAxisSize: MainAxisSize.min, children: [ Text('Berapa titik gali urug yang ingin ditambahkan?', style: TextStyle(color: _t2, fontSize: 13)), SizedBox(height: 16), TextField( controller: ctrl, keyboardType: TextInputType.number, style: TextStyle(color: _t1), decoration: InputDecoration( labelText: 'Jumlah Titik', labelStyle: TextStyle(color: _cyan), suffixText: 'titik', suffixStyle: TextStyle(color: _t2), enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: _line2), borderRadius: BorderRadius.circular(10)), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: _cyan), borderRadius: BorderRadius.circular(10)), filled: true, fillColor: _bg1, ), ), ]), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Batal', style: TextStyle(color: _t3))), ElevatedButton( onPressed: () async { final qty = int.tryParse(ctrl.text) ?? 1; Navigator.pop(context); setState(() => isLoading = true); try { await _api.addItem( idPenugasan: item.idPenugasan, item: { 'jenis_pekerjaan': 'gali_urug', 'jumlah_titik': qty }); await _loadData(); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('$qty titik Gali Urug berhasil ditambahkan!'), backgroundColor: _bg1, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: BorderSide(color: _cyan.withOpacity(0.4))), behavior: SnackBarBehavior.floating, )); } catch (e) { setState(() => isLoading = false); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('Gagal: $e'), backgroundColor: _bg1)); } }, child: Text('Tambah'), style: ElevatedButton.styleFrom( backgroundColor: _cyan, foregroundColor: Colors.black), ), ], ), ); } } // =================================== // UPLOAD FOTO SHEET (Sebelum & Sesudah) // =================================== class _UploadFotoSheet extends StatefulWidget { final PenugasanModel item; final PenugasanApi api; final VoidCallback onSuccess; final Function(String) onFail; final String tipeFoto; const _UploadFotoSheet({ required this.item, required this.api, required this.onSuccess, required this.onFail, this.tipeFoto = 'sesudah', }); @override __UploadFotoSheetState createState() => __UploadFotoSheetState(); } class __UploadFotoSheetState extends State<_UploadFotoSheet> { XFile? _foto; bool _isLoading = false; final _picker = ImagePicker(); Future _pickFoto() async { final picked = await _picker.pickImage(source: ImageSource.camera, imageQuality: 70); if (picked != null) setState(() => _foto = picked); } Future _submit() async { if (_foto == null) { widget.onFail('Pilih foto terlebih dahulu'); return; } setState(() => _isLoading = true); try { await widget.api.uploadFoto( idPenugasan: widget.item.idPenugasan, tipeFoto: widget.tipeFoto, foto: _foto!); widget.onSuccess(); } catch (e) { setState(() => _isLoading = false); widget.onFail('$e'); } } @override Widget build(BuildContext context) { final bool isSebelum = widget.tipeFoto == 'sebelum'; final Color accentColor = isSebelum ? _cyan : _amber; return DraggableScrollableSheet( expand: false, initialChildSize: 0.55, maxChildSize: 0.8, builder: (_, controller) => Container( decoration: BoxDecoration( color: _bg1, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), border: Border(top: BorderSide(color: _line2))), padding: EdgeInsets.all(24), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 48, height: 5, decoration: BoxDecoration( color: _bg2, borderRadius: BorderRadius.circular(3)))), SizedBox(height: 20), Text( 'Upload Foto ${isSebelum ? 'Sebelum' : 'Sesudah'}', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w800, color: _t1), ), Text(widget.item.labelJenisPekerjaanFallback, style: TextStyle(fontSize: 13, color: _t3)), SizedBox(height: 20), GestureDetector( onTap: _pickFoto, child: Container( height: 150, decoration: BoxDecoration( color: _bg2, borderRadius: BorderRadius.circular(12), border: Border.all(color: _line2)), child: _foto != null ? (kIsWeb ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.check_circle, color: _green, size: 40), SizedBox(height: 8), Text('Foto dipilih ✓', style: TextStyle( color: _green, fontWeight: FontWeight.w600)), ])) : ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.file(File(_foto!.path), fit: BoxFit.cover, width: double.infinity))) : Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: isSebelum ? _cyanDim : _amberDim, shape: BoxShape.circle), child: Icon( isSebelum ? Icons.camera_outlined : Icons.camera_alt_outlined, color: accentColor, size: 26)), SizedBox(height: 10), Text('Klik untuk ambil foto', style: TextStyle(color: _t2, fontSize: 13)), SizedBox(height: 4), Text( isSebelum ? 'Foto kondisi sebelum pengerjaan' : 'Foto kondisi sesudah pengerjaan', style: TextStyle(color: _t3, fontSize: 11), ), ])), ), ), SizedBox(height: 20), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isLoading ? null : _submit, style: ElevatedButton.styleFrom( backgroundColor: accentColor, foregroundColor: Colors.black, padding: EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12))), child: _isLoading ? SizedBox( height: 20, width: 20, child: CircularProgressIndicator( color: Colors.black, strokeWidth: 2)) : Text( 'UPLOAD FOTO ${isSebelum ? 'SEBELUM' : 'SESUDAH'}', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w800, letterSpacing: 1)), ), ), ]), ), ); } } // =================================== // DETAIL PENUGASAN SHEET (tugas selesai) // =================================== class DetailPenugasanSheet extends StatefulWidget { final PenugasanModel penugasan; final PenugasanApi api; final VoidCallback? onUpdated; final bool startEditing; const DetailPenugasanSheet( {required this.penugasan, required this.api, this.onUpdated, this.startEditing = false}); @override _DetailPenugasanSheetState createState() => _DetailPenugasanSheetState(); } class _DetailPenugasanSheetState extends State { bool _isLoading = false; bool _isEditing = false; Map? _detail; late TextEditingController _detailCtrl; late TextEditingController _dimensiCtrl; late TextEditingController _jarakCtrl; late TextEditingController _unitCtrl; @override void initState() { super.initState(); _detailCtrl = TextEditingController(); _dimensiCtrl = TextEditingController(); _jarakCtrl = TextEditingController(); _unitCtrl = TextEditingController(); _loadDetail(); _isEditing = widget.startEditing; } @override void dispose() { _detailCtrl.dispose(); _dimensiCtrl.dispose(); _jarakCtrl.dispose(); _unitCtrl.dispose(); super.dispose(); } Future _loadDetail() async { setState(() => _isLoading = true); try { final result = await widget.api.getPenugasanDetail(widget.penugasan.idPenugasan); final detail = result['data'] as Map?; if (mounted) { setState(() { _detail = detail; _isLoading = false; _populateForm(); }); } } catch (e) { if (mounted) setState(() => _isLoading = false); } } void _populateForm() { if (_detail == null) return; final List? itemsList = _detail!['items'] as List?; final Map? firstItem = (itemsList != null && itemsList.isNotEmpty) ? Map.from(itemsList[0]) : null; _detailCtrl.text = _detail!['detail_pekerjaan'] ?? ''; _dimensiCtrl.text = (_detail!['dimensi_pipa'] ?? firstItem?['dimensi_pipa'])?.toString() ?? ''; _jarakCtrl.text = (_detail!['jarak_meter'] ?? firstItem?['jarak_meter'])?.toString() ?? ''; _unitCtrl.text = (_detail!['jumlah_unit'] ?? firstItem?['jumlah_unit'])?.toString() ?? ''; } Future _submitEdit() async { setState(() => _isLoading = true); try { final List? itemsList = _detail!['items'] as List?; final Map? firstItem = (itemsList != null && itemsList.isNotEmpty) ? Map.from(itemsList[0]) : null; final int? idPenugasanItem = firstItem != null ? (firstItem['id_penugasan_item'] as num?)?.toInt() : null; final String jenisPekerjaan = (_detail!['jenis_pekerjaan'] ?? firstItem?['jenis_pekerjaan'] ?? '') .toString(); final pakaiPipaBesi = _detail!['pakai_pipa_besi'] ?? firstItem?['pakai_pipa_besi']; final bool showDimensi = ['sr', 'pengembangan_jaringan_pipa', 'pemasangan_gate_valve', 'perbaikan_jaringan_pipa', 'pengecatan_pipa_besi', 'penyempurnaan_jaringan_pipa'].contains(jenisPekerjaan); final bool showJarak = ['pengembangan_jaringan_pipa', 'penyempurnaan_jaringan_pipa'].contains(jenisPekerjaan); final bool showUnit = ['sr', 'pengangkatan', 'perbaikan_jaringan_pipa'].contains(jenisPekerjaan); final items = [ { if (idPenugasanItem != null) 'id_penugasan_item': idPenugasanItem, 'jenis_pekerjaan': jenisPekerjaan, 'dimensi_pipa': (!showDimensi || _dimensiCtrl.text.isEmpty) ? null : _dimensiCtrl.text, 'jarak_meter': (!showJarak || _jarakCtrl.text.isEmpty) ? null : double.tryParse(_jarakCtrl.text), 'jumlah_unit': (!showUnit || _unitCtrl.text.isEmpty) ? null : int.tryParse(_unitCtrl.text), 'pakai_pipa_besi': pakaiPipaBesi, } ]; final prefs = await SharedPreferences.getInstance(); final idTeknisi = prefs.getInt('id_teknisi') ?? 0; await widget.api.updateDetail( idPenugasan: widget.penugasan.idPenugasan, idTeknisi: idTeknisi, items: items, detailPekerjaan: _detailCtrl.text, tanggalMulai: (_detail!['tanggal_mulai'] ?? _detail!['tanggal_diberikan'] ?? DateTime.now().toIso8601String()) .toString(), ); if (mounted) { setState(() { _isLoading = false; _isEditing = false; }); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('Data berhasil diperbarui!'), backgroundColor: _green, behavior: SnackBarBehavior.floating, )); widget.onUpdated?.call(); await _loadDetail(); } } catch (e) { if (mounted) setState(() => _isLoading = false); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text('Gagal update: $e'), backgroundColor: _rose, behavior: SnackBarBehavior.floating)); } } double? _toDouble(dynamic val) { if (val == null) return null; if (val is double) return val; if (val is int) return val.toDouble(); if (val is String) return double.tryParse(val); return null; } bool _hasValue(dynamic val) { final num = _toDouble(val); return num != null && num != 0; } dynamic _getField(String key) { if (_detail == null) return null; if (_detail![key] != null) return _detail![key]; final List? itemsList = _detail!['items'] as List?; if (itemsList != null && itemsList.isNotEmpty) { return itemsList[0][key]; } return null; } Widget _buildPipaBesiToggle() { return StatefulBuilder(builder: (ctx, setInner) { final List? itemsList = _detail!['items'] as List?; final firstItem = (itemsList != null && itemsList.isNotEmpty) ? Map.from(itemsList[0]) : null; final pakai = _detail!['pakai_pipa_besi'] ?? firstItem?['pakai_pipa_besi']; return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Pakai Pipa Besi?', style: TextStyle( fontSize: 12, color: _t2, fontWeight: FontWeight.w600)), SizedBox(height: 8), Row(children: [ Expanded( child: GestureDetector( onTap: () => setInner(() { _detail!['pakai_pipa_besi'] = true; if (firstItem != null && itemsList != null && itemsList.isNotEmpty) { (itemsList[0] as Map)['pakai_pipa_besi'] = true; } }), child: Container( padding: EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: pakai == true ? _greenDim : _bg2, borderRadius: BorderRadius.circular(10), border: Border.all( color: pakai == true ? _green : _line2, width: pakai == true ? 1.5 : 1), ), child: Text('Ya (Rp 200.000)', textAlign: TextAlign.center, style: TextStyle( color: pakai == true ? _green : _t2, fontSize: 12, fontWeight: pakai == true ? FontWeight.w700 : FontWeight.w400)), ), ), ), SizedBox(width: 10), Expanded( child: GestureDetector( onTap: () => setInner(() { _detail!['pakai_pipa_besi'] = false; if (firstItem != null && itemsList != null && itemsList.isNotEmpty) { (itemsList[0] as Map)['pakai_pipa_besi'] = false; } }), child: Container( padding: EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: pakai == false ? _cyanDim : _bg2, borderRadius: BorderRadius.circular(10), border: Border.all( color: pakai == false ? _cyan : _line2, width: pakai == false ? 1.5 : 1), ), child: Text('Tidak (Gratis)', textAlign: TextAlign.center, style: TextStyle( color: pakai == false ? _cyan : _t2, fontSize: 12, fontWeight: pakai == false ? FontWeight.w700 : FontWeight.w400)), ), ), ), ]), SizedBox(height: 12), ]); }); } @override Widget build(BuildContext context) { return DraggableScrollableSheet( expand: false, initialChildSize: 0.7, maxChildSize: 0.9, builder: (_, controller) => Container( decoration: BoxDecoration( color: _bg1, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), border: Border(top: BorderSide(color: _line2))), child: Column(children: [ Padding( padding: EdgeInsets.all(20), child: Center( child: Container( width: 48, height: 5, decoration: BoxDecoration( color: _bg2, borderRadius: BorderRadius.circular(3)))), ), Expanded( child: _isLoading ? Center(child: CircularProgressIndicator(color: _green)) : _detail == null ? Center( child: Text('Gagal memuat detail', style: TextStyle(color: _t2))) : ListView( controller: controller, padding: EdgeInsets.all(20), children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Detail Tugas Selesai', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w800, color: _t1)), TextButton( onPressed: () { if (_isEditing) { setState(() { _isEditing = false; _populateForm(); }); } else { setState(() => _isEditing = true); } }, child: Text( _isEditing ? 'Batal' : 'Edit', style: TextStyle( color: _green, fontWeight: FontWeight.w700)), ), ]), SizedBox(height: 20), if (_isEditing) ...[ _buildTextField('Detail Pekerjaan', _detailCtrl, maxLines: 4), SizedBox(height: 12), if (['sr', 'pengembangan_jaringan_pipa', 'pemasangan_gate_valve', 'perbaikan_jaringan_pipa', 'pengecatan_pipa_besi', 'penyempurnaan_jaringan_pipa'].contains(_getField('jenis_pekerjaan') ?? '')) ...[ _buildTextField('Dimensi Pipa', _dimensiCtrl, hint: 'Cth: 2, 3, 4, 6'), SizedBox(height: 12), ], if (['pengembangan_jaringan_pipa', 'penyempurnaan_jaringan_pipa'].contains(_getField('jenis_pekerjaan') ?? '')) ...[ _buildTextField( 'Jarak (m)', _jarakCtrl, keyboardType: TextInputType.number), SizedBox(height: 12), ], if (['sr', 'pengangkatan', 'perbaikan_jaringan_pipa'].contains(_getField('jenis_pekerjaan') ?? '')) ...[ _buildTextField( 'Jumlah Unit', _unitCtrl, keyboardType: TextInputType.number), SizedBox(height: 12), ], SizedBox(height: 12), if ((_getField('jenis_pekerjaan') ?? '') == 'pemasangan_gate_valve') _buildPipaBesiToggle(), SizedBox(height: 4), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isLoading ? null : _submitEdit, style: ElevatedButton.styleFrom( backgroundColor: _green, foregroundColor: Colors.black, padding: EdgeInsets.symmetric( vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12))), child: _isLoading ? SizedBox( height: 20, width: 20, child: CircularProgressIndicator( color: Colors.black, strokeWidth: 2)) : Text('Simpan Perubahan', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w800, letterSpacing: 1)), ), ), SizedBox(height: 18), Divider(color: _line2), SizedBox(height: 18), ], _buildRow('Jenis Pekerjaan', (_detail!['label_jenis_pekerjaan'] ?? _getField('label_jenis_pekerjaan') ?? 'N/A') .toString()), _buildRow('Lokasi', (_detail!['alamat_lokasi'] ?? 'N/A').toString()), _buildRow('Pelanggan', (_detail!['nama_pelanggan'] ?? '-').toString()), _buildRow('No. Sambungan', (_detail!['no_sambungan'] ?? '-').toString()), if (_detail!['items'] != null && (_detail!['items'] as List).length > 1) ...[ SizedBox(height: 16), Divider(color: _line2), SizedBox(height: 10), Text('RINCIAN PEKERJAAN', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w800, color: _cyan, letterSpacing: 1.2)), SizedBox(height: 12), ...(_detail!['items'] as List).map((it) => Container( margin: EdgeInsets.only(bottom: 10), padding: EdgeInsets.all(12), decoration: BoxDecoration(color: _bg2, borderRadius: BorderRadius.circular(10), border: Border.all(color: _line2)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(it['label_jenis_pekerjaan'] ?? 'Pekerjaan', style: TextStyle(color: _t1, fontWeight: FontWeight.bold, fontSize: 13)), if (it['dimensi_pipa'] != null) Text('Dimensi: ${it['dimensi_pipa']}', style: TextStyle(color: _t2, fontSize: 12)), if (it['jarak_meter'] != null) Text('Jarak: ${it['jarak_meter']} m', style: TextStyle(color: _t2, fontSize: 12)), if (it['jumlah_unit'] != null) Text('Jumlah: ${it['jumlah_unit']} unit', style: TextStyle(color: _t2, fontSize: 12)), if (it['jumlah_titik'] != null) Text('Titik: ${it['jumlah_titik']}', style: TextStyle(color: _t2, fontSize: 12)), ]), )).toList(), ], if ((_getField('dimensi_pipa')) ?.toString() .isNotEmpty == true && (_detail!['items'] as List).length <= 1) _buildRow('Dimensi Pipa', _getField('dimensi_pipa').toString()), _buildRow( 'Tanggal Diberikan', DateFormat('dd MMM yyyy').format(DateTime.parse( (_detail!['tanggal_diberikan'] ?? DateTime.now().toString()) .toString()).toLocal())), _buildRow( 'Tanggal Selesai', _detail!['tanggal_diselesaikan'] != null ? DateFormat('dd MMM yyyy').format( DateTime.parse(_detail![ 'tanggal_diselesaikan'] .toString()).toLocal()) : '-'), if (_detail!['total_nilai_pekerjaan'] != null && _toDouble( _detail!['total_nilai_pekerjaan']) != null && _toDouble(_detail!['total_nilai_pekerjaan'])! > 0) _buildRow( 'Nilai Pekerjaan', 'Rp ${NumberFormat('#,###', 'id_ID').format(_toDouble(_detail!['total_nilai_pekerjaan']) ?? 0)}', color: _green), if (_detail!['foto_sebelum_url'] != null || _detail!['foto_sesudah_url'] != null) ...[ SizedBox(height: 16), Divider(color: _line2), SizedBox(height: 16), Text('FOTO BUKTI', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w800, color: _t3, letterSpacing: 1.2)), SizedBox(height: 12), Row(children: [ if (_detail!['foto_sebelum_url'] != null) Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Sebelum', style: TextStyle(fontSize: 12, color: _t2)), SizedBox(height: 8), ClipRRect(borderRadius: BorderRadius.circular(10), child: Image.network(_detail!['foto_sebelum_url'], height: 120, fit: BoxFit.cover, width: double.infinity, errorBuilder: (_, __, ___) => Container(height: 120, color: _bg2, child: Center(child: Icon(Icons.image_not_supported_outlined, color: _t3))))), ])), if (_detail!['foto_sebelum_url'] != null && _detail!['foto_sesudah_url'] != null) SizedBox(width: 16), if (_detail!['foto_sesudah_url'] != null) Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Sesudah', style: TextStyle(fontSize: 12, color: _t2)), SizedBox(height: 8), ClipRRect(borderRadius: BorderRadius.circular(10), child: Image.network(_detail!['foto_sesudah_url'], height: 120, fit: BoxFit.cover, width: double.infinity, errorBuilder: (_, __, ___) => Container(height: 120, color: _bg2, child: Center(child: Icon(Icons.image_not_supported_outlined, color: _t3))))), ])), ]), ], if (_detail!['detail_pekerjaan'] != null && !_isEditing) ...[ SizedBox(height: 16), Divider(color: _line2), SizedBox(height: 16), Text('Deskripsi Detail', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: _t1)), SizedBox(height: 10), Text( _detail!['detail_pekerjaan'].toString(), style: TextStyle( fontSize: 13, color: _t2, height: 1.5)), ], ], ), ), ]), ), ); } Widget _buildRow(String label, String value, {Color color = _t1}) { return Padding( padding: EdgeInsets.symmetric(vertical: 10), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 120, child: Text(label, style: TextStyle( fontSize: 13, color: _t3, fontWeight: FontWeight.w600))), Expanded( child: Text(value, style: TextStyle( fontSize: 13, color: color, fontWeight: FontWeight.w500), maxLines: 2, overflow: TextOverflow.ellipsis)), ]), ); } Widget _buildTextField( String label, TextEditingController controller, { int maxLines = 1, String hint = '', TextInputType keyboardType = TextInputType.text, }) { return TextField( controller: controller, keyboardType: keyboardType, maxLines: maxLines, style: TextStyle(color: _t1), decoration: InputDecoration( labelText: label, hintText: hint.isNotEmpty ? hint : null, hintStyle: TextStyle(color: _t3), labelStyle: TextStyle(color: _green), enabledBorder: OutlineInputBorder( borderSide: BorderSide(color: _line2), borderRadius: BorderRadius.circular(12)), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: _green), borderRadius: BorderRadius.circular(12)), filled: true, fillColor: _bg2, ), ); } }