SIPDAM/samooflutter/lib/tugas/Progress.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,
),
);
}
}