import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:intl/intl.dart'; import 'package:image_picker/image_picker.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 PenugasanScreen extends StatefulWidget { @override _PenugasanScreenState createState() => _PenugasanScreenState(); } class _PenugasanScreenState extends State { final _api = PenugasanApi(); List filteredList = []; StatistikModel? statistik; bool isLoading = false; String? errorMessage; @override void initState() { super.initState(); _loadData(); } Future _loadData() async { setState(() { isLoading = true; errorMessage = null; }); try { final results = await Future.wait([_api.getStatistik(), _api.getPenugasanList()]); final stat = StatistikModel.fromJson(results[0]['data']); final items = results[1]['data']['data'] as List? ?? []; final list = items.map((e) => PenugasanModel.fromJson(e)).toList(); // HANYA tampilkan tugas belum mulai final filtered = list.where((p) => p.statusPekerjaan == 'belum_mulai').toList(); if (mounted) setState(() { statistik = stat; filteredList = filtered; isLoading = false; }); } on PenugasanApiException catch (e) { if (mounted) setState(() { errorMessage = e.message; isLoading = false; }); } catch (_) { if (mounted) setState(() { errorMessage = 'Gagal terhubung ke server.'; isLoading = false; }); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: _bg, appBar: AppBar( title: Text('Penugasan Saya', style: TextStyle(fontWeight: FontWeight.w800, fontSize: 16)), backgroundColor: _bg1, foregroundColor: _t1, elevation: 0, surfaceTintColor: Colors.transparent, systemOverlayStyle: SystemUiOverlayStyle(statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.light), bottom: PreferredSize( preferredSize: Size.fromHeight(60), child: Container( padding: EdgeInsets.fromLTRB(16, 0, 16, 16), alignment: Alignment.centerLeft, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('DAFTAR TUGAS BARU', style: TextStyle(color: _cyan, fontSize: 11, fontWeight: FontWeight.w900, letterSpacing: 1.5)), SizedBox(height: 4), Text('Terima tugas dan mulai bekerja', style: TextStyle(color: _t2, fontSize: 12)), ]), ), ), ), body: _buildBody(), ); } Widget _buildStatistikCard() { if (statistik == null) return SizedBox.shrink(); return Container( padding: EdgeInsets.all(16), color: _bg, child: Row(children: [ Expanded(child: _buildStatItem('Menunggu Detail', statistik!.menungguDetail.toString(), _amber, _amberDim, Icons.pending_actions)), SizedBox(width: 12), Expanded(child: _buildStatItem('Detail Lengkap', statistik!.detailLengkap.toString(), _green, _greenDim, Icons.check_circle_outline)), ]), ); } Widget _buildStatItem(String label, String value, Color color, Color dimColor, IconData icon) { return Container( padding: EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration(color: _bg1, borderRadius: BorderRadius.circular(13), border: Border.all(color: _line2)), child: Row(children: [ Container( width: 38, height: 38, decoration: BoxDecoration(color: dimColor, borderRadius: BorderRadius.circular(10), border: Border.all(color: color.withOpacity(0.3))), child: Icon(icon, color: color, size: 20), ), SizedBox(width: 10), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(value, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: color)), SizedBox(height: 2), Text(label, style: TextStyle(fontSize: 11, color: _t2), maxLines: 1, overflow: TextOverflow.ellipsis), ]), ), ]), ); } Widget _buildBody() { if (isLoading) return Center(child: CircularProgressIndicator(color: _green)); if (errorMessage != null) return Center(child: Padding( padding: EdgeInsets.all(24), child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.wifi_off, size: 60, color: _rose), SizedBox(height: 16), Text(errorMessage!, textAlign: TextAlign.center, style: TextStyle(color: _t1)), SizedBox(height: 20), ElevatedButton.icon(onPressed: _loadData, icon: Icon(Icons.refresh, size: 18), label: Text('Coba Lagi'), style: ElevatedButton.styleFrom(backgroundColor: _greenDim, foregroundColor: _green, side: BorderSide(color: _green.withOpacity(0.4)))), ]), )); if (filteredList.isEmpty) return Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.assignment_outlined, size: 60, color: _t3), SizedBox(height: 12), Text('Belum ada penugasan', style: TextStyle(fontSize: 15, color: _t2, fontWeight: FontWeight.w500)), ])); return RefreshIndicator( onRefresh: _loadData, color: _green, backgroundColor: _bg1, child: ListView.builder( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), itemCount: filteredList.length, itemBuilder: (context, index) => _buildCard(filteredList[index]), ), ); } Widget _buildCard(PenugasanModel item) { return Container( margin: EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: _bg1, borderRadius: BorderRadius.circular(16), border: Border.all(color: _line2), ), child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Material( color: Colors.transparent, child: InkWell( onTap: () => _showDetail(item), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ // Foto & Instruksi Utama if (item.fotoSuratUrl != null) Image.network(item.fotoSuratUrl!, height: 160, width: double.infinity, fit: BoxFit.cover, loadingBuilder: (c, child, p) => p == null ? child : Container(height: 160, color: _bg2, child: Center(child: CircularProgressIndicator(color: _green, strokeWidth: 2))), errorBuilder: (_, __, ___) => Container(height: 100, color: _bg2, child: Center(child: Icon(Icons.image_not_supported_outlined, color: _t3)))) else Container( width: double.infinity, padding: EdgeInsets.fromLTRB(16, 20, 16, 12), decoration: BoxDecoration( gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [_bg2, _bg1]), border: Border(bottom: BorderSide(color: _line2)) ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('INSTRUKSI LOKASI & TUGAS', style: TextStyle(color: _cyan, fontSize: 10, fontWeight: FontWeight.w900, letterSpacing: 1.5)), SizedBox(height: 10), Text(item.catatanAdmin ?? 'Tidak ada catatan lokasi spesifik', style: TextStyle(color: _t1, fontSize: 14, fontWeight: FontWeight.w600, height: 1.4), maxLines: 3, overflow: TextOverflow.ellipsis), ]), ), Padding( padding: EdgeInsets.all(16), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row(children: [ Icon(Icons.calendar_today_outlined, size: 14, color: _t3), SizedBox(width: 6), Text(DateFormat('dd MMM yyyy').format(DateTime.parse(item.tanggalDiberikan).toLocal()), style: TextStyle(fontSize: 13, color: _t2)), ]), _buildStatusBadge(item.statusPekerjaan), ]), SizedBox(height: 12), if (item.teknisi != null) Row(children: [ Icon(Icons.person, size: 16, color: _cyan), SizedBox(width: 8), Expanded(child: Text(item.namaTim, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: _t1))), ]), SizedBox(height: 12), if (item.isDetailLengkap) ...[ Container( padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration(color: _cyanDim, borderRadius: BorderRadius.circular(8), border: Border.all(color: _cyan.withOpacity(0.3))), child: Row(mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.build_circle_outlined, size: 14, color: _cyan), SizedBox(width: 6), Text(item.labelJenisPekerjaanFallback, style: TextStyle(fontSize: 12, color: _cyan, fontWeight: FontWeight.w600)), ]), ), ], if (item.alamatLokasi != null) ...[ SizedBox(height: 10), Container( padding: EdgeInsets.all(12), 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, size: 14, color: _green), SizedBox(width: 8), Expanded(child: Text(item.alamatLokasi!, style: TextStyle(fontSize: 13, color: _t1, fontWeight: FontWeight.w600))), ]), if (item.namaPelanggan != null || item.noSambungan != null) ...[ SizedBox(height: 8), Row(children: [ Icon(Icons.person_outline, size: 14, color: _t2), SizedBox(width: 8), Text(item.namaPelanggan ?? '-', style: TextStyle(fontSize: 12, color: _t2)), SizedBox(width: 12), Icon(Icons.tag, size: 14, color: _t2), SizedBox(width: 4), Text(item.noSambungan ?? '-', style: TextStyle(fontSize: 12, color: _t2)), ]), ], ]), ), ], if (item.catatanAdmin != null) ...[ SizedBox(height: 10), Container( padding: EdgeInsets.all(12), decoration: BoxDecoration(color: _amberDim, borderRadius: BorderRadius.circular(10), border: Border.all(color: _amber.withOpacity(0.3))), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.info_outline, size: 16, color: _amber), SizedBox(width: 8), Expanded(child: Text(item.catatanAdmin!, style: TextStyle(fontSize: 12, color: _amber, height: 1.4), maxLines: 2, overflow: TextOverflow.ellipsis)), ]), ), ], SizedBox(height: 14), Divider(height: 1, color: _line2), SizedBox(height: 14), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () => _terimaTugas(item), icon: Icon(Icons.check_rounded, size: 18), label: Text('TERIMA TUGAS', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w800, letterSpacing: 0.8)), style: ElevatedButton.styleFrom( backgroundColor: _green, foregroundColor: Colors.black, elevation: 0, padding: EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ), ]), ), ]), ), ), ), ); } Widget _labelChip(String label, Color color) { return Container( padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration(color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.3))), child: Text(label, style: TextStyle(fontSize: 10, color: color, fontWeight: FontWeight.w700, letterSpacing: 0.5)), ); } Widget _actionButton(String label, IconData icon, Color color, VoidCallback onTap) { return ElevatedButton.icon( onPressed: onTap, icon: Icon(icon, size: 16), label: Text(label, style: TextStyle(fontWeight: FontWeight.w700, fontSize: 13)), style: ElevatedButton.styleFrom( backgroundColor: color.withOpacity(0.15), foregroundColor: color, shadowColor: Colors.transparent, side: BorderSide(color: color.withOpacity(0.5)), padding: EdgeInsets.symmetric(horizontal: 14, vertical: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), ); } Widget _buildStatusBadge(String status) { Color bg, text; String label; IconData icon; switch (status) { case 'belum_mulai': bg = _bg2; text = _t2; label = 'Belum Mulai'; icon = Icons.schedule; break; case 'dalam_proses': bg = _amberDim; text = _amber; label = 'Dalam Proses'; icon = Icons.pending_actions; break; case 'selesai': bg = _greenDim; text = _green; label = 'Selesai'; icon = Icons.check_circle_outline; break; default: bg = _bg2; text = _t3; label = status; icon = Icons.help_outline; } return Container( padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(12), border: Border.all(color: text.withOpacity(0.3))), child: Row(mainAxisSize: MainAxisSize.min, children: [Icon(icon, size: 12, color: text), SizedBox(width: 5), Text(label, style: TextStyle(fontSize: 10, fontWeight: FontWeight.w700, color: text, letterSpacing: 0.5))]), ); } void _toast(String msg, {bool isError = false}) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(msg, style: TextStyle(color: _t1)), backgroundColor: _bg1, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10), side: BorderSide(color: (isError ? _rose : _green).withOpacity(0.5))), behavior: SnackBarBehavior.floating, )); } Future _terimaTugas(PenugasanModel item) async { setState(() => isLoading = true); try { await _api.updateStatus( idPenugasan: item.idPenugasan, statusPekerjaan: 'dalam_proses', tanggalDiselesaikan: null, ); _loadData(); _toast('Tugas berhasil diterima! Cek menu Progress.'); } catch (e) { setState(() => isLoading = false); _toast('Gagal: $e', isError: true); } } void _showFormIsiDetail(PenugasanModel item) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => _FormIsiDetail(item: item, api: _api, onSuccess: () { Navigator.pop(context); _loadData(); _toast('Detail berhasil diisi!'); }, onFail: (err) => _toast(err, isError: true)), ); } void _showFormUpdateProgres(PenugasanModel item) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => _FormUpdateProgres(item: item, api: _api, onSuccess: () { Navigator.pop(context); _loadData(); _toast('Progres berhasil diupdate!'); }, onFail: (err) => _toast(err, isError: true)), ); } void _showDetail(PenugasanModel item) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => DraggableScrollableSheet( expand: false, initialChildSize: 0.75, maxChildSize: 0.95, builder: (_, controller) => Container( decoration: BoxDecoration( color: _bg1, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), border: Border(top: BorderSide(color: _line2)) ), child: SingleChildScrollView( controller: controller, 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: 24), Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Detail Tugas #${item.idPenugasan}', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: _t1)), _buildStatusBadge(item.statusPekerjaan), ]), SizedBox(height: 24), if (item.fotoSuratUrl != null) ...[ Text('FOTO SURAT TUGAS', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w800, color: _t3, letterSpacing: 1.2)), SizedBox(height: 10), ClipRRect(borderRadius: BorderRadius.circular(12), child: Image.network(item.fotoSuratUrl!, width: double.infinity, fit: BoxFit.cover, loadingBuilder: (c, child, p) => p == null ? child : Container(height: 180, color: _bg2, child: Center(child: CircularProgressIndicator(color: _green))), errorBuilder: (_, __, ___) => Container(height: 100, color: _bg2, child: Center(child: Icon(Icons.image_not_supported_outlined, color: _t3))))), SizedBox(height: 24), ], _detailRow('Teknisi', item.namaTim), _detailRow('Tanggal Tugas', DateFormat('dd MMMM yyyy').format(DateTime.parse(item.tanggalDiberikan).toLocal())), if (item.alamatLokasi != null) _detailRow('Alamat Lokasi', item.alamatLokasi!, isHighlight: true, valColor: _green), if (item.namaPelanggan != null) _detailRow('Nama Pelanggan', item.namaPelanggan!), if (item.noSambungan != null) _detailRow('No. Sambungan', item.noSambungan!), if (item.catatanAdmin != null) _detailRow('Instruksi Tambahan', item.catatanAdmin!), if (item.isDetailLengkap) ...[ SizedBox(height: 10), Divider(height: 1, color: _line2), SizedBox(height: 16), Text('DETAIL PEKERJAAN', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w800, color: _cyan, letterSpacing: 1.2)), SizedBox(height: 12), _detailRow('Jenis Pekerjaan', item.labelJenisPekerjaanFallback), if (item.dimensiPipa != null) _detailRow('Dimensi Pipa', 'Dim ${item.dimensiPipa}'), if (item.jarakMeter != null) _detailRow('Jarak', '${item.jarakMeter} meter'), if (item.jumlahUnit != null) _detailRow('Jumlah Unit', '${item.jumlahUnit} unit'), if (item.jumlahTitik != null) _detailRow('Jumlah Titik', '${item.jumlahTitik} titik'), if (item.pakaiPipaBesi != null) _detailRow('Pakai Pipa Besi', item.pakaiPipaBesi! ? 'Ya' : 'Tidak'), if (item.jenisPengangkatan != null) _detailRow('Jenis Pengangkatan', item.jenisPengangkatan == 'gate_valve' ? 'Gate Valve' : 'Meteran Air'), if (item.detailPekerjaan != null) _detailRow('Catatan Teknisi', item.detailPekerjaan!), if (item.tanggalMulai != null) _detailRow('Tanggal Mulai', item.tanggalMulai!), if (item.tanggalDiselesaikan != null) _detailRow('Waktu Selesai', item.tanggalDiselesaikan!), ], if (item.fotoSebelumUrl != null || item.fotoSesudahUrl != null) ...[ SizedBox(height: 10), Divider(height: 1, 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 (item.fotoSebelumUrl != 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(item.fotoSebelumUrl!, height: 100, fit: BoxFit.cover, width: double.infinity, errorBuilder: (_, __, ___) => Container(height: 100, color: _bg2, child: Center(child: Icon(Icons.image_not_supported_outlined, color: _t3))))), ])), if (item.fotoSebelumUrl != null && item.fotoSesudahUrl != null) SizedBox(width: 16), if (item.fotoSesudahUrl != 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(item.fotoSesudahUrl!, height: 100, fit: BoxFit.cover, width: double.infinity, errorBuilder: (_, __, ___) => Container(height: 100, color: _bg2, child: Center(child: Icon(Icons.image_not_supported_outlined, color: _t3))))), ])), ]), ], SizedBox(height: 32), if (item.statusPekerjaan == 'belum_mulai') SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () { Navigator.pop(context); _terimaTugas(item); }, icon: Icon(Icons.check_rounded, size: 18), label: Text('TERIMA TUGAS', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, letterSpacing: 0.8)), style: ElevatedButton.styleFrom( backgroundColor: _green, foregroundColor: Colors.black, elevation: 0, padding: EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ), SizedBox(height: 20), ]), ), ), ), ); } Widget _detailRow(String label, String value, {bool isHighlight = false, Color valColor = _t1}) { return Padding( padding: EdgeInsets.only(bottom: 12), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(width: 140, child: Text(label, style: TextStyle(fontSize: 13, color: _t2))), Expanded(child: Container( padding: isHighlight ? EdgeInsets.all(10) : null, decoration: isHighlight ? BoxDecoration(color: _bg2, borderRadius: BorderRadius.circular(8), border: Border.all(color: _line2)) : null, child: Text(value, style: TextStyle(fontSize: isHighlight ? 13 : 13.5, fontWeight: isHighlight ? FontWeight.w500 : FontWeight.w600, color: valColor, height: 1.4)) )), ]), ); } ButtonStyle _btnStyle(Color c) => ElevatedButton.styleFrom( backgroundColor: c, foregroundColor: Colors.black, elevation: 0, padding: EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ); } // =================================== // FORM ISI DETAIL // =================================== class _FormIsiDetail extends StatefulWidget { final PenugasanModel item; final PenugasanApi api; final VoidCallback onSuccess; final Function(String) onFail; const _FormIsiDetail({required this.item, required this.api, required this.onSuccess, required this.onFail}); @override __FormIsiDetailState createState() => __FormIsiDetailState(); } class __FormIsiDetailState extends State<_FormIsiDetail> { final _formKey = GlobalKey(); bool _isLoading = false; String? _tanggalMulai; final _detailController = TextEditingController(); XFile? _fotoSebelum; final _picker = ImagePicker(); // List item pekerjaan List> _items = [ { 'jenis_pekerjaan': null, 'dimensi_pipa': null, 'jarak_meter': TextEditingController(), 'jumlah_unit': TextEditingController(), 'jumlah_titik': TextEditingController(), 'pakai_pipa_besi': false, 'jenis_pengangkatan': null, } ]; final List> _jenisList = [ {'value': 'sr', 'label': 'SR (Sambungan Rumah)'}, {'value': 'pengembangan_jaringan_pipa', 'label': 'Pengembangan Jaringan Pipa'}, {'value': 'pengangkatan', 'label': 'Pengangkatan'}, {'value': 'pemasangan_gate_valve', 'label': 'Pemasangan Gate Valve'}, {'value': 'gali_urug', 'label': 'Gali Urug'}, {'value': 'perbaikan_jaringan_pipa', 'label': 'Perbaikan Jaringan Pipa'}, {'value': 'pengecatan_pipa_besi', 'label': 'Pengecatan Pipa Besi'}, {'value': 'penyempurnaan_jaringan_pipa', 'label': 'Penyempurnaan Jaringan Pipa'}, ]; @override void dispose() { _detailController.dispose(); for (var it in _items) { (it['jarak_meter'] as TextEditingController).dispose(); (it['jumlah_unit'] as TextEditingController).dispose(); (it['jumlah_titik'] as TextEditingController).dispose(); } super.dispose(); } void _addItem() { setState(() { _items.add({ 'jenis_pekerjaan': null, 'dimensi_pipa': null, 'jarak_meter': TextEditingController(), 'jumlah_unit': TextEditingController(), 'jumlah_titik': TextEditingController(), 'pakai_pipa_besi': false, 'jenis_pengangkatan': null, }); }); } void _removeItem(int index) { if (_items.length > 1) { setState(() { ( _items[index]['jarak_meter'] as TextEditingController).dispose(); ( _items[index]['jumlah_unit'] as TextEditingController).dispose(); ( _items[index]['jumlah_titik'] as TextEditingController).dispose(); _items.removeAt(index); }); } } Future _pickFoto() async { final picked = await _picker.pickImage(source: ImageSource.camera, imageQuality: 70); if (picked != null) setState(() => _fotoSebelum = picked); } Future _pickTanggal() async { final now = DateTime.now(); final picked = await showDatePicker(context: context, initialDate: now, firstDate: DateTime(now.year - 1), lastDate: now, builder: (ctx, child) => Theme(data: ThemeData.light().copyWith( colorScheme: ColorScheme.light(primary: _green, onPrimary: Colors.black, surface: _bg1, onSurface: _t1), dialogBackgroundColor: _bg1, ), child: child!)); if (picked != null) setState(() => _tanggalMulai = DateFormat('yyyy-MM-dd').format(picked)); } Future _submit() async { if (!_formKey.currentState!.validate()) return; if (_tanggalMulai == null) { widget.onFail('Pilih tanggal mulai'); return; } if (_fotoSebelum == null && !kIsWeb) { widget.onFail('Foto sebelum pekerjaan wajib diisi'); return; } setState(() => _isLoading = true); try { // Persiapkan list items untuk dikirim final List> itemsToSubmit = _items.map((it) { return { 'jenis_pekerjaan': it['jenis_pekerjaan'], 'dimensi_pipa': it['dimensi_pipa'], 'jarak_meter': (it['jarak_meter'] as TextEditingController).text.isNotEmpty ? double.tryParse((it['jarak_meter'] as TextEditingController).text) : null, 'jumlah_unit': (it['jumlah_unit'] as TextEditingController).text.isNotEmpty ? int.tryParse((it['jumlah_unit'] as TextEditingController).text) : null, 'jumlah_titik': (it['jumlah_titik'] as TextEditingController).text.isNotEmpty ? int.tryParse((it['jumlah_titik'] as TextEditingController).text) : null, 'pakai_pipa_besi': it['pakai_pipa_besi'], 'jenis_pengangkatan': it['jenis_pengangkatan'], }; }).toList(); await widget.api.lengkapiDetail( idPenugasan: widget.item.idPenugasan, items: itemsToSubmit, tanggalMulai: _tanggalMulai!, detailPekerjaan: _detailController.text.isNotEmpty ? _detailController.text : null, fotoSebelum: _fotoSebelum, ); widget.onSuccess(); } on PenugasanApiException catch (e) { setState(() => _isLoading = false); widget.onFail(e.getFirstError() ?? e.message); } catch (e) { setState(() => _isLoading = false); widget.onFail('Gagal: $e'); } } @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: DraggableScrollableSheet( expand: false, initialChildSize: 0.85, maxChildSize: 0.95, builder: (_, controller) => Container( decoration: BoxDecoration(color: _bg1, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), border: Border(top: BorderSide(color: _line2))), child: SingleChildScrollView( controller: controller, padding: EdgeInsets.all(24), child: Form(key: _formKey, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Center(child: Container(width: 48, height: 5, decoration: BoxDecoration(color: _bg2, borderRadius: BorderRadius.circular(3)))), SizedBox(height: 24), Text('Isi Detail Pekerjaan', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: _t1)), Text('Penugasan #${widget.item.idPenugasan}', style: TextStyle(fontSize: 13, color: _t3)), SizedBox(height: 24), _label('Tanggal Mulai *'), GestureDetector( onTap: _pickTanggal, child: Container( padding: EdgeInsets.symmetric(horizontal: 14, vertical: 14), decoration: BoxDecoration(color: _bg2, border: Border.all(color: _line2), borderRadius: BorderRadius.circular(10)), child: Row(children: [ Icon(Icons.calendar_today_rounded, size: 18, color: _t2), SizedBox(width: 10), Text(_tanggalMulai ?? 'Pilih tanggal mulai', style: TextStyle(fontSize: 14, color: _tanggalMulai != null ? _t1 : _t3)), ]), ), ), SizedBox(height: 24), // LOOP ITEMS ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), itemCount: _items.length, itemBuilder: (ctx, index) => _buildItemForm(index), ), Center( child: TextButton.icon( onPressed: _addItem, icon: Icon(Icons.add_circle_outline, color: _cyan), label: Text('Tambah Jenis Pekerjaan Lain', style: TextStyle(color: _cyan, fontWeight: FontWeight.w700)), ), ), SizedBox(height: 24), _label('Catatan Teknisi (Opsional)'), TextFormField(controller: _detailController, maxLines: 2, style: TextStyle(color: _t1, fontSize: 14), decoration: _inputDecor(hint: 'Catatan tambahan terkait lapangan...')), SizedBox(height: 16), _label('Foto Sebelum Pekerjaan *'), GestureDetector( onTap: _pickFoto, child: Container( height: 120, decoration: BoxDecoration(color: _bg2, borderRadius: BorderRadius.circular(10), border: Border.all(color: _line2)), child: _fotoSebelum != null ? kIsWeb ? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(Icons.check_circle, color: _green, size: 36), SizedBox(height: 8), Text('Foto dipilih ✓', style: TextStyle(color: _green, fontWeight: FontWeight.w600))])) : ClipRRect(borderRadius: BorderRadius.circular(10), child: Image.file(File(_fotoSebelum!.path), fit: BoxFit.cover, width: double.infinity)) : Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Container(padding: EdgeInsets.all(10), decoration: BoxDecoration(color: _cyanDim, shape: BoxShape.circle), child: Icon(Icons.camera_alt_outlined, color: _cyan, size: 24)), SizedBox(height: 10), Text('Ambil Foto Sebelum', style: TextStyle(color: _t2, fontSize: 13, fontWeight: FontWeight.w500)) ])), ), ), SizedBox(height: 32), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isLoading ? null : _submit, style: ElevatedButton.styleFrom(backgroundColor: _green, foregroundColor: Colors.black, padding: EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), child: _isLoading ? SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.black, strokeWidth: 2)) : Text('KIRIM & SIMPAN', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, letterSpacing: 1.2)), ), ), SizedBox(height: 20), ])), ), ), ), ); } Widget _buildItemForm(int index) { final item = _items[index]; final jenisPek = item['jenis_pekerjaan']; return Container( margin: EdgeInsets.only(bottom: 24), padding: EdgeInsets.all(16), decoration: BoxDecoration( color: _bg, borderRadius: BorderRadius.circular(16), border: Border.all(color: _line2), ), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('PEKERJAAN #${index + 1}', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w800, color: _cyan, letterSpacing: 1)), if (_items.length > 1) IconButton( onPressed: () => _removeItem(index), icon: Icon(Icons.delete_outline, color: _rose, size: 20), visualDensity: VisualDensity.compact, ), ]), SizedBox(height: 12), _label('Jenis Pekerjaan *'), DropdownButtonFormField( value: jenisPek, hint: Text('Pilih jenis pekerjaan', style: TextStyle(color: _t3)), dropdownColor: _bg2, style: TextStyle(color: _t1, fontSize: 13), decoration: _inputDecor(), iconEnabledColor: _t2, items: _jenisList.map((j) => DropdownMenuItem(value: j['value'], child: Text(j['label']!))).toList(), onChanged: (val) { setState(() { _items[index]['jenis_pekerjaan'] = val; }); }, validator: (v) => v == null ? 'Wajib' : null, ), SizedBox(height: 16), if (jenisPek != null) ...[ if (['sr','pengembangan_jaringan_pipa','pemasangan_gate_valve','perbaikan_jaringan_pipa','pengecatan_pipa_besi','penyempurnaan_jaringan_pipa'].contains(jenisPek)) ...[ _label('Dimensi Pipa'), DropdownButtonFormField( value: item['dimensi_pipa'], hint: Text('Pilih dimensi', style: TextStyle(color: _t3)), dropdownColor: _bg2, style: TextStyle(color: _t1, fontSize: 13), decoration: _inputDecor(), iconEnabledColor: _t2, items: ['1-2','3','4','6','8','10','12'].map((d) => DropdownMenuItem(value: d, child: Text('Dim $d inch'))).toList(), onChanged: (val) => setState(() => _items[index]['dimensi_pipa'] = val), ), SizedBox(height: 16), ], if (['pengembangan_jaringan_pipa','penyempurnaan_jaringan_pipa'].contains(jenisPek)) ...[ _label('Jarak (meter)'), TextFormField(controller: item['jarak_meter'], keyboardType: TextInputType.numberWithOptions(decimal: true), style: TextStyle(color: _t1, fontSize: 13), decoration: _inputDecor(hint: 'Contoh: 50.5', suffix: 'm')), SizedBox(height: 16), ], if (['sr','pengangkatan'].contains(jenisPek)) ...[ _label('Jumlah Unit'), TextFormField(controller: item['jumlah_unit'], keyboardType: TextInputType.number, style: TextStyle(color: _t1, fontSize: 13), decoration: _inputDecor(hint: 'Contoh: 2', suffix: 'unit')), SizedBox(height: 16), ], if (jenisPek == 'gali_urug') ...[ _label('Jumlah Titik'), TextFormField(controller: item['jumlah_titik'], keyboardType: TextInputType.number, style: TextStyle(color: _t1, fontSize: 13), decoration: _inputDecor(hint: 'Contoh: 3', suffix: 'titik')), SizedBox(height: 16), ], if (jenisPek == 'pemasangan_gate_valve') ...[ Theme( data: ThemeData(unselectedWidgetColor: _t3), child: CheckboxListTile( value: item['pakai_pipa_besi'], onChanged: (v) => setState(() => _items[index]['pakai_pipa_besi'] = v ?? false), title: Text('Pakai Pipa Besi (+Rp 200rb)', style: TextStyle(fontSize: 12, color: _t1)), activeColor: _green, checkColor: Colors.black, contentPadding: EdgeInsets.zero, ), ), ], if (jenisPek == 'pengangkatan') ...[ _label('Jenis Pengangkatan'), DropdownButtonFormField( value: item['jenis_pengangkatan'], hint: Text('Pilih jenis', style: TextStyle(color: _t3)), dropdownColor: _bg2, style: TextStyle(color: _t1, fontSize: 13), decoration: _inputDecor(), iconEnabledColor: _t2, items: [DropdownMenuItem(value: 'meteran', child: Text('Meteran Air')), DropdownMenuItem(value: 'gate_valve', child: Text('Gate Valve'))], onChanged: (val) => setState(() => _items[index]['jenis_pengangkatan'] = val), ), SizedBox(height: 16), ], ], ]), ); } Widget _label(String text) => Padding(padding: EdgeInsets.only(bottom: 8), child: Text(text, style: TextStyle(fontSize: 11, fontWeight: FontWeight.w800, color: _t2, letterSpacing: 0.8))); InputDecoration _inputDecor({String? hint, String? suffix}) => InputDecoration( hintText: hint, hintStyle: TextStyle(color: _t3), suffixText: suffix, suffixStyle: TextStyle(color: _t2), filled: true, fillColor: _bg2, border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: _line2)), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: _green, width: 1.5)), contentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 14), ); } // =================================== // FORM UPDATE PROGRES // =================================== class _FormUpdateProgres extends StatefulWidget { final PenugasanModel item; final PenugasanApi api; final VoidCallback onSuccess; final Function(String) onFail; const _FormUpdateProgres({required this.item, required this.api, required this.onSuccess, required this.onFail}); @override __FormUpdateProgresState createState() => __FormUpdateProgresState(); } class __FormUpdateProgresState extends State<_FormUpdateProgres> { bool _isLoading = false; bool _tandaiSelesai = false; XFile? _fotoSesudah; final _picker = ImagePicker(); Future _pickFoto() async { final picked = await _picker.pickImage(source: ImageSource.camera, imageQuality: 70); if (picked != null) setState(() => _fotoSesudah = picked); } Future _submit() async { setState(() => _isLoading = true); try { if (_fotoSesudah != null) { await widget.api.uploadFoto(idPenugasan: widget.item.idPenugasan, tipeFoto: 'sesudah', foto: _fotoSesudah!); } else if (_tandaiSelesai) { // Jika mau selesai, harus ada foto throw PenugasanApiException(message: "Foto sesudah pekerjaan wajib diisi sebelum selesai."); } if (_tandaiSelesai) { await widget.api.updateStatus( idPenugasan: widget.item.idPenugasan, statusPekerjaan: 'selesai', tanggalDiselesaikan: DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()), ); } else if (_fotoSesudah == null) { throw PenugasanApiException(message: "Tidak ada aksi yang dilakukan."); } widget.onSuccess(); } on PenugasanApiException catch (e) { setState(() => _isLoading = false); widget.onFail(e.message); } catch (e) { setState(() => _isLoading = false); widget.onFail('Gagal: $e'); } } @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: DraggableScrollableSheet( expand: false, initialChildSize: 0.65, maxChildSize: 0.9, builder: (_, controller) => Container( decoration: BoxDecoration(color: _bg1, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), border: Border(top: BorderSide(color: _line2))), child: SingleChildScrollView( controller: controller, 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: 24), Text('Update Progres Pekerjaan', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: _t1)), Text('${widget.item.labelJenisPekerjaanFallback} • ${widget.item.totalNilaiFormatted}', style: TextStyle(fontSize: 13, color: _t3)), SizedBox(height: 24), Text('FOTO HASIL PEKERJAAN', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w800, color: _t2, letterSpacing: 0.8)), SizedBox(height: 10), GestureDetector( onTap: _pickFoto, child: Container( height: 140, decoration: BoxDecoration(color: _bg2, borderRadius: BorderRadius.circular(10), border: Border.all(color: _line2)), child: _fotoSesudah != null ? kIsWeb ? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [Icon(Icons.check_circle, color: _cyan, size: 36), SizedBox(height: 8), Text('Foto dipilih ✓', style: TextStyle(color: _cyan, fontWeight: FontWeight.w600))])) : ClipRRect(borderRadius: BorderRadius.circular(10), child: Image.file(File(_fotoSesudah!.path), fit: BoxFit.cover, width: double.infinity)) : Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Container(padding: EdgeInsets.all(10), decoration: BoxDecoration(color: _greenDim, shape: BoxShape.circle), child: Icon(Icons.camera_alt_outlined, color: _green, size: 28)), SizedBox(height: 10), Text('Ambil Foto Hasil', style: TextStyle(color: _t2, fontSize: 13, fontWeight: FontWeight.w500)) ])), ), ), SizedBox(height: 24), AnimatedContainer( duration: Duration(milliseconds: 200), decoration: BoxDecoration( color: _tandaiSelesai ? _greenDim : _bg2, borderRadius: BorderRadius.circular(12), border: Border.all(color: _tandaiSelesai ? _green.withOpacity(0.4) : _line2), ), child: Theme( data: ThemeData(unselectedWidgetColor: _t3), child: CheckboxListTile( value: _tandaiSelesai, onChanged: (v) => setState(() => _tandaiSelesai = v ?? false), title: Text('Tandai Pekerjaan Selesai', style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14, color: _tandaiSelesai ? _green : _t1)), subtitle: Text('Centang jika pekerjaan sudah selesai', style: TextStyle(fontSize: 12, color: _tandaiSelesai ? _green.withOpacity(0.8) : _t2)), activeColor: _green, checkColor: Colors.black, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), ), ), SizedBox(height: 32), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isLoading ? null : _submit, style: ElevatedButton.styleFrom( backgroundColor: _tandaiSelesai ? _green : _cyan, foregroundColor: Colors.black, padding: EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), child: _isLoading ? SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.black, strokeWidth: 2)) : Text(_tandaiSelesai ? 'SIMPAN & TANDAI SELESAI' : 'SIMPAN FOTO', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, letterSpacing: 1.2)), ), ), SizedBox(height: 20), ]), ), ), ), ); } }