1171 lines
38 KiB
Dart
1171 lines
38 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
import 'package:tugas_akhir_supabase/utils/date_formatter.dart';
|
|
import 'package:table_calendar/table_calendar.dart';
|
|
import 'package:tugas_akhir_supabase/screens/calendar/add_daily_log_dialog.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
class ScheduleDetailScreen extends StatefulWidget {
|
|
final String scheduleId;
|
|
final int initialTabIndex;
|
|
final DateTime? initialSelectedDate;
|
|
|
|
const ScheduleDetailScreen({
|
|
super.key,
|
|
required this.scheduleId,
|
|
this.initialTabIndex = 0,
|
|
this.initialSelectedDate,
|
|
});
|
|
|
|
@override
|
|
_ScheduleDetailScreenState createState() => _ScheduleDetailScreenState();
|
|
}
|
|
|
|
class _ScheduleDetailScreenState extends State<ScheduleDetailScreen>
|
|
with SingleTickerProviderStateMixin {
|
|
bool _isLoading = true;
|
|
Map<String, dynamic>? _schedule;
|
|
final List<Map<String, dynamic>> _activities = [];
|
|
List<Map<String, dynamic>> _dailyLogs = [];
|
|
Map<DateTime, List<dynamic>> _events = {};
|
|
|
|
// Calendar properties
|
|
CalendarFormat _calendarFormat = CalendarFormat.month;
|
|
DateTime _focusedDay = DateTime.now();
|
|
DateTime? _selectedDay;
|
|
|
|
late TabController _tabController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Use initialSelectedDate if provided, otherwise use current date
|
|
_focusedDay = widget.initialSelectedDate ?? DateTime.now();
|
|
_selectedDay = widget.initialSelectedDate ?? _focusedDay;
|
|
|
|
_tabController = TabController(
|
|
length: 2,
|
|
vsync: this,
|
|
initialIndex: widget.initialTabIndex,
|
|
);
|
|
_fetchScheduleDetails();
|
|
_fetchDailyLogs();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_tabController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _fetchScheduleDetails() async {
|
|
try {
|
|
// Fetch schedule details using crop_schedules
|
|
debugPrint('INFO: Fetching schedule detail for ID: ${widget.scheduleId}');
|
|
final scheduleResponse =
|
|
await Supabase.instance.client
|
|
.from('crop_schedules')
|
|
.select('*, fields!crop_schedules_field_id_fkey(name)')
|
|
.eq('id', widget.scheduleId)
|
|
.single();
|
|
|
|
debugPrint('INFO: Schedule detail response: $scheduleResponse');
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_schedule = scheduleResponse;
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error fetching schedule details: $e');
|
|
if (mounted) {
|
|
setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _fetchDailyLogs() async {
|
|
try {
|
|
debugPrint(
|
|
'INFO: Fetching daily logs for schedule: ${widget.scheduleId}',
|
|
);
|
|
final response = await Supabase.instance.client
|
|
.from('daily_logs')
|
|
.select('*')
|
|
.eq('schedule_id', widget.scheduleId)
|
|
.order('date', ascending: true);
|
|
|
|
debugPrint('INFO: Daily logs response type: ${response.runtimeType}');
|
|
debugPrint('INFO: Raw daily logs response: $response');
|
|
|
|
final logs =
|
|
response is List
|
|
? response.map((item) => item).toList()
|
|
: <Map<String, dynamic>>[];
|
|
|
|
debugPrint('INFO: Found ${logs.length} daily logs');
|
|
|
|
final events = <DateTime, List<dynamic>>{};
|
|
|
|
for (final log in logs) {
|
|
final date = DateTime.parse(log['date']).toLocal();
|
|
final dateKey = DateTime(date.year, date.month, date.day);
|
|
|
|
if (events[dateKey] != null) {
|
|
events[dateKey]!.add(log);
|
|
} else {
|
|
events[dateKey] = [log];
|
|
}
|
|
}
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_dailyLogs = logs;
|
|
_events = events;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('ERROR: Error fetching daily logs: $e');
|
|
}
|
|
}
|
|
|
|
List<dynamic> _getEventsForDay(DateTime day) {
|
|
final dateKey = DateTime(day.year, day.month, day.day);
|
|
return _events[dateKey] ?? [];
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title:
|
|
_isLoading || _schedule == null
|
|
? const Text('Detail Jadwal')
|
|
: Text(
|
|
_schedule!['crop_name'] ?? 'Tanaman',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
backgroundColor: const Color(0xFF056839),
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
tooltip: 'Refresh Data',
|
|
onPressed: () {
|
|
setState(() => _isLoading = true);
|
|
_fetchScheduleDetails();
|
|
_fetchDailyLogs();
|
|
},
|
|
),
|
|
],
|
|
bottom: TabBar(
|
|
controller: _tabController,
|
|
labelColor: Colors.white,
|
|
indicatorColor: Colors.white,
|
|
indicatorWeight: 2.5,
|
|
labelStyle: GoogleFonts.poppins(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
tabs: [Tab(text: 'Info Jadwal'), Tab(text: 'Catatan Harian')],
|
|
),
|
|
),
|
|
body:
|
|
_isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: _schedule == null
|
|
? _buildErrorState()
|
|
: TabBarView(
|
|
controller: _tabController,
|
|
children: [_buildScheduleDetails(), _buildDailyLogCalendar()],
|
|
),
|
|
floatingActionButton:
|
|
!_isLoading && _schedule != null && _tabController.index == 1
|
|
? FloatingActionButton(
|
|
onPressed:
|
|
() =>
|
|
_showAddDailyLogDialog(_selectedDay ?? DateTime.now()),
|
|
backgroundColor: const Color(0xFF056839),
|
|
elevation: 2,
|
|
child: const Icon(Icons.add, color: Colors.white, size: 22),
|
|
)
|
|
: null,
|
|
);
|
|
}
|
|
|
|
Widget _buildErrorState() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.error_outline, size: 64, color: Colors.grey[400]),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Jadwal tidak ditemukan',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF056839),
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: Text(
|
|
'Kembali',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildScheduleDetails() {
|
|
final cropName = _schedule!['crop_name'] ?? 'Tanaman';
|
|
final fieldName =
|
|
_schedule!['fields!crop_schedules_field_id_fkey']?['name'] ?? 'Lahan';
|
|
final startDate = DateTime.parse(_schedule!['start_date']);
|
|
final endDate = DateTime.parse(_schedule!['end_date']);
|
|
final status = _schedule!['status'] ?? 'active';
|
|
final notes = _schedule!['notes'] ?? '';
|
|
|
|
// Calculate progress
|
|
final totalDuration = endDate.difference(startDate).inDays;
|
|
final elapsedDuration = DateTime.now().difference(startDate).inDays;
|
|
double progress = elapsedDuration / totalDuration;
|
|
progress = progress.clamp(0.0, 1.0);
|
|
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildHeaderCard(
|
|
cropName,
|
|
fieldName,
|
|
startDate,
|
|
endDate,
|
|
status,
|
|
progress,
|
|
),
|
|
const SizedBox(height: 20),
|
|
if (notes.isNotEmpty) ...[
|
|
_buildNotesSection(notes),
|
|
const SizedBox(height: 20),
|
|
],
|
|
_buildActivitiesSection(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeaderCard(
|
|
String cropName,
|
|
String fieldName,
|
|
DateTime startDate,
|
|
DateTime endDate,
|
|
String status,
|
|
double progress,
|
|
) {
|
|
Color statusColor;
|
|
String statusText;
|
|
|
|
switch (status.toLowerCase()) {
|
|
case 'active':
|
|
statusColor = Colors.green;
|
|
statusText = 'Aktif';
|
|
break;
|
|
case 'completed':
|
|
statusColor = Colors.blue;
|
|
statusText = 'Selesai';
|
|
break;
|
|
case 'cancelled':
|
|
statusColor = Colors.red;
|
|
statusText = 'Dibatalkan';
|
|
break;
|
|
default:
|
|
statusColor = Colors.orange;
|
|
statusText = 'Pending';
|
|
}
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFF056839), Color(0xFF0E8C51)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(10),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFF056839).withOpacity(0.2),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 3),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
cropName,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: statusColor,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
statusText,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
Text(
|
|
'Lahan: $fieldName',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 13,
|
|
color: Colors.white.withOpacity(0.9),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'Periode',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 11,
|
|
color: Colors.white.withOpacity(0.8),
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'${formatDate(startDate)} - ${formatDate(endDate)}',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'Progress',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 11,
|
|
color: Colors.white.withOpacity(0.8),
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(6),
|
|
child: LinearProgressIndicator(
|
|
value: progress,
|
|
backgroundColor: Colors.white.withOpacity(0.3),
|
|
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
|
|
minHeight: 6,
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
'${(progress * 100).toInt()}%',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDailyLogCalendar() {
|
|
// Ambil tanggal jadwal
|
|
final startDate =
|
|
_schedule != null
|
|
? DateTime.parse(_schedule!['start_date'])
|
|
: DateTime.now();
|
|
final endDate =
|
|
_schedule != null
|
|
? DateTime.parse(_schedule!['end_date'])
|
|
: DateTime.now().add(const Duration(days: 90));
|
|
|
|
// Set _focusedDay to be within the range if it's not already
|
|
if (_focusedDay.isBefore(startDate) || _focusedDay.isAfter(endDate)) {
|
|
_focusedDay = startDate;
|
|
}
|
|
|
|
// Also set _selectedDay to be within range if needed
|
|
if (_selectedDay == null ||
|
|
_selectedDay!.isBefore(startDate) ||
|
|
_selectedDay!.isAfter(endDate)) {
|
|
_selectedDay = startDate;
|
|
}
|
|
|
|
return Column(
|
|
children: [
|
|
// Tampilkan periode tanggal
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.03),
|
|
blurRadius: 3,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
'Periode Tanam Aktif',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'${DateFormat('dd MMM yyyy').format(startDate)} - ${DateFormat('dd MMM yyyy').format(endDate)}',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: const Color(0xFF056839),
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
_buildCalendarHeader(),
|
|
_buildCalendar(),
|
|
const SizedBox(height: 8),
|
|
Expanded(
|
|
child:
|
|
_getEventsForDay(_selectedDay ?? DateTime.now()).isEmpty
|
|
? _buildEmptyDailyLogState()
|
|
: _buildDailyLogsList(
|
|
_getEventsForDay(_selectedDay ?? DateTime.now()),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCalendarHeader() {
|
|
return Container(
|
|
padding: const EdgeInsets.fromLTRB(14, 10, 14, 10),
|
|
decoration: const BoxDecoration(color: Color(0xFF056839)),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(
|
|
Icons.chevron_left,
|
|
color: Colors.white,
|
|
size: 20,
|
|
),
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(),
|
|
onPressed: () {
|
|
setState(() {
|
|
_focusedDay = DateTime(
|
|
_focusedDay.year,
|
|
_focusedDay.month - 1,
|
|
);
|
|
});
|
|
},
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
DateFormat('MMMM yyyy').format(_focusedDay),
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
IconButton(
|
|
icon: const Icon(
|
|
Icons.chevron_right,
|
|
color: Colors.white,
|
|
size: 20,
|
|
),
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(),
|
|
onPressed: () {
|
|
setState(() {
|
|
_focusedDay = DateTime(
|
|
_focusedDay.year,
|
|
_focusedDay.month + 1,
|
|
);
|
|
});
|
|
},
|
|
),
|
|
],
|
|
),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () {
|
|
setState(() {
|
|
_calendarFormat = CalendarFormat.month;
|
|
});
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10,
|
|
vertical: 5,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color:
|
|
_calendarFormat == CalendarFormat.month
|
|
? Colors.white
|
|
: Colors.transparent,
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: Text(
|
|
'2 weeks',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color:
|
|
_calendarFormat == CalendarFormat.month
|
|
? const Color(0xFF056839)
|
|
: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildCalendar() {
|
|
// Get start and end date from schedule for limiting calendar interaction
|
|
final startDate =
|
|
_schedule != null
|
|
? DateTime.parse(
|
|
_schedule!['start_date'],
|
|
).subtract(const Duration(days: 1))
|
|
: DateTime.now().subtract(const Duration(days: 30));
|
|
|
|
final endDate =
|
|
_schedule != null
|
|
? DateTime.parse(
|
|
_schedule!['end_date'],
|
|
).add(const Duration(days: 1))
|
|
: DateTime.now().add(const Duration(days: 90));
|
|
|
|
// If initialSelectedDate is provided and within range, use it as focused day
|
|
DateTime effectiveFocusedDay = _focusedDay;
|
|
if (_focusedDay.isAfter(startDate) && _focusedDay.isBefore(endDate)) {
|
|
effectiveFocusedDay = _focusedDay;
|
|
} else {
|
|
effectiveFocusedDay = DateTime.parse(_schedule!['start_date']);
|
|
}
|
|
|
|
return Container(
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.03),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 3),
|
|
),
|
|
],
|
|
),
|
|
child: TableCalendar(
|
|
firstDay: startDate,
|
|
lastDay: endDate,
|
|
focusedDay: effectiveFocusedDay,
|
|
calendarFormat: _calendarFormat,
|
|
eventLoader: _getEventsForDay,
|
|
selectedDayPredicate: (day) {
|
|
return isSameDay(_selectedDay, day);
|
|
},
|
|
// Enable only days within the schedule period
|
|
enabledDayPredicate: (day) {
|
|
return day.isAfter(startDate) && day.isBefore(endDate);
|
|
},
|
|
onDaySelected: (selectedDay, focusedDay) {
|
|
// Only allow selection within the valid range
|
|
if (selectedDay.isAfter(startDate) && selectedDay.isBefore(endDate)) {
|
|
setState(() {
|
|
_selectedDay = selectedDay;
|
|
_focusedDay = focusedDay;
|
|
});
|
|
}
|
|
},
|
|
onFormatChanged: (format) {
|
|
if (_calendarFormat != format) {
|
|
setState(() {
|
|
_calendarFormat = format;
|
|
});
|
|
}
|
|
},
|
|
onPageChanged: (focusedDay) {
|
|
setState(() {
|
|
_focusedDay = focusedDay;
|
|
});
|
|
},
|
|
calendarStyle: CalendarStyle(
|
|
markersMaxCount: 3,
|
|
markerDecoration: const BoxDecoration(
|
|
color: Color(0xFF056839),
|
|
shape: BoxShape.circle,
|
|
),
|
|
markerSize: 6,
|
|
selectedDecoration: const BoxDecoration(
|
|
color: Color(0xFF4364CD),
|
|
shape: BoxShape.circle,
|
|
),
|
|
todayDecoration: BoxDecoration(
|
|
color: const Color(0xFF4364CD).withOpacity(0.5),
|
|
shape: BoxShape.circle,
|
|
),
|
|
weekendTextStyle: TextStyle(color: Colors.red[300], fontSize: 12),
|
|
defaultTextStyle: const TextStyle(fontSize: 12),
|
|
outsideTextStyle: TextStyle(color: Colors.grey[400], fontSize: 12),
|
|
disabledTextStyle: TextStyle(color: Colors.grey[300], fontSize: 12),
|
|
selectedTextStyle: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
todayTextStyle: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
cellMargin: const EdgeInsets.all(0),
|
|
cellPadding: const EdgeInsets.all(0),
|
|
// Highlight days in the schedule period
|
|
outsideDaysVisible: false,
|
|
),
|
|
headerVisible: false,
|
|
daysOfWeekStyle: DaysOfWeekStyle(
|
|
weekdayStyle: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.black87,
|
|
),
|
|
weekendStyle: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.red[300],
|
|
),
|
|
decoration: BoxDecoration(color: Colors.grey[50]),
|
|
),
|
|
daysOfWeekHeight: 28,
|
|
rowHeight: 38,
|
|
availableGestures: AvailableGestures.all,
|
|
sixWeekMonthsEnforced: true,
|
|
calendarBuilders: CalendarBuilders(
|
|
// Custom day builder to highlight days in the range
|
|
defaultBuilder: (context, day, focusedDay) {
|
|
// Check if the day is within our schedule period
|
|
final isInRange = day.isAfter(startDate) && day.isBefore(endDate);
|
|
|
|
if (!isInRange) {
|
|
// Days outside our range
|
|
return Container(
|
|
margin: const EdgeInsets.all(4),
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
'${day.day}',
|
|
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Default day display for days in range
|
|
return null; // Use default builder for in-range days
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDailyLogsList(List<dynamic> dailyLogs) {
|
|
return ListView.builder(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
itemCount: dailyLogs.length,
|
|
itemBuilder: (context, index) {
|
|
final log = dailyLogs[index] as Map<String, dynamic>;
|
|
final date = DateTime.parse(log['date']);
|
|
final note = log['note'] ?? '';
|
|
final cost = log['cost'] ?? 0.0;
|
|
final imageUrl = log['image_url'];
|
|
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
elevation: 1,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(10),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Jika ada gambar, tampilkan di sebelah kiri
|
|
if (imageUrl != null)
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(6),
|
|
child: Image.network(
|
|
imageUrl,
|
|
height: 60,
|
|
width: 60,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return Container(
|
|
height: 60,
|
|
width: 60,
|
|
color: Colors.grey[300],
|
|
child: const Icon(Icons.error_outline, size: 20),
|
|
);
|
|
},
|
|
),
|
|
)
|
|
else
|
|
Container(
|
|
height: 60,
|
|
width: 60,
|
|
decoration: BoxDecoration(
|
|
color: Colors.green.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Icon(Icons.eco, color: Colors.green, size: 28),
|
|
),
|
|
const SizedBox(width: 10),
|
|
|
|
// Konten catatan
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
note.isNotEmpty
|
|
? Text(
|
|
note,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
)
|
|
: Text(
|
|
'Aktivitas',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Biaya: Rp ${NumberFormat('#,###').format(cost)}',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.green[700],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Tombol hapus
|
|
SizedBox(
|
|
height: 32,
|
|
width: 32,
|
|
child: IconButton(
|
|
padding: EdgeInsets.zero,
|
|
iconSize: 18,
|
|
icon: const Icon(Icons.delete, color: Colors.red),
|
|
onPressed: () => _confirmDeleteLog(log['id']),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _confirmDeleteLog(String logId) async {
|
|
final confirm = await showDialog<bool>(
|
|
context: context,
|
|
builder:
|
|
(context) => AlertDialog(
|
|
title: Text(
|
|
'Hapus Catatan',
|
|
style: GoogleFonts.poppins(fontWeight: FontWeight.bold),
|
|
),
|
|
content: Text(
|
|
'Apakah Anda yakin ingin menghapus catatan ini?',
|
|
style: GoogleFonts.poppins(),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: Text('Batal', style: GoogleFonts.poppins()),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
child: Text(
|
|
'Hapus',
|
|
style: GoogleFonts.poppins(color: Colors.red),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirm == true) {
|
|
try {
|
|
await Supabase.instance.client
|
|
.from('daily_logs')
|
|
.delete()
|
|
.eq('id', logId);
|
|
|
|
// Refresh data setelah menghapus
|
|
_fetchDailyLogs();
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Catatan berhasil dihapus')),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('ERROR: Failed to delete daily log: $e');
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Gagal menghapus catatan'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Widget _buildEmptyDailyLogState() {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.event_note, size: 32, color: Colors.grey[350]),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Belum ada catatan harian',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Tambahkan catatan harian untuk tanggal ini',
|
|
textAlign: TextAlign.center,
|
|
style: GoogleFonts.poppins(fontSize: 11, color: Colors.grey[500]),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showAddDailyLogDialog(DateTime selectedDate) async {
|
|
// Get schedule date range
|
|
final startDate =
|
|
_schedule != null
|
|
? DateTime.parse(
|
|
_schedule!['start_date'],
|
|
).subtract(const Duration(days: 1))
|
|
: DateTime.now().subtract(const Duration(days: 30));
|
|
|
|
final endDate =
|
|
_schedule != null
|
|
? DateTime.parse(
|
|
_schedule!['end_date'],
|
|
).add(const Duration(days: 1))
|
|
: DateTime.now().add(const Duration(days: 90));
|
|
|
|
// Check if selected date is within the schedule period
|
|
if (!selectedDate.isAfter(startDate) || !selectedDate.isBefore(endDate)) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Tanggal di luar periode jadwal tanam'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
final result = await showModalBottomSheet<bool>(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: Colors.transparent,
|
|
useSafeArea: true,
|
|
enableDrag: true,
|
|
isDismissible: true,
|
|
builder:
|
|
(context) => AddDailyLogDialog(
|
|
scheduleId: widget.scheduleId,
|
|
date: selectedDate,
|
|
),
|
|
);
|
|
|
|
if (result == true) {
|
|
// Refresh daily logs data
|
|
_fetchDailyLogs();
|
|
}
|
|
}
|
|
|
|
Widget _buildNotesSection(String notes) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Catatan',
|
|
style: GoogleFonts.poppins(fontSize: 16, fontWeight: FontWeight.w600),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(10),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Text(
|
|
notes,
|
|
style: GoogleFonts.poppins(fontSize: 13, color: Colors.grey[800]),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildActivitiesSection() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Aktivitas',
|
|
style: GoogleFonts.poppins(fontSize: 16, fontWeight: FontWeight.w600),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_dailyLogs.isEmpty
|
|
? _buildEmptyActivitiesState()
|
|
: Column(
|
|
children:
|
|
_dailyLogs.map((log) {
|
|
final activityName = log['note'] ?? 'Aktivitas';
|
|
final date = DateTime.parse(log['date']);
|
|
final cost = log['cost'] ?? 0;
|
|
final imageUrl = log['image_url'];
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 10),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(10),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(10),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Display image if available
|
|
if (imageUrl != null)
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(6),
|
|
child: Image.network(
|
|
imageUrl,
|
|
height: 60,
|
|
width: 60,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return Container(
|
|
height: 60,
|
|
width: 60,
|
|
color: Colors.grey[300],
|
|
child: const Icon(
|
|
Icons.error_outline,
|
|
size: 24,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
)
|
|
else
|
|
Container(
|
|
height: 60,
|
|
width: 60,
|
|
decoration: BoxDecoration(
|
|
color: Colors.green.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: const Icon(
|
|
Icons.eco,
|
|
color: Colors.green,
|
|
size: 30,
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
|
|
// Activity details
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
activityName,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Tanggal: ${formatDate(date)}',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'Biaya: Rp ${NumberFormat('#,###').format(cost)}',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
color: Colors.green[700],
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildEmptyActivitiesState() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(10),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Center(
|
|
child: Column(
|
|
children: [
|
|
Icon(Icons.event_busy, size: 36, color: Colors.grey[400]),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Belum ada aktivitas',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Tambahkan aktivitas untuk jadwal ini',
|
|
style: GoogleFonts.poppins(fontSize: 12, color: Colors.grey[500]),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|