diff --git a/README.md b/README.md index 79da0e6..c22ac61 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,40 @@ -# ๐Ÿฝ๏ธ MessApp Frontend +# ๐Ÿฝ๏ธ MessApp Frontend A Flutter-based frontend for the Mess Management App. -This repository only contains the mobile frontend UI & state management. -โžก๏ธ Backend and Firebase setup are not included here. +This repository contains the mobile frontend UI & state management. +โžก๏ธ Backend and Firebase setup need to be configured after cloning. --- -## ๐Ÿ“Œ Features +## ๐Ÿ“Œ Features -- Role-based navigation (Student, Admin) -- Digital Mess Card view -- Student photo upload page -- Mess menu & dinner provider with state management -- Organized Flutter architecture (core, models, pages, providers) +- Role-based navigation (Student, Admin) +- Digital Mess Card view +- Student photo upload page +- Mess menu & dinner provider with state management +- Organized Flutter architecture (core, models, pages, providers) --- -## ๐Ÿš€ Tech Stack +## ๐Ÿš€ Tech Stack -- Flutter (Dart) -- Provider (State management) -- SharedPreferences (Local storage) +- Flutter (Dart) +- Provider (State management) +- SharedPreferences (Local storage) +- Firebase: Authentication, Firestore, Storage --- -## โœ… Prerequisites +## โœ… Prerequisites -Before running the app, install: +Before running the app, install: -- Flutter SDK (latest stable) -- Android Studio or VS Code -- Xcode (macOS only, for iOS builds) -- Emulator or physical device +- Flutter SDK (latest stable) +- Android Studio or VS Code +- Xcode (macOS only, for iOS builds) +- Emulator or physical device -Check Flutter setup: +Check Flutter setup: ```bash flutter doctor @@ -41,61 +42,95 @@ flutter doctor --- -## ๐Ÿ“ฆ Installation +## ๐Ÿ“ฆ Installation -1. Clone this repository +### 1. Clone this repository ```bash -git clone -cd MessApp +git clone +cd MessApp ``` -2. Install dependencies +### 2. Install dependencies ```bash -flutter pub get +flutter pub get ``` --- -## โ–ถ๏ธ Running the App +## ๐Ÿ”ง Firebase Setup -### Android +### 1. Create a Firebase Project: + +- Go to [Firebase Console](https://console.firebase.google.com/) โ†’ Add Project โ†’ Follow steps. + +### 2. Add Your Apps: + +**Android:** +- Add your app using the package name (e.g., `com.example.messapp`) +- Download `google-services.json` โ†’ Place it in `android/app/` + +**iOS:** +- Add your app using the Bundle ID +- Download `GoogleService-Info.plist` โ†’ Place it in `ios/Runner/` +- Open Xcode โ†’ Add `GoogleService-Info.plist` to the Runner target + +### 3. Enable Firebase Services: + +- **Authentication:** Enable Email/Password (or Google Sign-In if required) +- **Firestore:** Create collections like `users` and `mess` +- **Storage:** Create a storage bucket for profile pictures + +๐Ÿ”น Firebase initialization is already implemented in the code, so no further code changes are needed. You just need to add the config files. + +--- + +## โ–ถ๏ธ Running the App + +### Android ```bash -flutter run +flutter run ``` -### iOS (macOS only) +### iOS (macOS only) ```bash -flutter run -d ios +flutter run -d ios ``` -### Web +### Web ```bash -flutter run -d chrome +flutter run -d chrome ``` --- -## ๐Ÿ“‚ Project Structure +## ๐Ÿ“‚ Project Structure ``` -lib/ - โ”œโ”€โ”€ core/ โ†’ App-level utilities, themes, constants - โ”œโ”€โ”€ models/ โ†’ Data models (user, mess, menu, etc.) - โ”œโ”€โ”€ pages/ โ†’ Screens and UI pages - โ”œโ”€โ”€ providers/ โ†’ State management logic - โ””โ”€โ”€ main.dart โ†’ Entry point of the app +lib/ + โ”œโ”€โ”€ core/ โ†’ App-level utilities, themes, constants + โ”œโ”€โ”€ models/ โ†’ Data models (user, mess, menu, etc.) + โ”œโ”€โ”€ pages/ โ†’ Screens and UI pages + โ”œโ”€โ”€ providers/ โ†’ State management logic + โ””โ”€โ”€ main.dart โ†’ Entry point of the app ``` --- -## ๐Ÿงน Useful Commands +## ๐Ÿงน Useful Commands + +- Clean build cache โ†’ `flutter clean` +- Analyze code โ†’ `flutter analyze` +- Run tests (if available) โ†’ `flutter test` + +--- -- Clean build cache โ†’ `flutter clean` -- Analyze code โ†’ `flutter analyze` -- Run tests (if available) โ†’ `flutter test` +## ๐Ÿ” Notes +- SharedPreferences stores Auth Tokens and Email information locally. +- Provider handles UI state like menu selection, mess card balance, and photo upload. +- Firebase is already integrated; just ensure you add the correct configuration files for your project. diff --git a/lib/Pages/admin_login_page.dart b/lib/Pages/admin_login_page.dart index 7a3892f..bdb5299 100644 --- a/lib/Pages/admin_login_page.dart +++ b/lib/Pages/admin_login_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:mess_management_app/Pages/admin_page.dart'; import 'package:mess_management_app/Services/admin_service.dart'; import 'package:mess_management_app/services/admin_auth_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class AdminLoginPage extends StatefulWidget { const AdminLoginPage({super.key}); diff --git a/lib/Pages/admin_page.dart b/lib/Pages/admin_page.dart index 9e3c860..e034365 100644 --- a/lib/Pages/admin_page.dart +++ b/lib/Pages/admin_page.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; import 'package:mess_management_app/services/admin_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'role_selection_page.dart'; class AdminPage extends StatefulWidget { @@ -19,24 +20,37 @@ class _AdminPageState extends State { File? _studentFile; File? _menuFile; - // For menu editing - final Map _controllers = { - "Breakfast": TextEditingController(), - "Lunch": TextEditingController(), - "Dinner": TextEditingController(), - }; + // For special token button + late String msg = "Start Session"; + + // Meal items + List breakfastItems = []; + List lunchItems = []; + List dinnerItems = []; + + @override + void initState() { + super.initState(); + _initTokenSession(); + _loadTodayMenu(); + } @override void dispose() { - _controllers.forEach((key, controller) => controller.dispose()); super.dispose(); } + Future _initTokenSession() async { + final sessionActive = await _adminService.tokenSession(); + setState(() { + msg = sessionActive ? "End Session" : "Start Session"; + }); + } + // ---- FILE PICKERS ---- Future _pickStudentFile() async { FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.any, - //allowedExtensions: ['csv'], ); if (result != null && result.files.single.path != null) { @@ -50,7 +64,6 @@ class _AdminPageState extends State { Future _pickMenuFile() async { FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.any, - //allowedExtensions: ['csv'], ); if (result != null && result.files.single.path != null) { @@ -70,6 +83,14 @@ class _AdminPageState extends State { ); } + Future _uploadMenu() async { + if (_menuFile == null) return; + final success = await _adminService.uploadMenu(_menuFile!); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(success ? "Menu uploaded successfully" : "Menu upload failed")), + ); + } + Future _resetStudents() async { final success = await _adminService.resetStudents(); ScaffoldMessenger.of(context).showSnackBar( @@ -77,31 +98,66 @@ class _AdminPageState extends State { ); } - Future _uploadMenu() async { - if (_menuFile == null) return; - final success = await _adminService.uploadMenu(_menuFile!); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(success ? "Menu uploaded successfully" : "Menu upload failed")), - ); + Future _loadTodayMenu() async { + try { + final menu = await _adminService.fetchMenu(); // fetch today's menu + if (menu != null) { + setState(() { + breakfastItems = List.from(menu['breakfast'] ?? []); + lunchItems = List.from(menu['lunch'] ?? []); + dinnerItems = List.from(menu['dinner'] ?? []); + }); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Failed to load menu: $e")), + ); + } } - Future _updateDayMenu(String day) async { + Future _updateDayMenu() async { + final now = DateTime.now(); + final currentDay = [ + "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" + ][now.weekday - 1]; + final success = await _adminService.updateDayMenu( - day, - _controllers["Breakfast"]!.text.split(","), - _controllers["Lunch"]!.text.split(","), - _controllers["Dinner"]!.text.split(","), + currentDay, + breakfastItems, + lunchItems, + dinnerItems, ); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(success ? "Menu for $currentDay updated" : "Update failed")), + ); + } + + Future _startToken() async { + final success = await _adminService.startToken(); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(success ? "Menu for $day updated" : "Update failed")), + SnackBar(content: Text(success ? "Special token issued" : "Failed to start token")), ); + + if (success) setState(() => msg = "End Session"); } - Future _issueToken() async { - final success = await _adminService.issueToken(); + Future _endToken() async { + final success = await _adminService.endToken(); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(success ? "Special token issued" : "Failed to issue token")), + SnackBar(content: Text(success ? "Special token ended" : "Failed to end token")), ); + + if (success) setState(() => msg = "Start Session"); + } + + Future _tokenSystem() async { + final session = await _adminService.tokenSession(); + if (session) { + await _endToken(); + } else { + await _startToken(); + } } Future _logout() async { @@ -114,6 +170,76 @@ class _AdminPageState extends State { ); } + // Helper widget for meals + // Helper widget for meals + Widget _mealSection(String title, List items) { + return Card( + margin: const EdgeInsets.symmetric(vertical: 8), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + ...items.asMap().entries.map((entry) { + int index = entry.key; + String item = entry.value; + return ListTile( + title: Text(item), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.edit), + onPressed: () async { + String? edited = await _showEditDialog(item,"Edit Item"); + if (edited != null && edited.isNotEmpty) { + setState(() => items[index] = edited); + } + }, + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () => setState(() => items.removeAt(index)), + ), + ], + ), + ); + }).toList(), + TextButton.icon( + onPressed: () async { + String? newItem = await _showEditDialog("","Add Item"); // empty string for new item + if (newItem != null && newItem.isNotEmpty) { + setState(() => items.add(newItem)); + } + }, + icon: const Icon(Icons.add), + label: const Text("Add Item"), + ), + ], + ), + ), + ); + } + + + // Dialog to edit item + Future _showEditDialog(String current,String msg) { + final controller = TextEditingController(text: current); + return showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(msg), + content: TextField(controller: controller), + actions: [ + TextButton(onPressed: () => Navigator.pop(context), child: const Text("Cancel")), + TextButton(onPressed: () => Navigator.pop(context, controller.text), child: const Text("Save")), + ], + ), + ); + } + @override Widget build(BuildContext context) { final pages = [ @@ -123,33 +249,11 @@ class _AdminPageState extends State { children: [ const Text("Edit Mess Menu", style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), const SizedBox(height: 16), - ..._controllers.entries.map((entry) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: TextField( - controller: entry.value, - decoration: InputDecoration( - labelText: entry.key, - border: const OutlineInputBorder(), - hintText: "Enter items separated by comma", - ), - ), - ); - }), + _mealSection("Breakfast", breakfastItems), + _mealSection("Lunch", lunchItems), + _mealSection("Dinner", dinnerItems), ElevatedButton( - onPressed: () { - final now = DateTime.now(); - final currentDay = [ - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday" - ][now.weekday - 1]; - _updateDayMenu(currentDay); - }, + onPressed: _updateDayMenu, child: const Text("Save Today's Menu"), ), const Divider(), @@ -170,8 +274,8 @@ class _AdminPageState extends State { // ---- Special Token Page ---- Center( child: ElevatedButton( - onPressed: _issueToken, - child: const Text("Issue Special Dinner Token"), + onPressed: _tokenSystem, + child: Text(msg), ), ), diff --git a/lib/Pages/digital_mess_card.dart b/lib/Pages/digital_mess_card.dart index be8ca6e..9be2342 100644 --- a/lib/Pages/digital_mess_card.dart +++ b/lib/Pages/digital_mess_card.dart @@ -120,7 +120,7 @@ class _SpecialDinnerPageState extends State Text(provider.mess, style: const TextStyle(fontSize: 16)), const SizedBox(height: 10), - if (!provider.hasEaten) + if (provider.token) ElevatedButton( onPressed: () => _confirmRedeem(context), child: const Text("Redeem Special Dinner"), @@ -131,7 +131,7 @@ class _SpecialDinnerPageState extends State ], ), ), - if (!provider.hasEaten) + if (provider.token) Positioned( top: 8, right: 8, diff --git a/lib/Providers/dinner_provider.dart b/lib/Providers/dinner_provider.dart index e4a14d8..12d61d5 100644 --- a/lib/Providers/dinner_provider.dart +++ b/lib/Providers/dinner_provider.dart @@ -9,7 +9,7 @@ class DinnerProvider extends ChangeNotifier { String email = ""; String photoUrl = ""; bool hasUploadedPhoto = false; - bool hasEaten = false; + bool token = false; String mess = ""; String rollNo = ""; @@ -23,8 +23,9 @@ class DinnerProvider extends ChangeNotifier { hasUploadedPhoto = data["hasUploadedPhoto"] ?? false; mess = data["mess"] ?? ""; rollNo = data["rollNo"] ?? ""; + token = (data["specialToken"]["active"] == true && + data["specialToken"]["redeemed"] == false); - hasEaten = data["specialToken"]?["redeemed"] ?? false; notifyListeners(); } @@ -34,7 +35,7 @@ class DinnerProvider extends ChangeNotifier { final success = await _dinnerService.syncTokenToBackend(true); if (success) { print("changed"); - hasEaten = true; + token = false; notifyListeners(); } } @@ -44,7 +45,7 @@ class DinnerProvider extends ChangeNotifier { final success = await _dinnerService.syncTokenToBackend(false); if (success) { print("changed"); - hasEaten = false; + token = true; notifyListeners(); } } diff --git a/lib/Services/admin_service.dart b/lib/Services/admin_service.dart index e77b81a..ce16e71 100644 --- a/lib/Services/admin_service.dart +++ b/lib/Services/admin_service.dart @@ -101,18 +101,73 @@ class AdminService { return response.statusCode == 200; } - // ============ ISSUE SPECIAL TOKEN ============ - Future issueToken() async { + // ============ START SPECIAL TOKEN ============ + Future startToken() async { final token = await _getToken(); if (token == null) return false; final response = await http.post( - Uri.parse("$baseUrl/special-dinner/redeem"), + Uri.parse("$baseUrl/special-dinner/start"), + headers: {"Authorization": "Bearer $token"}, + ); + final body = jsonDecode(response.body); + print(body['error']); + print(body['message']); + return response.statusCode == 200; + } + // ============ START SPECIAL TOKEN ============ + Future endToken() async { + final token = await _getToken(); + if (token == null) return false; + + final response = await http.post( + Uri.parse("$baseUrl/special-dinner/end"), headers: {"Authorization": "Bearer $token"}, ); return response.statusCode == 200; } + //===========Check if Session Began======= + Future tokenSession() async { + final token = await _getToken(); + if (token == null) return false; + + final response = await http.get( + Uri.parse("$baseUrl/tokenSession"), + headers: {"Authorization": "Bearer $token"}, + ); + + if (response.statusCode == 200) { + final body = jsonDecode(response.body); + print("Token session status: ${body['active']}"); + return body['active']; + } else { + print("Failed to get token session: ${response.body}"); + return false; + } + } + +// ============ FETCH MENU ============ + Future?> fetchMenu() async { + final backendUrl = '$baseUrl/menu'; + final token = await _getToken(); + + if (token == null) return null; + + final response = await http.get( + Uri.parse(backendUrl), + headers: {"Authorization": "Bearer $token"}, + ); + + if (response.statusCode == 200) { + print(jsonDecode(response.body) ); + return jsonDecode(response.body) as Map; + } else { + print(response.body); + throw Exception("Failed to load menu: ${response.body}"); + } + } + // ============ LOGOUT ============ Future logout() async {