Booking physical consultation , bugs fixed and Profile picture adding using firebase storage is complete. Co-authored-by: Jipson George <152465898+Jipson-cosq@users.noreply.github.com> Reviewed-on: cosqnet/telemednet#6 Co-authored-by: DhanshCOSQ <dhanshas@cosq.net> Co-committed-by: DhanshCOSQ <dhanshas@cosq.net>
554 lines
17 KiB
Dart
554 lines
17 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:medora/data/models/consultation_center.dart';
|
|
import 'package:medora/data/models/doctor.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:medora/route/route_names.dart';
|
|
|
|
class ConsultationTimeScreen extends StatefulWidget {
|
|
final Doctor doctor;
|
|
final ConsultationCenter selectedConsultation;
|
|
|
|
const ConsultationTimeScreen({
|
|
super.key,
|
|
required this.doctor,
|
|
required this.selectedConsultation,
|
|
});
|
|
|
|
@override
|
|
State<ConsultationTimeScreen> createState() => _ConsultationTimeScreenState();
|
|
}
|
|
|
|
class _ConsultationTimeScreenState extends State<ConsultationTimeScreen> {
|
|
DateTime? selectedDate;
|
|
String? selectedTime;
|
|
|
|
List<TimeSlot> getTimeSlotsForDay(String dayName) {
|
|
try {
|
|
final schedule = widget.selectedConsultation.weeklySchedule?.firstWhere(
|
|
(schedule) => schedule.day == dayName,
|
|
orElse: () => AvailabilitySchedule(
|
|
day: dayName,
|
|
timeSlots: [],
|
|
),
|
|
);
|
|
return schedule?.timeSlots ?? [];
|
|
} catch (e) {
|
|
debugPrint('Error getting time slots: $e');
|
|
return [];
|
|
}
|
|
}
|
|
|
|
DateTime? parseTimeString(String? timeStr) {
|
|
if (timeStr == null) return null;
|
|
|
|
try {
|
|
// Try parsing 12-hour format first
|
|
return DateFormat('h:mm a').parse(timeStr);
|
|
} catch (e) {
|
|
try {
|
|
// Try parsing 24-hour format
|
|
return DateFormat('HH:mm').parse(timeStr);
|
|
} catch (e) {
|
|
debugPrint('Error parsing time: $timeStr');
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
String get formattedAddress {
|
|
final parts = [
|
|
widget.selectedConsultation.floorBuilding,
|
|
widget.selectedConsultation.street,
|
|
widget.selectedConsultation.city,
|
|
widget.selectedConsultation.state,
|
|
widget.selectedConsultation.postalCode
|
|
].where((part) => part != null && part.isNotEmpty).toList();
|
|
return parts.join(', ');
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF5F7FF),
|
|
appBar: _buildAppBar(),
|
|
body: SingleChildScrollView(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildLocationInfo(),
|
|
const SizedBox(height: 24),
|
|
_buildDateSelection(),
|
|
const SizedBox(height: 24),
|
|
_buildTimeSlots(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
PreferredSizeWidget _buildAppBar() {
|
|
return AppBar(
|
|
backgroundColor: Colors.white,
|
|
elevation: 0,
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back, color: Colors.black87),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
title: Text(
|
|
'Select Date & Time',
|
|
style: GoogleFonts.poppins(
|
|
color: Colors.black87,
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 20,
|
|
),
|
|
),
|
|
centerTitle: true,
|
|
);
|
|
}
|
|
|
|
Widget _buildLocationInfo() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
widget.doctor.firstName ?? '',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
Text(
|
|
widget.doctor.speciality!,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Image.network(
|
|
widget.doctor.profileImage!,
|
|
width: 60,
|
|
height: 60,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return Container(
|
|
width: 60,
|
|
height: 60,
|
|
color: Colors.grey[300],
|
|
child:
|
|
Icon(Icons.person, size: 30, color: Colors.grey[600]),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const Divider(height: 24),
|
|
Text(
|
|
'Selected Location',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
widget.selectedConsultation.city ?? '',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Average consultation time: ${widget.selectedConsultation.averageDurationMinutes}',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDateSelection() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Select Date',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
height: 100,
|
|
child: ListView.builder(
|
|
scrollDirection: Axis.horizontal,
|
|
itemCount: 20, // Show next 20 days
|
|
itemBuilder: (context, index) {
|
|
final date = DateTime.now().add(Duration(days: index));
|
|
final isSelected = selectedDate?.day == date.day &&
|
|
selectedDate?.month == date.month &&
|
|
selectedDate?.year == date.year;
|
|
final isAvailable = _isDateAvailable(date);
|
|
|
|
return GestureDetector(
|
|
onTap: isAvailable
|
|
? () {
|
|
setState(() {
|
|
selectedDate = date;
|
|
selectedTime = null; // Reset time when date changes
|
|
});
|
|
}
|
|
: null,
|
|
child: Container(
|
|
width: 70,
|
|
margin: const EdgeInsets.only(right: 12),
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? Colors.blue
|
|
: isAvailable
|
|
? Colors.white
|
|
: Colors.grey[200],
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: isAvailable
|
|
? [
|
|
BoxShadow(
|
|
color: Colors.grey.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
]
|
|
: null,
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
DateFormat('EEE').format(date).toUpperCase(),
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: isSelected
|
|
? Colors.white
|
|
: isAvailable
|
|
? Colors.grey[600]
|
|
: Colors.grey[400],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
date.day.toString(),
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w600,
|
|
color: isSelected
|
|
? Colors.white
|
|
: isAvailable
|
|
? Colors.black87
|
|
: Colors.grey[400],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildTimeSlots() {
|
|
if (selectedDate == null) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.5),
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
'Please select a date to view available time slots',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
final dayName = DateFormat('EEEE').format(selectedDate!);
|
|
final timeSlots = getTimeSlotsForDay(dayName);
|
|
final allTimeSlots = _generateTimeSlots(timeSlots);
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Select Time',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.grey.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Wrap(
|
|
spacing: 12,
|
|
runSpacing: 12,
|
|
children: allTimeSlots.map((time) {
|
|
final isSelected = selectedTime == time;
|
|
final isAvailable = _isTimeSlotAvailable(time);
|
|
|
|
return GestureDetector(
|
|
onTap: isAvailable
|
|
? () {
|
|
Navigator.pushNamed(
|
|
context,
|
|
RouteNames.consultationBookingScreen,
|
|
arguments: {
|
|
'doctor': widget.doctor,
|
|
'selectedConsultation': widget.selectedConsultation,
|
|
'selectedDate': selectedDate,
|
|
'selectedTime': time
|
|
},
|
|
);
|
|
}
|
|
: null,
|
|
child: Container(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? Colors.blue
|
|
: isAvailable
|
|
? Colors.white
|
|
: Colors.grey[200],
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: isSelected
|
|
? Colors.blue
|
|
: isAvailable
|
|
? Colors.grey.withOpacity(0.2)
|
|
: Colors.grey.withOpacity(0.1),
|
|
),
|
|
),
|
|
child: Text(
|
|
time,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: isSelected
|
|
? Colors.white
|
|
: isAvailable
|
|
? Colors.black87
|
|
: Colors.grey[400],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
bool _isDateAvailable(DateTime date) {
|
|
final dayName = DateFormat('EEEE').format(date);
|
|
return widget.selectedConsultation.weeklySchedule
|
|
?.any((schedule) => schedule.day == dayName) ??
|
|
false;
|
|
}
|
|
|
|
List<String> _generateTimeSlots(List<TimeSlot> timeSlots) {
|
|
final slots = <String>[];
|
|
final timeFormat = DateFormat('h:mm a');
|
|
|
|
for (var slot in timeSlots) {
|
|
final startTime = parseTimeString(slot.startTime);
|
|
final endTime = parseTimeString(slot.endTime);
|
|
|
|
if (startTime == null || endTime == null) continue;
|
|
|
|
var currentTime = startTime;
|
|
while (currentTime.isBefore(endTime)) {
|
|
slots.add(timeFormat.format(currentTime));
|
|
currentTime = currentTime.add(const Duration(minutes: 30));
|
|
}
|
|
}
|
|
|
|
return slots;
|
|
}
|
|
|
|
bool _isTimeSlotAvailable(String time) {
|
|
final now = DateTime.now();
|
|
|
|
if (selectedDate == null) return false;
|
|
|
|
// Parse the time slot
|
|
final timeSlot = parseTimeString(time);
|
|
if (timeSlot == null) return false;
|
|
|
|
// Create a DateTime combining selected date and time
|
|
final slotDateTime = DateTime(
|
|
selectedDate!.year,
|
|
selectedDate!.month,
|
|
selectedDate!.day,
|
|
timeSlot.hour,
|
|
timeSlot.minute,
|
|
);
|
|
|
|
// Check if the slot is in the past
|
|
if (slotDateTime.isBefore(now)) return false;
|
|
|
|
// Here you would typically check against your booking database
|
|
// For now, returning true for future slots
|
|
return true;
|
|
}
|
|
|
|
void _handleBooking() {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
title: Text(
|
|
'Confirm Booking',
|
|
style: GoogleFonts.poppins(
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildConfirmationDetail(
|
|
'Doctor',
|
|
widget.doctor.firstName!,
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildConfirmationDetail(
|
|
'Location',
|
|
widget.selectedConsultation.city!,
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildConfirmationDetail(
|
|
'Date',
|
|
DateFormat('EEEE, MMMM d').format(selectedDate!),
|
|
),
|
|
const SizedBox(height: 8),
|
|
_buildConfirmationDetail(
|
|
'Time',
|
|
selectedTime!,
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: Text(
|
|
'Cancel',
|
|
style: GoogleFonts.poppins(
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: Text(
|
|
'Confirm',
|
|
style: GoogleFonts.poppins(
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildConfirmationDetail(String label, String value) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SizedBox(
|
|
width: 80,
|
|
child: Text(
|
|
'$label:',
|
|
style: GoogleFonts.poppins(
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Text(
|
|
value,
|
|
style: GoogleFonts.poppins(
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|