MIF_E31222656/lib/screens/calendar/schedule_detail_screen.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,
),
],
),
),
);
}
}