MIF_E31222656/lib/screens/calendar/add_daily_log_dialog.dart

382 lines
13 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:uuid/uuid.dart';
class AddDailyLogDialog extends StatefulWidget {
final String scheduleId;
final DateTime date;
const AddDailyLogDialog({
super.key,
required this.scheduleId,
required this.date,
});
@override
State<AddDailyLogDialog> createState() => _AddDailyLogDialogState();
}
class _AddDailyLogDialogState extends State<AddDailyLogDialog> {
final _formKey = GlobalKey<FormState>();
final _noteController = TextEditingController();
final _costController = TextEditingController();
final _noteFocus = FocusNode();
final _costFocus = FocusNode();
final _scrollController = ScrollController();
File? _imageFile;
bool _isUploading = false;
@override
void initState() {
super.initState();
// Hapus semua kode yang mungkin mengganggu keyboard
}
@override
void dispose() {
_noteController.dispose();
_costController.dispose();
_noteFocus.dispose();
_costFocus.dispose();
_scrollController.dispose();
super.dispose();
}
Future<void> _pickImage() async {
final picker = ImagePicker();
final picked = await picker.pickImage(source: ImageSource.camera);
if (picked != null) {
setState(() => _imageFile = File(picked.path));
}
}
Future<String?> _uploadImage(String id) async {
if (_imageFile == null) return null;
final fileExt = _imageFile!.path.split('.').last;
final path = 'daily_logs/$id.$fileExt';
final storage = Supabase.instance.client.storage;
try {
await storage
.from('images')
.upload(
path,
_imageFile!,
fileOptions: const FileOptions(upsert: true),
);
} catch (e) {
debugPrint('Upload error: $e');
return null;
}
final url = storage.from('images').getPublicUrl(path);
return url;
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isUploading = true);
final id = const Uuid().v4();
final imageUrl = await _uploadImage(id);
if (_imageFile != null && imageUrl == null) {
setState(() => _isUploading = false);
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Gagal mengunggah gambar')));
return;
}
try {
final selectedDate = DateTime.utc(
widget.date.year,
widget.date.month,
widget.date.day,
DateTime.now().hour,
DateTime.now().minute,
);
await Supabase.instance.client.from('daily_logs').insert({
'id': id,
'schedule_id': widget.scheduleId,
'date': selectedDate.toIso8601String(),
'note': _noteController.text,
'cost': double.tryParse(_costController.text),
'image_url': imageUrl,
});
if (mounted) {
Navigator.pop(context, true);
}
} catch (e) {
debugPrint('Insert error: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Gagal menyimpan log harian')),
);
}
}
}
String _formatDate(DateTime date) {
const months = [
'Januari',
'Februari',
'Maret',
'April',
'Mei',
'Juni',
'Juli',
'Agustus',
'September',
'Oktober',
'November',
'Desember',
];
return '${date.day} ${months[date.month - 1]} ${date.year}';
}
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header dengan judul dan tanggal
Container(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Judul
const Text(
'Catatan Harian',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
// Tanggal
Text(
'Tanggal: ${_formatDate(widget.date)}',
style: const TextStyle(fontSize: 16, color: Colors.black87),
),
],
),
),
// Konten form yang dapat di-scroll
Flexible(
child: SingleChildScrollView(
padding: EdgeInsets.only(
bottom:
MediaQuery.of(context).viewInsets.bottom +
80, // Meningkatkan padding bottom saat keyboard muncul
left: 20,
right: 20,
top: 10,
),
controller: _scrollController,
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label Catatan
const Text(
'Catatan',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
// Field Catatan
TextFormField(
controller: _noteController,
focusNode: _noteFocus,
maxLines: 3,
textInputAction: TextInputAction.next,
onFieldSubmitted:
(_) =>
FocusScope.of(context).requestFocus(_costFocus),
decoration: InputDecoration(
hintText: 'Catatan',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
),
),
const SizedBox(height: 16),
// Label Biaya
const Text(
'Biaya (Rp)',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
// Field Biaya
TextFormField(
controller: _costController,
focusNode: _costFocus,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
hintText: 'Biaya (Rp)',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Biaya tidak boleh kosong';
}
if (double.tryParse(value) == null) {
return 'Masukkan angka yang valid';
}
return null;
},
),
const SizedBox(height: 20),
// Tombol Ambil Foto
Center(
child: OutlinedButton.icon(
onPressed: _pickImage,
icon: const Icon(
Icons.camera_alt,
color: Color(0xFF056839),
),
label: const Text(
'Ambil Foto',
style: TextStyle(color: Color(0xFF056839)),
),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFF056839)),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
),
// Preview gambar jika ada
if (_imageFile != null) ...[
const SizedBox(height: 16),
Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
_imageFile!,
width: 150,
height: 150,
fit: BoxFit.cover,
),
),
),
],
const SizedBox(height: 24),
// Tombol Aksi
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
'Batal',
style: TextStyle(
color: Colors.black54,
fontSize: 16,
),
),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: _isUploading ? null : _submit,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF056839),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 12,
),
),
child:
_isUploading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Text(
'Simpan',
style: TextStyle(fontSize: 16),
),
),
],
),
],
),
),
),
),
],
),
),
);
}
}
Future<dynamic> _showAddDailyLogDialog(
BuildContext context,
String scheduleId,
DateTime date,
) {
// Gunakan showModalBottomSheet standar
return showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
useSafeArea: true,
enableDrag: true,
isDismissible: true,
builder: (BuildContext context) {
return AddDailyLogDialog(scheduleId: scheduleId, date: date);
},
);
}