953 lines
46 KiB
Dart
953 lines
46 KiB
Dart
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<PenugasanScreen> {
|
|
final _api = PenugasanApi();
|
|
|
|
List<PenugasanModel> filteredList = [];
|
|
StatistikModel? statistik;
|
|
bool isLoading = false;
|
|
String? errorMessage;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadData();
|
|
}
|
|
|
|
Future<void> _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<void> _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<FormState>();
|
|
bool _isLoading = false;
|
|
String? _tanggalMulai;
|
|
final _detailController = TextEditingController();
|
|
XFile? _fotoSebelum;
|
|
final _picker = ImagePicker();
|
|
|
|
// List item pekerjaan
|
|
List<Map<String, dynamic>> _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<Map<String, String>> _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<void> _pickFoto() async {
|
|
final picked = await _picker.pickImage(source: ImageSource.camera, imageQuality: 70);
|
|
if (picked != null) setState(() => _fotoSebelum = picked);
|
|
}
|
|
|
|
Future<void> _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<void> _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<Map<String, dynamic>> 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<String>(
|
|
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<String>(
|
|
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<String>(
|
|
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<void> _pickFoto() async {
|
|
final picked = await _picker.pickImage(source: ImageSource.camera, imageQuality: 70);
|
|
if (picked != null) setState(() => _fotoSesudah = picked);
|
|
}
|
|
|
|
Future<void> _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),
|
|
]),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |