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
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,
|
|
),
|
|
),
|
|
]
|
|
),
|
|
);
|
|
},
|
|
),
|
|
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|