我有两页,其中一页是用户的个人资料,他在日历中添加了可供人们预订的时间,另一页我可以在其中查看个人资料并选择可用的时段和日期。唯一的问题是,一个显示来自 Firebase 的数据,并添加了时间段(用户在其中插入可用日期 https://imgur.com/a/j7KGVn0),另一个显示未获取数据https://imgur.com/a/5fLKEln
class BookingCalendar extends StatefulWidget {
@override
_BookingCalendarState createState() => _BookingCalendarState();
}
class _BookingCalendarState extends State<BookingCalendar> {
CalendarFormat _calendarFormat = CalendarFormat.month;
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
Map<DateTime, List<TimeSlot>> _availableSlots = {};
late String userId;
bool _isLoading = false;
@override
void initState() {
super.initState();
userId = FirebaseAuth.instance.currentUser!.uid;
_fetchAvailableSlots();
}
void _fetchAvailableSlots() async {
setState(() {
_isLoading = true;
});
final slotsSnapshot = await FirebaseFirestore.instance
.collection('users')
.doc(userId)
.collection('availableSlots')
.get();
setState(() {
_availableSlots = {};
for (var doc in slotsSnapshot.docs) {
DateTime date = (doc['date'] as Timestamp).toDate();
List<TimeSlot> slots = (doc['slots'] as List)
.map((slot) => TimeSlot(
TimeOfDay.fromDateTime(
(slot['startTime'] as Timestamp).toDate()),
TimeOfDay.fromDateTime((slot['endTime'] as Timestamp).toDate()),
))
.toList();
_availableSlots[DateTime(date.year, date.month, date.day)] = slots;
}
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Booking Calendar'),
actions: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: ElevatedButton(
onPressed: _saveAllSlots,
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF1A237E), // Dark blue background (Indigo 900)
foregroundColor: Colors.white, // White text
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20), // Rounded borders
),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 0, // No shadow
),
child: Text(
'Save',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
body: _isLoading
? Center(child: CircularProgressIndicator())
: Column(
children: [
TableCalendar(
firstDay: DateTime.utc(2023, 1, 1),
lastDay: DateTime.utc(2024, 12, 31),
focusedDay: _focusedDay,
calendarFormat: _calendarFormat,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
onDaySelected: _onDaySelected,
onFormatChanged: (format) {
if (_calendarFormat != format) {
setState(() {
_calendarFormat = format;
});
}
},
),
SizedBox(height: 20),
Expanded(
child: _selectedDay == null
? Center(child: Text('Please select a day'))
: _buildTimeSlotsList(),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _showAddTimeSlotDialog,
child: Icon(Icons.add),
),
);
}
void _onDaySelected(DateTime selectedDay, DateTime focusedDay) {
setState(() {
_selectedDay = DateTime(selectedDay.year, selectedDay.month, selectedDay.day);
_focusedDay = focusedDay;
});
}
Widget _buildTimeSlotsList() {
List<TimeSlot> slots = _availableSlots[_selectedDay!] ?? [];
return slots.isEmpty
? Center(child: Text('No available slots for this day'))
: ListView.builder(
itemCount: slots.length,
itemBuilder: (context, index) {
TimeSlot slot = slots[index];
return ListTile(
title: Text('${slot.startTime.format(context)} - ${slot.endTime.format(context)}'),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _removeTimeSlot(slot),
),
);
},
);
}
void _showAddTimeSlotDialog() {
TimeOfDay startTime = TimeOfDay(hour: 9, minute: 0);
TimeOfDay endTime = TimeOfDay(hour: 17, minute: 0);
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) {
return Dialog(
backgroundColor: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Add Time Schedule',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 24),
Text('Start Time'),
TimeInputWidget(
initialTime: startTime,
onChanged: (time) => startTime = time,
),
SizedBox(height: 16),
Text('End Time'),
TimeInputWidget(
initialTime: endTime,
onChanged: (time) => endTime = time,
),
SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
child: Text('Cancel'),
onPressed: () => Navigator.of(context).pop(),
),
SizedBox(width: 8),
ElevatedButton(
child: Text('Add'),
onPressed: () {
_addTimeSlot(startTime, endTime);
Navigator.of(context).pop();
},
),
],
),
],
),
),
);
},
),
);
}
void _addTimeSlot(TimeOfDay startTime, TimeOfDay endTime) {
if (_selectedDay == null) return;
setState(() {
_availableSlots.update(
_selectedDay!,
(value) => [...value, TimeSlot(startTime, endTime)],
ifAbsent: () => [TimeSlot(startTime, endTime)],
);
});
}
void _removeTimeSlot(TimeSlot slot) {
setState(() {
_availableSlots[_selectedDay!]?.remove(slot);
});
}
void _saveAllSlots() async {
setState(() {
_isLoading = true;
});
try {
for (var entry in _availableSlots.entries) {
final date = entry.key;
final slots = entry.value;
final docRef = FirebaseFirestore.instance
.collection('users')
.doc(userId)
.collection('availableSlots')
.doc(date.toIso8601String().split('T')[0]);
if (slots.isEmpty) {
await docRef.delete();
} else {
await docRef.set({
'date': Timestamp.fromDate(date),
'slots': slots.map((slot) => {
'startTime': Timestamp.fromDate(DateTime(
date.year, date.month, date.day, slot.startTime.hour, slot.startTime.minute)),
'endTime': Timestamp.fromDate(DateTime(
date.year, date.month, date.day, slot.endTime.hour, slot.endTime.minute)),
}).toList(),
});
}
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('All slots saved successfully')),
);
} catch (e) {
print('Error saving slots: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error saving slots')),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
}
class TimeSlot {
final TimeOfDay startTime;
final TimeOfDay endTime;
TimeSlot(this.startTime, this.endTime);
}
class TimeInputWidget extends StatefulWidget {
final TimeOfDay initialTime;
final ValueChanged<TimeOfDay> onChanged;
TimeInputWidget({required this.initialTime, required this.onChanged});
@override
_TimeInputWidgetState createState() => _TimeInputWidgetState();
}
class _TimeInputWidgetState extends State<TimeInputWidget> {
late TextEditingController _hourController;
late TextEditingController _minuteController;
@override
void initState() {
super.initState();
_hourController = TextEditingController(text: widget.initialTime.hour.toString().padLeft(2, '0'));
_minuteController = TextEditingController(text: widget.initialTime.minute.toString().padLeft(2, '0'));
}
@override
void dispose() {
_hourController.dispose();
_minuteController.dispose();
super.dispose();
}
void _updateTime() {
int hour = int.tryParse(_hourController.text) ?? 0;
int minute = int.tryParse(_minuteController.text) ?? 0;
widget.onChanged(TimeOfDay(hour: hour.clamp(0, 23), minute: minute.clamp(0, 59)));
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 50,
child: TextField(
controller: _hourController,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(2),
],
onChanged: (value) => _updateTime(),
decoration: InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(vertical: 8, horizontal: 4),
),
),
),
Text(' : ', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
SizedBox(
width: 50,
child: TextField(
controller: _minuteController,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(2),
],
onChanged: (value) => _updateTime(),
decoration: InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(vertical: 8, horizontal: 4),
),
),
),
],
);
}
}
这是用户显示数据的第二个
class BookingCalendarWidget extends StatefulWidget {
final String serviceProviderId;
final String serviceName;
final String servicePrice;
BookingCalendarWidget({
required this.serviceProviderId,
required this.serviceName,
required this.servicePrice,
});
@override
_BookingCalendarWidgetState createState() => _BookingCalendarWidgetState();
}
class _BookingCalendarWidgetState extends State<BookingCalendarWidget> {
CalendarFormat _calendarFormat = CalendarFormat.month;
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
Map<DateTime, List<TimeSlot>> _availableSlots = {};
bool _isLoading = true;
String _statusMessage = '';
@override
void initState() {
super.initState();
_fetchAvailableSlots();
}
void _fetchAvailableSlots() async {
setState(() {
_statusMessage = 'Fetching available slots...';
});
print("Fetching available slots for provider: ${widget.serviceProviderId}");
try {
final snapshot = await FirebaseFirestore.instance
.collection("Users")
.doc(widget.serviceProviderId)
.collection("availableSlots")
.get();
print("Number of documents fetched: ${snapshot.docs.length}");
Map<DateTime, List<TimeSlot>> slots = {};
for (var doc in snapshot.docs) {
print("Processing document: ${doc.id}");
final data = doc.data();
print("Document data: $data");
final date = DateTime.parse(doc.id);
print("Date: $date");
final List<dynamic> slotsData = data['slots'] ?? [];
print("Slots data: $slotsData");
slots[date] = slotsData.map((slot) {
final start = (slot['start'] as Timestamp).toDate();
final end = (slot['end'] as Timestamp).toDate();
return TimeSlot(
start: start,
end: end,
serviceName: widget.serviceName,
servicePrice: widget.servicePrice,
);
}).toList();
print("Processed slots for $date: ${slots[date]}");
}
print("All processed slots: $slots");
setState(() {
_availableSlots = slots;
_isLoading = false;
if (_availableSlots.isEmpty) {
_statusMessage = 'No available slots found for this service provider.';
} else {
_statusMessage = '';
}
});
} catch (e) {
print("Error fetching available slots: $e");
setState(() {
_isLoading = false;
_statusMessage = 'Error fetching available slots: $e';
});
}
}
@override
Widget build(BuildContext context) {
print("Building widget. isLoading: $_isLoading, statusMessage: $_statusMessage");
return Scaffold(
appBar: AppBar(
title: Text('Book ${widget.serviceName}'),
),
body: _isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text(_statusMessage),
],
),
)
: Column(
children: [
TableCalendar(
firstDay: DateTime.now(),
lastDay: DateTime.now().add(Duration(days: 365)),
focusedDay: _focusedDay,
calendarFormat: _calendarFormat,
selectedDayPredicate: (day) {
return isSameDay(_selectedDay, day);
},
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
},
onFormatChanged: (format) {
setState(() {
_calendarFormat = format;
});
},
eventLoader: (day) {
return _availableSlots[day] ?? [];
},
),
Expanded(
child: _selectedDay == null
? Center(child: Text('Select a day to see available slots'))
: _buildTimeSlotList(),
),
],
),
);
}
Widget _buildTimeSlotList() {
print("Building time slot list for selected day: $_selectedDay");
final slots = _availableSlots[_selectedDay!] ?? [];
print("Slots for selected day: $slots");
if (slots.isEmpty) {
return Center(child: Text('No available slots for this day'));
}
return ListView.builder(
itemCount: slots.length,
itemBuilder: (context, index) {
final slot = slots[index];
print("Rendering slot: ${slot.start} - ${slot.end}");
return ListTile(
title: Text('${_formatTime(slot.start)} - ${_formatTime(slot.end)}'),
subtitle: Text('${slot.serviceName} - ${slot.servicePrice}'),
onTap: () => _confirmBooking(slot),
);
},
);
}
String _formatTime(DateTime time) {
return DateFormat('HH:mm').format(time);
}
void _confirmBooking(TimeSlot slot) {
// ... (keep the existing _confirmBooking method)
}
}
class TimeSlot {
final DateTime start;
final DateTime end;
final String serviceName;
final String servicePrice;
TimeSlot({
required this.start,
required this.end,
required this.serviceName,
required this.servicePrice,
});
}
您不能期望此代码:
@override
void initState() {
super.initState();
_fetchAvailableSlots();
}
在 build() 开始之前完成,因此您将获得部分结果。 您将需要使用某种 FutureBuilder 在返回数据时进行加载显示。