382 lines
13 KiB
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);
|
|
},
|
|
);
|
|
}
|