Files
2025-10-29 18:55:48 +05:30

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