Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
Expand Down Expand Up @@ -46,7 +47,7 @@ void main() async {

FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

runApp(const MyApp());
runApp(const ProviderScope(child: MyApp()));
}

Future<void> _initializeNotifications() async {
Expand Down
104 changes: 104 additions & 0 deletions frontend/lib/providers/timetable_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import 'package:dashbaord/models/lecture_model.dart';
import 'package:dashbaord/models/time_table_model.dart';
import 'package:dashbaord/services/api_service.dart';
import 'package:dashbaord/services/shared_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// Provider for timetable state management
final timetableProvider = StateNotifierProvider<TimetableNotifier, AsyncValue<Timetable?>>((ref) {
return TimetableNotifier();
});

/// Notifier class for managing timetable state
class TimetableNotifier extends StateNotifier<AsyncValue<Timetable?>> {
TimetableNotifier() : super(const AsyncValue.loading());

/// Fetch timetable from API or local storage
Future<void> fetchTimetable(BuildContext context, {bool isGuest = false}) async {
try {
state = const AsyncValue.loading();

Timetable? localTimetable = await SharedService().getTimetable();
Timetable? response;

if (!isGuest) {
response = await ApiServices().getTimetable(context);
}

if (response == null) {
if (localTimetable == null) {
state = AsyncValue.data(Timetable(courses: {}, slots: []));
return;
} else {
localTimetable.cleanUp();
state = AsyncValue.data(localTimetable);
return;
}
} else {
response.cleanUp();
state = AsyncValue.data(response);
await SharedService().saveTimetable(response);
}
} catch (e, stackTrace) {
state = AsyncValue.error(e, stackTrace);
}
}

/// Add a course to the timetable
Future<void> addCourse({
required String courseCode,
required String courseName,
required List<Lecture> lectures,
String? classRoom,
String? slot,
}) async {
final currentTimetable = state.value;
if (currentTimetable == null) return;

try {
final updatedTimetable = currentTimetable.addCourse(
courseCode,
courseName,
lectures,
classRoom: classRoom,
slot: slot,
);

state = AsyncValue.data(updatedTimetable);

// Save to backend and local storage
final res = await ApiServices().postTimetable(updatedTimetable);
if (res['status'] == 200) {
await SharedService().saveTimetable(updatedTimetable);
}
} catch (e, stackTrace) {
state = AsyncValue.error(e, stackTrace);
}
}

/// Update the entire timetable
Future<void> updateTimetable(Timetable timetable) async {
try {
state = AsyncValue.data(timetable);

// Save to backend and local storage
final res = await ApiServices().postTimetable(timetable);
if (res['status'] == 200) {
await SharedService().saveTimetable(timetable);
}
} catch (e, stackTrace) {
state = AsyncValue.error(e, stackTrace);
}
}

/// Set timetable directly (useful for shared timetables)
void setTimetable(Timetable? timetable) {
state = AsyncValue.data(timetable);
}

/// Clear timetable
void clearTimetable() {
state = AsyncValue.data(Timetable(courses: {}, slots: []));
}
}
146 changes: 78 additions & 68 deletions frontend/lib/screens/calendar_screen.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:dashbaord/extensions.dart';
import 'package:dashbaord/models/lecture_model.dart';
import 'package:dashbaord/models/time_table_model.dart';
import 'package:dashbaord/providers/timetable_provider.dart';
import 'package:dashbaord/services/api_service.dart';
import 'package:dashbaord/services/shared_service.dart';
import 'package:dashbaord/widgets/custom_appbar.dart';
Expand All @@ -16,11 +17,12 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:calendar_view/calendar_view.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:share_plus/share_plus.dart';

class CalendarScreen extends StatefulWidget {
class CalendarScreen extends ConsumerStatefulWidget {
final Timetable? timetable;
final Function(String, String, List<Lecture>, String?, String?)?
onLectureAdded;
Expand All @@ -34,13 +36,12 @@ class CalendarScreen extends StatefulWidget {
});

@override
State<CalendarScreen> createState() => _CalendarScreenState();
ConsumerState<CalendarScreen> createState() => _CalendarScreenState();
}

class _CalendarScreenState extends State<CalendarScreen> {
class _CalendarScreenState extends ConsumerState<CalendarScreen> {
String selectedViewType = "List";
List<String> viewTypeList = ["List", "Day", "Week", "Month"];
Timetable? timetable;
DateTime? initialDate = DateTime.now();
final List<CalendarEventData> _events = [];

Expand Down Expand Up @@ -98,14 +99,15 @@ class _CalendarScreenState extends State<CalendarScreen> {
@override
void initState() {
super.initState();
timetable = widget.timetable;
WidgetsBinding.instance.addPostFrameCallback((_) {
requestNotifPerms(context);
});
}

@override
Widget build(BuildContext context) {
final timetableAsyncValue = ref.watch(timetableProvider);

return CalendarControllerProvider(
controller: EventController()..addAll(_events),
child: Scaffold(
Expand All @@ -118,15 +120,10 @@ class _CalendarScreenState extends State<CalendarScreen> {
iconColor: context.customColors.customAccentColor,
onSelected: (value) async {
if (value == "refresh") {
final response = await ApiServices().getTimetable(context);
if (response == null) {
await ref.read(timetableProvider.notifier).fetchTimetable(context);
if (ref.read(timetableProvider).hasError) {
showError(msg: "Failed to fetch timetable");
return;
}
setState(() {
timetable = response;
});
SharedService().saveTimetable(response);
} else if (value == "manageCourses") {
_showManageCoursesBottomSheet(context);
} else if (value == "shareCode") {
Expand Down Expand Up @@ -178,61 +175,65 @@ class _CalendarScreenState extends State<CalendarScreen> {
)
],
),
body: Column(
children: [
const SizedBox(
height: 2,
),
ToggleButtons(
isSelected: viewTypeList
.map((viewType) => viewType == selectedViewType)
.toList(),
onPressed: (int index) {
setState(() {
selectedViewType = viewTypeList[index];
});
},
borderRadius: BorderRadius.circular(10),
selectedColor: Colors.white,
fillColor: context.customColors.customAccentColor,
constraints: BoxConstraints(
minHeight: 40.0,
minWidth: MediaQuery.of(context).size.width * 2 / 9,
body: timetableAsyncValue.when(
data: (timetable) => Column(
children: [
const SizedBox(
height: 2,
),
children: viewTypeList.map((String viewType) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
viewType,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: viewType == selectedViewType
? Colors.white
: Theme.of(context).textTheme.bodyLarge?.color,
ToggleButtons(
isSelected: viewTypeList
.map((viewType) => viewType == selectedViewType)
.toList(),
onPressed: (int index) {
setState(() {
selectedViewType = viewTypeList[index];
});
},
borderRadius: BorderRadius.circular(10),
selectedColor: Colors.white,
fillColor: context.customColors.customAccentColor,
constraints: BoxConstraints(
minHeight: 40.0,
minWidth: MediaQuery.of(context).size.width * 2 / 9,
),
children: viewTypeList.map((String viewType) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
viewType,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: viewType == selectedViewType
? Colors.white
: Theme.of(context).textTheme.bodyLarge?.color,
),
),
),
);
}).toList(),
),
const SizedBox(height: 10),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _getCurrentView(context),
);
}).toList(),
),
),
SizedBox(
height: 10,
)
],
const SizedBox(height: 10),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _getCurrentView(context, timetable),
),
),
SizedBox(
height: 10,
)
],
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error loading timetable')),
),
floatingActionButton: _buildFABs(),
),
);
}

Widget _getCurrentView(BuildContext context) {
Widget _getCurrentView(BuildContext context, Timetable? timetable) {
switch (selectedViewType) {
case "List":
return ListViewScreen(
Expand Down Expand Up @@ -267,16 +268,20 @@ class _CalendarScreenState extends State<CalendarScreen> {
}

void _showAddEventBottomSheet(BuildContext context) {
final timetable = ref.read(timetableProvider).value;
showModalBottomSheet(
context: context,
builder: (context) {
return AddLectureBottomSheet(
timetable: timetable,
onLectureAdded: (courseCode, courseName, lectures, classRoom, slot, segment) {
setState(() {
timetable =
timetable!.addCourse(courseCode, courseName, lectures);
});
onLectureAdded: (courseCode, courseName, lectures, classRoom, slot, segment) async {
await ref.read(timetableProvider.notifier).addCourse(
courseCode: courseCode,
courseName: courseName,
lectures: lectures,
classRoom: classRoom,
slot: slot,
);
widget.onLectureAdded!(
courseCode, courseName, lectures, classRoom, slot);
},
Expand All @@ -288,15 +293,14 @@ class _CalendarScreenState extends State<CalendarScreen> {
}

void _showManageCoursesBottomSheet(BuildContext context) {
final timetable = ref.read(timetableProvider).value;
showModalBottomSheet(
context: context,
builder: (context) {
return ManageCoursesBottomSheet(
timetable: timetable,
onEditTimetable: (editedTimetable) {
setState(() {
timetable = editedTimetable;
});
onEditTimetable: (editedTimetable) async {
await ref.read(timetableProvider.notifier).updateTimetable(editedTimetable);
widget.onEditTimetable!(editedTimetable);
},
);
Expand Down Expand Up @@ -324,7 +328,13 @@ class _CalendarScreenState extends State<CalendarScreen> {
}

void _shareSchedule() async {
final courseDetails = timetable!.courses.entries.map((entry) {
final timetable = ref.read(timetableProvider).value;
if (timetable == null) {
showError(msg: "No timetable to share");
return;
}

final courseDetails = timetable.courses.entries.map((entry) {
final code = entry.key;
final name = entry.value['title'];
return '$code: $name';
Expand Down
Loading