403 lines
13 KiB
Dart
403 lines
13 KiB
Dart
import 'package:citycards_partner_flutter/core/app_router.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:table_calendar/table_calendar.dart';
|
|
import '../blocs/booking_bloc.dart';
|
|
import '../repositories/booking_repository.dart';
|
|
import '../viewmodels/booking_viewmodel.dart';
|
|
import '../models/booking_model.dart';
|
|
import 'booking_bottom_sheet.dart';
|
|
|
|
class BookingPage extends StatelessWidget {
|
|
const BookingPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocProvider(
|
|
create: (_) => BookingBloc(
|
|
viewModel: BookingViewModel(repository: BookingRepository()),
|
|
)..add(LoadBookings(DateTime.now())),
|
|
child: const _BookingView(),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BookingView extends StatelessWidget {
|
|
const _BookingView();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: Colors.white,
|
|
body: BlocBuilder<BookingBloc, BookingState>(
|
|
builder: (context, state) {
|
|
final bloc = context.read<BookingBloc>();
|
|
return SafeArea(
|
|
child: SingleChildScrollView(
|
|
physics: const BouncingScrollPhysics(),
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(bottom: 40.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
const SizedBox(height: 20),
|
|
_buildHeader(context),
|
|
const SizedBox(height: 16),
|
|
_buildMonthHeader(bloc, state),
|
|
const SizedBox(height: 10),
|
|
|
|
// 🗓️ Scrollable calendar section
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
height: 350,
|
|
child: _buildCalendar(context, bloc, state)),
|
|
),
|
|
const SizedBox(height: 20),
|
|
_buildAttractionLegend(),
|
|
const SizedBox(height: 20),
|
|
|
|
_buildRecurringButton(context),
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// ---------------- HEADER ----------------
|
|
Widget _buildHeader(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
InkWell(
|
|
onTap: () => Navigator.pop(context),
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(left: 24.0),
|
|
child: CircleAvatar(
|
|
radius: 20,
|
|
backgroundColor: const Color(0xffF95F62),
|
|
child:
|
|
const Icon(Icons.arrow_back, color: Colors.white, size: 22),
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
"Booking",
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 32,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(width: 25),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
|
child: Text(
|
|
"Easily schedule and manage your bookings anytime, anywhere. Fast, simple, and secure.",
|
|
textAlign: TextAlign.center,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 13,
|
|
color: Colors.black,
|
|
height: 1.4,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// ---------------- MONTH HEADER ----------------
|
|
Widget _buildMonthHeader(BookingBloc bloc, BookingState state) {
|
|
final formatter = DateFormat("MMM yyyy");
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () {
|
|
final prevMonth =
|
|
DateTime(state.focusedMonth.year, state.focusedMonth.month - 1, 1);
|
|
bloc.add(LoadBookings(prevMonth));
|
|
},
|
|
child: _navButton(Icons.keyboard_arrow_left_rounded),
|
|
),
|
|
Text(
|
|
formatter.format(state.focusedMonth),
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
GestureDetector(
|
|
onTap: () {
|
|
final nextMonth =
|
|
DateTime(state.focusedMonth.year, state.focusedMonth.month + 1, 1);
|
|
bloc.add(LoadBookings(nextMonth));
|
|
},
|
|
child: _navButton(Icons.keyboard_arrow_right_rounded),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _navButton(IconData icon) {
|
|
return Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xffF6F6F6),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(icon, color: Colors.black, size: 25),
|
|
);
|
|
}
|
|
|
|
// ---------------- CALENDAR ----------------
|
|
Widget _buildCalendar(
|
|
BuildContext context, BookingBloc bloc, BookingState state) {
|
|
return TableCalendar(
|
|
firstDay: DateTime(2020),
|
|
lastDay: DateTime(2030),
|
|
focusedDay: state.focusedMonth,
|
|
calendarFormat: CalendarFormat.month,
|
|
headerVisible: false,
|
|
availableGestures: AvailableGestures.none,
|
|
startingDayOfWeek: StartingDayOfWeek.monday,
|
|
daysOfWeekStyle: DaysOfWeekStyle(
|
|
weekdayStyle: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
color: Colors.black54,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
weekendStyle: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
color: Colors.black54,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
calendarStyle: CalendarStyle(
|
|
outsideDaysVisible: false,
|
|
markersMaxCount: 0,
|
|
defaultTextStyle:
|
|
GoogleFonts.poppins(color: Colors.black87, fontSize: 22),
|
|
weekendTextStyle: GoogleFonts.poppins(color: Colors.black87),
|
|
),
|
|
eventLoader: (day) {
|
|
return state.bookings
|
|
.where((b) =>
|
|
b.date.year == day.year &&
|
|
b.date.month == day.month &&
|
|
b.date.day == day.day)
|
|
.toList();
|
|
},
|
|
calendarBuilders: CalendarBuilders(
|
|
defaultBuilder: (context, date, _) {
|
|
final bookings = state.bookings
|
|
.where((b) =>
|
|
b.date.year == date.year &&
|
|
b.date.month == date.month &&
|
|
b.date.day == date.day)
|
|
.toList();
|
|
|
|
return GestureDetector(
|
|
onTap: () {
|
|
if (bookings.isNotEmpty) {
|
|
bloc.add(SelectDate(date));
|
|
showModalBottomSheet(
|
|
context: context,
|
|
backgroundColor: Colors.transparent,
|
|
isScrollControlled: true,
|
|
builder: (_) => BlocProvider.value(
|
|
value: bloc,
|
|
child: BookingBottomSheet(
|
|
date: date,
|
|
booking: bookings.first,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
},
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
(bookings.isNotEmpty && bookings.first.attractions.length > 3)
|
|
? Align(
|
|
alignment: Alignment.topRight,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 3, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xffF95F62),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Text(
|
|
"+${bookings.first.attractions.length - 3}",
|
|
style: GoogleFonts.poppins(
|
|
color: Colors.white,
|
|
fontSize: 8,
|
|
),
|
|
),
|
|
),
|
|
)
|
|
: Container(height: 15),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
"${date.day}",
|
|
style: GoogleFonts.poppins(
|
|
color: Colors.black,
|
|
fontWeight: FontWeight.w500,
|
|
fontSize: 13,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
if (bookings.isNotEmpty)
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
...List.generate(
|
|
bookings.first.attractions.length > 3
|
|
? 3
|
|
: bookings.first.attractions.length,
|
|
(i) => Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 2),
|
|
width: 5,
|
|
height: 5,
|
|
decoration: BoxDecoration(
|
|
color: Color(int.parse(
|
|
"0xff${bookings.first.attractions[i].colorHex.substring(1)}")),
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
// ---------------- LEGEND ----------------
|
|
Widget _buildAttractionLegend() {
|
|
final legends = [
|
|
{"name": "The Enchanted Garden", "color": "#4CAF50", "percent": "90%"},
|
|
{"name": "Central City Museum", "color": "#F48FB1", "percent": "50%"},
|
|
{"name": "Skyline Observatory", "color": "#BA68C8", "percent": "50%"},
|
|
{"name": "Historic Downtown Walking", "color": "#2196F3", "percent": "30%"},
|
|
{"name": "Scenic Riverfront Stroll", "color": "#3F51B5", "percent": "30%"},
|
|
{"name": "Cultural Arts District Tour", "color": "#8BC34A", "percent": "40%"},
|
|
];
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 20),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFFFF1EE),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
"Attraction Legend",
|
|
style: GoogleFonts.poppins(
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 16,
|
|
color: Colors.black,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
...legends.map((item) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 5),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 14,
|
|
height: 14,
|
|
decoration: BoxDecoration(
|
|
color:
|
|
Color(int.parse("0xff${item["color"]!.substring(1)}")),
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: Text(
|
|
item["name"]!,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 13,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.group, color: Colors.teal, size: 16),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
item["percent"]!,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.redAccent,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ---------------- BUTTON ----------------
|
|
Widget _buildRecurringButton(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.pushNamed(context, AppRouter.recurringBlockBasicInfo);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xffF95F62),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
),
|
|
child: Text(
|
|
"Add Recurring Block",
|
|
style: GoogleFonts.poppins(
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: 15,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|