1679 lines
68 KiB
Dart
1679 lines
68 KiB
Dart
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<ProgressScreen>
|
|
with SingleTickerProviderStateMixin {
|
|
final _api = PenugasanApi();
|
|
List<PenugasanModel> activeList = [];
|
|
List<PenugasanModel> 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<void> _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<List<PenugasanModel>> _fetchAllPages({required String status}) async {
|
|
final List<PenugasanModel> 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<PenugasanModel> 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<void> _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<void> _pickFoto() async {
|
|
final picked =
|
|
await _picker.pickImage(source: ImageSource.camera, imageQuality: 70);
|
|
if (picked != null) setState(() => _foto = picked);
|
|
}
|
|
|
|
Future<void> _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<DetailPenugasanSheet> {
|
|
bool _isLoading = false;
|
|
bool _isEditing = false;
|
|
Map<String, dynamic>? _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<void> _loadDetail() async {
|
|
setState(() => _isLoading = true);
|
|
try {
|
|
final result =
|
|
await widget.api.getPenugasanDetail(widget.penugasan.idPenugasan);
|
|
final detail = result['data'] as Map<String, dynamic>?;
|
|
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<String, dynamic>? firstItem =
|
|
(itemsList != null && itemsList.isNotEmpty)
|
|
? Map<String, dynamic>.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<void> _submitEdit() async {
|
|
setState(() => _isLoading = true);
|
|
try {
|
|
final List? itemsList = _detail!['items'] as List?;
|
|
final Map<String, dynamic>? firstItem =
|
|
(itemsList != null && itemsList.isNotEmpty)
|
|
? Map<String, dynamic>.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<String, dynamic>.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,
|
|
),
|
|
);
|
|
}
|
|
} |