MIF_E31222656/lib/screens/calendar/add_schedule_dialog.dart

1014 lines
35 KiB
Dart

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:uuid/uuid.dart';
import 'dart:async';
import 'package:flutter/services.dart';
class AddScheduleDialog extends StatefulWidget {
final Function(Map<String, dynamic>)? onScheduleAdded;
final List<Map<String, dynamic>> existingSchedules;
final DateTime? initialStartDate;
final Map<String, dynamic>? scheduleToEdit;
const AddScheduleDialog({
super.key,
this.onScheduleAdded,
required this.existingSchedules,
this.initialStartDate,
this.scheduleToEdit,
});
@override
State<AddScheduleDialog> createState() => _AddScheduleDialogState();
}
class _AddScheduleDialogState extends State<AddScheduleDialog> {
final _formKey = GlobalKey<FormState>();
// Track validation state for each field
bool _seedCostValid = false;
bool _fertilizerCostValid = false;
bool _pesticideCostValid = false;
bool _irrigationCostValid = false;
bool _expectedYieldValid = false;
final _cropNameController = TextEditingController();
final _notesController = TextEditingController();
final _seedCostController = TextEditingController();
final _fertilizerCostController = TextEditingController();
final _pesticideCostController = TextEditingController();
final _irrigationCostController = TextEditingController();
final _expectedYieldController = TextEditingController();
// Tambahkan ScrollController untuk auto-scroll
final _scrollController = ScrollController();
DateTime _startDate = DateTime.now();
DateTime _endDate = DateTime.now().add(const Duration(days: 90));
String? _selectedFieldId;
int? _selectedPlot;
Map<String, dynamic>? _selectedFieldData;
List<Map<String, dynamic>> _fields = [];
bool _isLoading = false;
bool _isLoadingFields = true;
bool _isSaved = false;
bool _isEditMode = false;
final List<String> _cropOptions = [
'Padi',
'Jagung',
'Kedelai',
'Cabai',
'Tomat',
'Bawang',
'Lainnya',
];
// FocusNode untuk mengelola fokus keyboard
final _cropNameFocus = FocusNode();
final _notesFocus = FocusNode();
final _seedCostFocus = FocusNode();
final _fertilizerCostFocus = FocusNode();
final _pesticideCostFocus = FocusNode();
final _irrigationCostFocus = FocusNode();
final _expectedYieldFocus = FocusNode();
@override
void initState() {
super.initState();
_isEditMode = widget.scheduleToEdit != null;
if (_isEditMode) {
_initializeWithExistingData();
// Check if fields already have values when editing
Future.delayed(Duration.zero, () {
setState(() {
_seedCostValid = _seedCostController.text.isNotEmpty;
_fertilizerCostValid = _fertilizerCostController.text.isNotEmpty;
_pesticideCostValid = _pesticideCostController.text.isNotEmpty;
_irrigationCostValid = _irrigationCostController.text.isNotEmpty;
_expectedYieldValid = _expectedYieldController.text.isNotEmpty;
});
});
} else {
if (widget.initialStartDate != null) {
_startDate = widget.initialStartDate!;
_endDate = _startDate.add(const Duration(days: 90));
}
// Set default crop name, but leave costs empty
_cropNameController.text = 'Padi';
}
_loadFields();
// Konfigurasi focus nodes untuk auto-scroll saat keyboard muncul
_setupFocusNodes();
}
void _setupFocusNodes() {
// Fungsi untuk scroll ke field yang sedang difokuskan
void scrollToFocusedField(FocusNode focusNode) {
// Hapus kode yang mungkin mengganggu keyboard
}
// Tambahkan listener ke setiap focus node tanpa implementasi yang mengganggu keyboard
_seedCostFocus.addListener(() {});
_fertilizerCostFocus.addListener(() {});
_pesticideCostFocus.addListener(() {});
_irrigationCostFocus.addListener(() {});
_expectedYieldFocus.addListener(() {});
_notesFocus.addListener(() {});
}
@override
void dispose() {
// Dispose semua controller
_cropNameController.dispose();
_notesController.dispose();
_seedCostController.dispose();
_fertilizerCostController.dispose();
_pesticideCostController.dispose();
_irrigationCostController.dispose();
_expectedYieldController.dispose();
// Dispose semua focus node
_cropNameFocus.dispose();
_notesFocus.dispose();
_seedCostFocus.dispose();
_fertilizerCostFocus.dispose();
_pesticideCostFocus.dispose();
_irrigationCostFocus.dispose();
_expectedYieldFocus.dispose();
super.dispose();
}
String _formatCostForDisplay(dynamic cost) {
if (cost == null) return '';
String costStr = cost.toString();
// Remove .0 for whole numbers
if (costStr.endsWith('.0')) {
return costStr.substring(0, costStr.length - 2);
}
return costStr;
}
void _initializeWithExistingData() {
try {
final schedule = widget.scheduleToEdit!;
_cropNameController.text = schedule['crop_name'] ?? '';
_notesController.text = schedule['notes'] ?? '';
// Use the formatter to hide zero values
_seedCostController.text = _formatCostForDisplay(schedule['seed_cost']);
_fertilizerCostController.text = _formatCostForDisplay(
schedule['fertilizer_cost'],
);
_pesticideCostController.text = _formatCostForDisplay(
schedule['pesticide_cost'],
);
_irrigationCostController.text = _formatCostForDisplay(
schedule['irrigation_cost'],
);
_expectedYieldController.text = _formatCostForDisplay(
schedule['expected_yield'],
);
if (schedule['start_date'] != null) {
_startDate = DateTime.parse(schedule['start_date']);
}
if (schedule['end_date'] != null) {
_endDate = DateTime.parse(schedule['end_date']);
}
_selectedFieldId = schedule['field_id'];
_selectedPlot = schedule['plot'];
} catch (e) {
debugPrint('Error initializing data: $e');
}
}
Future<void> _loadFields() async {
if (!mounted) return;
setState(() => _isLoadingFields = true);
try {
final response = await Supabase.instance.client
.from('fields')
.select('id, name, plot_count')
.order('name', ascending: true)
.timeout(const Duration(seconds: 8));
if (!mounted) return;
setState(() {
_fields = List<Map<String, dynamic>>.from(response);
if (_isEditMode && _selectedFieldId != null) {
_selectedFieldData = _fields.firstWhere(
(field) => field['id'] == _selectedFieldId,
orElse: () => _fields.isNotEmpty ? _fields.first : {},
);
} else if (_fields.isNotEmpty) {
_selectedFieldId ??= _fields.first['id'];
_selectedFieldData = _fields.first;
}
_isLoadingFields = false;
});
} catch (e) {
if (!mounted) return;
setState(() => _isLoadingFields = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
e.toString().contains('timeout')
? 'Koneksi timeout.'
: 'Gagal memuat data lahan.',
),
backgroundColor: Colors.red,
),
);
}
}
List<int> _getTakenPlots(
Map<String, dynamic> fieldData,
DateTime startDate,
DateTime endDate,
) {
if (fieldData['id'] == null) return [];
final takenPlotsSet = <int>{};
for (var schedule in widget.existingSchedules) {
if (schedule['field_id'] != fieldData['id']) continue;
if (_isEditMode && schedule['id'] == widget.scheduleToEdit!['id'])
continue;
final scheduleStartDate = DateTime.tryParse(schedule['start_date'] ?? '');
final scheduleEndDate = DateTime.tryParse(schedule['end_date'] ?? '');
if (scheduleStartDate == null || scheduleEndDate == null) continue;
final overlap =
(startDate.isBefore(scheduleEndDate) ||
startDate.isAtSameMomentAs(scheduleEndDate)) &&
(endDate.isAfter(scheduleStartDate) ||
endDate.isAtSameMomentAs(scheduleStartDate));
if (overlap && schedule['plot'] is int) {
takenPlotsSet.add(schedule['plot'] as int);
}
}
return takenPlotsSet.toList();
}
Future<void> _pickDate(bool isStartDate) async {
final picked = await showDatePicker(
context: context,
initialDate: isStartDate ? _startDate : _endDate,
firstDate:
isStartDate
? DateTime.now().subtract(const Duration(days: 365))
: _startDate,
lastDate: DateTime.now().add(const Duration(days: 730)),
);
if (picked != null && mounted) {
setState(() {
if (isStartDate) {
_startDate = picked;
if (_endDate.isBefore(_startDate)) {
_endDate = _startDate.add(const Duration(days: 1));
}
} else {
_endDate = picked;
}
_selectedPlot = null;
});
}
}
double _safeParseDouble(String? text) {
if (text == null || text.trim().isEmpty) return 0.0;
try {
final cleanText = text.trim().replaceAll(',', '.');
return double.tryParse(cleanText) ?? 0.0;
} catch (e) {
return 0.0;
}
}
Future<void> _submit() async {
if (_isLoading || _isSaved) return;
if (!_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Mohon lengkapi data yang diperlukan.'),
backgroundColor: Colors.red,
),
);
return;
}
if (mounted) setState(() => _isLoading = true);
try {
final userId = Supabase.instance.client.auth.currentUser?.id;
if (userId == null) throw Exception('User tidak ditemukan.');
final data = {
'user_id': userId,
'crop_name': _cropNameController.text.trim(),
'start_date': _startDate.toIso8601String(),
'end_date': _endDate.toIso8601String(),
'field_id': _selectedFieldId,
'plot': _selectedPlot,
'notes':
_notesController.text.trim().isEmpty
? null
: _notesController.text.trim(),
'status': 'active',
'seed_cost': _safeParseDouble(_seedCostController.text),
'fertilizer_cost': _safeParseDouble(_fertilizerCostController.text),
'pesticide_cost': _safeParseDouble(_pesticideCostController.text),
'irrigation_cost': _safeParseDouble(_irrigationCostController.text),
'expected_yield': _safeParseDouble(_expectedYieldController.text),
};
if (!_isEditMode) {
data['id'] = const Uuid().v4();
data['created_at'] = DateTime.now().toIso8601String();
}
final scheduleId =
_isEditMode ? widget.scheduleToEdit!['id'] : data['id'];
if (_isEditMode) {
await Supabase.instance.client
.from('crop_schedules')
.update(data)
.eq('id', scheduleId);
} else {
await Supabase.instance.client.from('crop_schedules').insert(data);
}
if (!mounted) return;
setState(() => _isSaved = true);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Jadwal berhasil ${_isEditMode ? 'diperbarui' : 'disimpan'}.',
),
backgroundColor: Colors.green,
),
);
final newScheduleData =
await Supabase.instance.client
.from('crop_schedules')
.select()
.eq('id', scheduleId)
.single();
widget.onScheduleAdded?.call(newScheduleData);
await Future.delayed(const Duration(milliseconds: 300));
if (mounted) Navigator.of(context).pop(true);
} catch (e) {
if (mounted) setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal menyimpan jadwal: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}
@override
Widget build(BuildContext context) {
List<int> availablePlots = [];
if (_selectedFieldData != null &&
_selectedFieldData!['plot_count'] != null) {
final int plotCount = _selectedFieldData!['plot_count'] as int;
final takenPlots = _getTakenPlots(
_selectedFieldData!,
_startDate,
_endDate,
);
availablePlots =
List.generate(
plotCount,
(i) => i + 1,
).where((p) => !takenPlots.contains(p)).toList();
if (_selectedPlot != null && !availablePlots.contains(_selectedPlot)) {
_selectedPlot = null;
}
}
// Gunakan MediaQuery untuk mendapatkan ukuran keyboard
final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
return Material(
color: Colors.transparent,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header yang tetap di atas
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
_isEditMode ? 'Edit Jadwal Tanam' : 'Tambah Jadwal Tanam',
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
// Konten form yang dapat di-scroll
Flexible(
child: SingleChildScrollView(
controller: _scrollController,
padding: EdgeInsets.only(
bottom: keyboardHeight > 0 ? keyboardHeight + 80 : 20,
left: 20,
right: 20,
),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('Periode Tanam'),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildDateField(
'Tanggal Mulai',
_startDate,
() => _pickDate(true),
),
),
const SizedBox(width: 12),
Expanded(
child: _buildDateField(
'Tanggal Selesai',
_endDate,
() => _pickDate(false),
),
),
],
),
const SizedBox(height: 20),
_buildSectionTitle('Informasi Tanaman'),
const SizedBox(height: 12),
_buildDropdownField<String>(
value:
_cropOptions.contains(_cropNameController.text)
? _cropNameController.text
: 'Lainnya',
items: _cropOptions,
onChanged: (String? newValue) {
setState(() {
if (newValue != 'Lainnya') {
_cropNameController.text = newValue!;
} else {
_cropNameController.clear();
}
});
},
labelText: 'Nama Tanaman',
icon: Icons.eco,
),
if (!_cropOptions.contains(_cropNameController.text) ||
_cropNameController.text.isEmpty)
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: _buildTextField(
controller: _cropNameController,
labelText: 'Nama Tanaman (Lainnya)',
icon: Icons.eco,
validator:
(v) =>
v == null || v.isEmpty
? 'Nama tanaman harus diisi'
: null,
),
),
const SizedBox(height: 20),
_buildSectionTitle('Detail Lahan & Plot'),
const SizedBox(height: 12),
_isLoadingFields
? const Center(child: CircularProgressIndicator())
: _fields.isEmpty
? _buildEmptyFieldsWarning()
: Column(
children: [
_buildDropdownField<String>(
value: _selectedFieldId,
items: _fields,
onChanged: (String? value) {
setState(() {
_selectedFieldId = value;
_selectedFieldData = _fields.firstWhere(
(f) => f['id'] == value,
orElse: () => {},
);
_selectedPlot = null;
});
},
labelText: 'Pilih Lahan',
icon: Icons.landscape,
),
if (_selectedFieldData != null &&
_selectedFieldData!['plot_count'] > 0) ...[
const SizedBox(height: 16),
_buildDropdownField<int>(
value: _selectedPlot,
items: availablePlots,
onChanged:
(int? value) =>
setState(() => _selectedPlot = value),
labelText: 'Pilih Nomor Plot',
icon: Icons.format_list_numbered,
hint:
availablePlots.isEmpty
? 'Tidak ada plot tersedia'
: null,
),
],
],
),
const SizedBox(height: 20),
_buildSectionTitle('Estimasi Biaya *'),
const SizedBox(height: 4),
Text(
'Semua biaya harus diisi untuk analisis',
style: TextStyle(
fontSize: 12,
color: Colors.red.shade700,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
_buildCostTextField(
_seedCostController,
'Biaya Bibit (Rp)',
Icons.grass,
focusNode: _seedCostFocus,
isRequired: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Biaya bibit harus diisi';
}
return null;
},
),
const SizedBox(height: 16),
_buildCostTextField(
_fertilizerCostController,
'Biaya Pupuk (Rp)',
Icons.local_florist,
focusNode: _fertilizerCostFocus,
isRequired: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Biaya pupuk harus diisi';
}
return null;
},
),
const SizedBox(height: 16),
_buildCostTextField(
_pesticideCostController,
'Biaya Pestisida (Rp)',
Icons.bug_report,
focusNode: _pesticideCostFocus,
isRequired: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Biaya pestisida harus diisi';
}
return null;
},
),
const SizedBox(height: 16),
_buildCostTextField(
_irrigationCostController,
'Biaya Irigasi (Rp)',
Icons.water_drop,
focusNode: _irrigationCostFocus,
isRequired: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Biaya irigasi harus diisi';
}
return null;
},
),
const SizedBox(height: 20),
_buildSectionTitle('Estimasi Hasil Panen *'),
const SizedBox(height: 4),
Text(
'Data ini wajib diisi untuk analisis hasil panen',
style: TextStyle(
fontSize: 12,
color: Colors.red.shade700,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
_buildCostTextField(
_expectedYieldController,
'Hasil Panen (Kg)',
Icons.agriculture,
focusNode: _expectedYieldFocus,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Estimasi hasil panen harus diisi';
}
return null;
},
isRequired: true,
helperText: 'Wajib diisi untuk analisis hasil panen',
),
const SizedBox(height: 20),
_buildSectionTitle('Catatan Tambahan (Opsional)'),
const SizedBox(height: 12),
_buildTextField(
controller: _notesController,
labelText: 'Catatan',
icon: Icons.note,
maxLines: 3,
focusNode: _notesFocus,
),
// Tambahkan padding bawah untuk memastikan konten terlihat saat keyboard muncul
const SizedBox(height: 30),
],
),
),
),
),
// Tombol simpan yang tetap di bawah
Padding(
padding: const EdgeInsets.all(20),
child: SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _isSaved || _isLoading ? null : _submit,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF056839),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child:
_isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.save, size: 18),
const SizedBox(width: 8),
Text(
'Simpan Jadwal',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
],
),
),
);
}
Widget _buildCostTextField(
TextEditingController controller,
String label,
IconData icon, {
FocusNode? focusNode,
String? Function(String?)? validator,
bool isRequired = false,
String? helperText,
}) {
// Determine which validation state to update
void Function(bool) updateValidState;
bool isValid = false;
if (controller == _seedCostController) {
updateValidState = (value) => setState(() => _seedCostValid = value);
isValid = _seedCostValid;
} else if (controller == _fertilizerCostController) {
updateValidState =
(value) => setState(() => _fertilizerCostValid = value);
isValid = _fertilizerCostValid;
} else if (controller == _pesticideCostController) {
updateValidState = (value) => setState(() => _pesticideCostValid = value);
isValid = _pesticideCostValid;
} else if (controller == _irrigationCostController) {
updateValidState =
(value) => setState(() => _irrigationCostValid = value);
isValid = _irrigationCostValid;
} else if (controller == _expectedYieldController) {
updateValidState = (value) => setState(() => _expectedYieldValid = value);
isValid = _expectedYieldValid;
} else {
updateValidState = (_) {};
}
return _buildTextField(
controller: controller,
labelText: label,
icon: icon,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
focusNode: focusNode,
validator: validator,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) => FocusScope.of(context).nextFocus(),
isRequired: isRequired,
helperText: helperText,
onChanged: (value) {
final isFieldValid = value.isNotEmpty;
updateValidState(isFieldValid);
// Trigger validation to update error messages
_formKey.currentState?.validate();
},
isValid: isValid,
);
}
Widget _buildEmptyFieldsWarning() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.orange.shade200),
),
child: const Column(
children: [
Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 32),
SizedBox(height: 8),
Text(
'Belum ada lahan terdaftar.',
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 4),
Text(
'Silakan tambahkan lahan terlebih dahulu.',
textAlign: TextAlign.center,
),
],
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String labelText,
IconData? icon,
String? Function(String?)? validator,
TextInputType keyboardType = TextInputType.text,
int? maxLines,
FocusNode? focusNode,
bool isRequired = false,
String? helperText,
TextInputAction? textInputAction,
void Function(String)? onFieldSubmitted,
void Function(String)? onChanged,
bool isValid = false,
}) {
return TextFormField(
controller: controller,
focusNode: focusNode,
autofocus: false,
decoration: InputDecoration(
labelText: isRequired ? '$labelText *' : labelText,
prefixIcon: icon != null ? Icon(icon) : null,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
helperText: helperText,
helperStyle: TextStyle(
color: isRequired ? Colors.red.shade700 : Colors.grey.shade600,
fontSize: 12,
),
// Show green border when valid
enabledBorder:
isValid
? OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: Colors.green.shade500,
width: 1.5,
),
)
: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300),
),
// Change icon color to green when valid
prefixIconColor: isValid ? Colors.green : null,
),
keyboardType: keyboardType,
maxLines: maxLines,
onChanged: onChanged,
validator:
validator ??
(value) {
if (isRequired && (value == null || value.isEmpty)) {
return '$labelText harus diisi';
}
if (keyboardType ==
const TextInputType.numberWithOptions(decimal: true) &&
value != null &&
value.isNotEmpty) {
try {
final cleanText = value.trim().replaceAll(',', '.');
double.parse(cleanText);
} catch (e) {
return 'Format angka tidak valid';
}
}
return null;
},
textInputAction: textInputAction ?? TextInputAction.next,
onFieldSubmitted:
onFieldSubmitted ?? (_) => FocusScope.of(context).nextFocus(),
);
}
Widget _buildDropdownField<T>({
required T? value,
required List<dynamic> items,
required void Function(T?) onChanged,
required String labelText,
required IconData icon,
String? hint,
}) {
List<DropdownMenuItem<T>> dropdownItems = [];
if (items is List<String>) {
dropdownItems =
items
.map(
(item) => DropdownMenuItem<T>(
value: item as T,
child: Text(item, style: const TextStyle(fontSize: 14)),
),
)
.toList();
} else if (items is List<Map<String, dynamic>>) {
dropdownItems =
items
.map(
(field) => DropdownMenuItem<T>(
value: field['id'] as T,
child: Text(
field['name'] ?? 'Tanpa Nama',
style: const TextStyle(fontSize: 14),
),
),
)
.toList();
} else if (items is List<int>) {
dropdownItems =
items
.map(
(plot) => DropdownMenuItem<T>(
value: plot as T,
child: Text(
'Plot $plot',
style: const TextStyle(fontSize: 14),
),
),
)
.toList();
}
final T? selectedValue =
(value != null &&
items.any(
(item) => (item is Map ? item['id'] == value : item == value),
))
? value
: null;
return DropdownButtonFormField<T>(
value: selectedValue,
items: dropdownItems,
onChanged: onChanged,
decoration: _inputDecoration(
labelText,
icon,
isValid: selectedValue != null,
),
isExpanded: true,
hint: Text(
hint ?? 'Pilih $labelText',
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
icon: const Icon(Icons.arrow_drop_down),
);
}
Widget _buildDateField(String label, DateTime date, VoidCallback onTap) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: InputDecorator(
decoration: _inputDecoration(
label,
Icons.calendar_today,
isValid: true,
),
child: Text(
DateFormat('dd MMM yyyy').format(date),
style: const TextStyle(fontSize: 14),
),
),
);
}
Widget _buildSectionTitle(String title) {
return Text(
title,
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
);
}
InputDecoration _inputDecoration(
String labelText,
IconData icon, {
String? hintText,
bool isValid = false,
}) {
final Color primaryColor = const Color(0xFF056839);
final Color validColor = Colors.green.shade500;
return InputDecoration(
labelText: labelText,
hintText: hintText,
prefixIcon: Icon(icon, color: isValid ? validColor : primaryColor),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: isValid ? validColor : Colors.grey.shade300,
width: isValid ? 1.5 : 1.0,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: isValid ? validColor : primaryColor,
width: 2,
),
),
filled: true,
fillColor: Colors.white,
labelStyle: TextStyle(fontSize: 14, color: isValid ? validColor : null),
hintStyle: const TextStyle(fontSize: 14, color: Colors.grey),
);
}
}