You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

717 lines
25 KiB

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
import '../common/settings.dart';
class SupplierCalendar extends StatefulWidget {
@override
State<SupplierCalendar> createState() => _SupplierCalendarState();
}
class _SupplierCalendarState extends State<SupplierCalendar> {
late DateTime _focusedDay;
late DateTime _firstDay;
late DateTime _lastDay;
Map<DateTime, List<Map<String, dynamic>>> calendarEvents = {};
DateTime? _selectedDay;
bool isLoading = true;
Future<void> cancelSingleDate(Map<String, dynamic> delivery) async {
try {
await AppSettings.recurringDateAction(
delivery["_id"],
{
"action": "cancel",
"date": delivery["date"], // IMPORTANT
"reason": "Cancelled by supplier",
},
);
await fetchOrdersFromApi();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Delivery cancelled successfully")),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Cancel failed")),
);
}
}
Future<void> rescheduleSingleDate(
Map<String, dynamic> delivery,
String newDate,
) async {
try {
await AppSettings.recurringDateAction(
delivery["_id"],
{
"action": "reschedule",
"date": delivery["date"], // original date
"new_date": newDate, // selected date
"reason": "Rescheduled by supplier",
},
);
await fetchOrdersFromApi();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Delivery rescheduled")),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Reschedule failed")),
);
}
}
@override
void initState() {
super.initState();
final now = DateTime.now();
_focusedDay = now.add(const Duration(days: 2));
_firstDay = DateTime(2025, 1, 1);
_lastDay = DateTime(now.year, now.month + 6, 0);
fetchOrdersFromApi();
}
// ===========================================================================
// FETCH API → BUILD TWO EVENTS (ORIGINAL + RESCHEDULED)
// ===========================================================================
Future<void> fetchOrdersFromApi() async {
try {
final response = await AppSettings.getSupplierBookings();
final decoded = jsonDecode(response);
if (decoded == null || decoded['data'] == null) {
setState(() => isLoading = false);
return;
}
final List<dynamic> orders = decoded['data'];
calendarEvents.clear();
for (var order in orders) {
final String capacity = order["capacity"] ?? "";
final String time = order["time"] ?? "";
final String bookingStatus = order["booking_status"] ?? "";
final String supplierName =
order["customer_details"]?["profile"]?["username"] ?? "Customer";
final List<dynamic> dates = order["dates"] ?? [];
for (var d in dates) {
final String? dateStr = d["date"];
if (dateStr == null) continue;
DateTime date = DateTime.parse(dateStr);
DateTime key = DateTime(date.year, date.month, date.day);
calendarEvents.putIfAbsent(key, () => []);
// ---------------- STATUS LOGIC ----------------
String status = "delivery";
if (d["status"] == "rescheduled") {
status = "rescheduled_from";
}
if (d["rescheduled_from"] != null) {
status = "rescheduled_to";
}
if (d["status"] == "cancelled") {
status = "cancelled";
}
calendarEvents[key]!.add({
"status": status,
"_id": order["_id"],
"supplierName": supplierName,
"capacity": capacity,
"time": time,
"date": dateStr,
"originalDate": d["rescheduled_from"],
"newDate": d["rescheduled_to"],
});
}
}
setState(() => isLoading = false);
} catch (e) {
setState(() => isLoading = false);
}
}
// ===========================================================================
// CANCEL ORDER API
// ===========================================================================
/*Future<void> cancelDelivery(String id) async {
try {
await AppSettings.cancelPlanOrder(id); // your endpoint
await fetchOrdersFromApi();
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Order cancelled successfully")));
} catch (e) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Cancel failed")));
}
}*/
/*void _confirmCancel(String orderId) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text("Cancel Delivery"),
content: Text("Are you sure you want to cancel this delivery?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context), child: Text("No")),
TextButton(
onPressed: () {
Navigator.pop(context);
cancelDelivery(orderId);
},
child: Text("Yes, Cancel", style: TextStyle(color: Colors.red))),
],
),
);
}*/
// ===========================================================================
// HELPER FUNCTIONS
// ===========================================================================
String formatShort(String date) {
try {
final d = DateTime.parse(date);
return "${d.day} ${monthShort[d.month - 1]}";
} catch (e) {
return date;
}
}
List<String> monthShort = [
"Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec"
];
Widget _buildStatusIcon(String status) {
if (status == "rescheduled_from") {
return Icon(Icons.subdirectory_arrow_left, color: Colors.orange);
}
if (status == "rescheduled_to") {
return Icon(Icons.subdirectory_arrow_right, color: Colors.deepOrange);
}
if (status == "cancelled") {
return Icon(Icons.cancel, color: Colors.red);
}
return Icon(Icons.local_shipping, color: Colors.blue);
}
String _buildStatusText(Map<String, dynamic> d) {
String status = d["status"];
String? originalDate = d["originalDate"];
String? newDate = d["newDate"];
if (status == "rescheduled_from") {
return "Rescheduled from this date → ${formatShort(newDate!)}";
}
if (status == "rescheduled_to") {
return "Rescheduled to this date (from ${formatShort(originalDate!)})";
}
if (status == "cancelled") {
return "Cancelled";
}
return status;
}
// ===========================================================================
// OPEN RESCHEDULE CALENDAR
// ===========================================================================
Future<void> openRescheduleCalendar(Map<String, dynamic> delivery) async {
DateTime now = DateTime.now();
final DateTime? pickedDate = await showDatePicker(
context: context,
initialDate: now.add(Duration(days: 1)),
firstDate: now.add(Duration(days: 1)),
lastDate: DateTime(now.year + 1, 12, 31),
);
if (pickedDate == null) return;
String formatted =
"${pickedDate.year}-${pickedDate.month.toString().padLeft(2,'0')}-${pickedDate.day.toString().padLeft(2,'0')}";
await rescheduleSingleDate(delivery, formatted);
//await sendRescheduleToServer(delivery["_id"], formatted);
}
/*Future<void> sendRescheduleToServer(String id, String newDate) async {
try {
final payload = {"reScheduleDateOfDelivery": newDate};
await AppSettings.rescheduleOrder(id, payload);
await fetchOrdersFromApi();
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Delivery rescheduled to $newDate")));
} catch (e) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Reschedule failed")));
}
}
*/
// ===========================================================================
// SHOW DELIVERY LIST
// ===========================================================================
void showDeliveryList(DateTime date) {
final key = DateTime(date.year, date.month, date.day);
final events = calendarEvents[key];
if (events == null || events.isEmpty) return;
bool isPast = date.isBefore(DateTime(
DateTime.now().year, DateTime.now().month, DateTime.now().day));
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (_) {
final list = events.toList();
return DraggableScrollableSheet(
initialChildSize: 0.9,
maxChildSize: 0.9,
minChildSize: 0.4,
builder: (_, controller) {
return Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
Row(
children: [
Expanded(
child: Text(
"Select Delivery",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
GestureDetector(
onTap: () => Navigator.pop(context),
child: Icon(Icons.close)),
],
),
Expanded(
child: ListView(
controller: controller,
children: list.map((delivery) {
return ListTile(
leading: _buildStatusIcon(delivery["status"]),
title: Text(_buildStatusText(delivery)),
subtitle: Text(
"Capacity: ${delivery["capacity"]}${delivery["time"]}"),
onTap: () {
Navigator.pop(context);
showActionsForSingleDelivery(delivery, isPast);
},
);
}).toList(),
),
)
],
),
);
},
);
},
);
}
// ===========================================================================
// ACTIONS SHEET
// ===========================================================================
void showActionsForSingleDelivery(Map<String, dynamic> delivery, bool isPast) {
String status = delivery["status"];
// ❌ ORIGINAL DATE → NO ACTIONS
if (status == "rescheduled_from") {
showModalBottomSheet(
context: context,
builder: (_) {
return Container(
padding: EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("Rescheduled from this date",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 10),
Text("This delivery was moved to another date.",
style: TextStyle(color: Colors.grey)),
],
),
);
},
);
return;
}
// ❌ CANCELLED ORDER → NO ACTIONS
if (status == "cancelled") {
showModalBottomSheet(
context: context,
builder: (_) =>
Container(
padding: EdgeInsets.all(20),
child: Text("Order was cancelled", style: TextStyle(color: Colors.red)),
),
);
return;
}
// NORMAL OR RESCHEDULED_TO (ACTIONS ENABLED)
showModalBottomSheet(
context: context,
builder: (_) {
return Container(
padding: EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("${delivery["supplierName"]}",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
SizedBox(height: 16),
if (!isPast) ...[
ListTile(
leading: Icon(Icons.calendar_month, color: Colors.blue),
title: Text("Reschedule Delivery"),
onTap: () {
Navigator.pop(context);
openRescheduleCalendar(delivery);
},
),
ListTile(
leading: Icon(Icons.delete_forever, color: Colors.red),
title: Text("Cancel Delivery"),
onTap: () {
Navigator.pop(context);
cancelSingleDate(delivery);
},
),
],
if (isPast)
Text("Past delivery — actions disabled",
style: TextStyle(color: Colors.grey)),
],
),
);
},
);
}
// ===========================================================================
// BUILD UI
// ===========================================================================
@override
Widget build(BuildContext context) {
if (isLoading) {
return Scaffold(body: Center(child: CircularProgressIndicator()));
}
final height = MediaQuery.of(context).size.height;
return Scaffold(
backgroundColor: Colors.white,
body: Column(
children: [
SizedBox(height: 50),
Text(
"${monthShort[_focusedDay.month - 1]} ${_focusedDay.year}",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
Expanded(
child: TableCalendar(
firstDay: _firstDay,
lastDay: _lastDay,
focusedDay: _focusedDay,
headerVisible: false,
calendarFormat: CalendarFormat.month,
rowHeight: (height - 200) / 6 + 12,
daysOfWeekHeight: 40,
calendarStyle: CalendarStyle(
selectedDecoration: const BoxDecoration(
color: Colors.transparent, // remove selected circle
),
todayDecoration: const BoxDecoration(
color: Colors.transparent, // 🚫 removes TODAY highlight
),
todayTextStyle: const TextStyle(
color: Colors.red, // same as normal day
fontWeight: FontWeight.bold,
),
tableBorder: TableBorder(
horizontalInside: BorderSide(
color: Colors.grey.shade300,
width: 1,
),
),
),
onPageChanged: (focused) {
setState(() => _focusedDay = focused);
},
onDaySelected: (selected, focused) {
_focusedDay = focused;
_selectedDay = selected;
showDeliveryList(selected);
setState(() {});
},
calendarBuilders: CalendarBuilders(
/// 🔴 TODAY BUILDER (dot only here)
todayBuilder: (context, date, _) {
final key = DateTime(date.year, date.month, date.day);
final events = calendarEvents[key];
Map<String, int> grouped = {};
if (events != null) {
for (var e in events) {
grouped[e["status"]] = (grouped[e["status"]] ?? 0) + 1;
}
}
return Container(
margin: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.only(top: 1),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 🔴 Today date
Text(
"${date.day}",
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
// 🔴 Dot ONLY for today
const SizedBox(height: 2),
Container(
width: 5,
height: 5,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
),
// Events OR reserved space
if (events != null && events.isNotEmpty)
Wrap(
spacing: 3,
alignment: WrapAlignment.center,
children: grouped.entries.map((entry) {
String label;
IconData icon;
Color color;
if (entry.key == "rescheduled_from" ||
entry.key == "rescheduled_to") {
label = "Rescheduled";
icon = Icons.update;
color = Colors.orange;
} else if (entry.key == "cancelled") {
label = "Cancelled";
icon = Icons.cancel;
color = Colors.red;
} else {
label = "Delivery";
icon = Icons.local_shipping;
color = Colors.blue;
}
return SizedBox(
height: 26,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 3, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(0.12),
borderRadius: BorderRadius.zero,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 10, color: color),
const SizedBox(height: 1),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
"$label x${entry.value}",
style: TextStyle(
fontSize: 7,
fontWeight: FontWeight.bold,
color: color,
),
),
),
],
),
),
);
}).toList(),
)
else
const SizedBox(height: 26), // keep height same
],
),
);
},
/// 📅 NORMAL DAYS (NO DOT)
defaultBuilder: (context, date, _) {
final key = DateTime(date.year, date.month, date.day);
final events = calendarEvents[key];
Map<String, int> grouped = {};
if (events != null) {
for (var e in events) {
grouped[e["status"]] = (grouped[e["status"]] ?? 0) + 1;
}
}
return Container(
margin: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.only(top: 1),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"${date.day}",
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
if (events != null && events.isNotEmpty)
Wrap(
spacing: 3,
alignment: WrapAlignment.center,
children: grouped.entries.map((entry) {
String label;
IconData icon;
Color color;
if (entry.key == "rescheduled_from" ||
entry.key == "rescheduled_to") {
label = "Rescheduled";
icon = Icons.update;
color = Colors.orange;
} else if (entry.key == "cancelled") {
label = "Cancelled";
icon = Icons.cancel;
color = Colors.red;
} else {
label = "Delivery";
icon = Icons.local_shipping;
color = Colors.blue;
}
return SizedBox(
height: 26,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 3, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(0.12),
borderRadius: BorderRadius.zero,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 10, color: color),
const SizedBox(height: 1),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
"$label x${entry.value}",
style: TextStyle(
fontSize: 7,
fontWeight: FontWeight.bold,
color: color,
),
),
),
],
),
),
);
}).toList(),
)
else
const SizedBox(height: 26),
],
),
);
},
/// 🌫 Outside month days
outsideBuilder: (context, date, _) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.only(top: 1),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
"${date.day}",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey.shade400,
),
),
]
),
);
},
),
),
),
],
),
);
}
}