├── .gitignore ├── frontend ├── src │ ├── components │ │ ├── WeekMenu │ │ │ ├── index.module.css │ │ │ └── index.js │ │ └── MenuBar │ │ │ ├── index.module.css │ │ │ └── index.js │ ├── utility │ │ ├── fade.module.css │ │ ├── removeLoader.js │ │ ├── fade.js │ │ └── routeAnimation.js │ ├── index.css │ ├── routes │ │ ├── Schedule │ │ │ ├── index.module.css │ │ │ └── index.js │ │ ├── QRCode │ │ │ ├── index.module.css │ │ │ └── index.js │ │ ├── TotalMeals │ │ │ ├── index.module.css │ │ │ └── index.js │ │ ├── PurchaseHistory │ │ │ ├── index.module.css │ │ │ └── index.js │ │ ├── Buy │ │ │ ├── index.module.css │ │ │ └── index.js │ │ ├── ScanQR │ │ │ ├── index.module.css │ │ │ └── index.js │ │ └── AdminPanel │ │ │ ├── index.module.css │ │ │ └── index.js │ ├── index.js │ └── App.js ├── build │ ├── favicon.ico │ ├── assets │ │ └── icon.png │ ├── asset-manifest.json │ ├── index.html │ └── static │ │ └── js │ │ └── main.d9d36117.js.LICENSE.txt ├── public │ ├── favicon.ico │ ├── assets │ │ └── icon.png │ └── index.html ├── .gitignore └── package.json ├── assets ├── payment.jpg ├── play_yt.jpg ├── qr_code.jpg ├── scan_qr.jpg ├── admin_panel.jpg ├── time_menu.jpg ├── total_meals.jpg ├── google_signin.jpg ├── purchase_page.jpg └── purchase_history.jpg ├── models ├── User.js ├── Menu.js ├── Order.js ├── Time.js └── Buyer.js ├── package.json ├── routes ├── data.js ├── auth.js ├── admin.js └── user.js ├── SETUP.md ├── config └── passport.js ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | config.env -------------------------------------------------------------------------------- /frontend/src/components/WeekMenu/index.module.css: -------------------------------------------------------------------------------- 1 | .table { 2 | margin: 0.5rem; 3 | } -------------------------------------------------------------------------------- /assets/payment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashroshan/mess-management-system/HEAD/assets/payment.jpg -------------------------------------------------------------------------------- /assets/play_yt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashroshan/mess-management-system/HEAD/assets/play_yt.jpg -------------------------------------------------------------------------------- /assets/qr_code.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashroshan/mess-management-system/HEAD/assets/qr_code.jpg -------------------------------------------------------------------------------- /assets/scan_qr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashroshan/mess-management-system/HEAD/assets/scan_qr.jpg -------------------------------------------------------------------------------- /assets/admin_panel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashroshan/mess-management-system/HEAD/assets/admin_panel.jpg -------------------------------------------------------------------------------- /assets/time_menu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashroshan/mess-management-system/HEAD/assets/time_menu.jpg -------------------------------------------------------------------------------- /assets/total_meals.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashroshan/mess-management-system/HEAD/assets/total_meals.jpg -------------------------------------------------------------------------------- /assets/google_signin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashroshan/mess-management-system/HEAD/assets/google_signin.jpg -------------------------------------------------------------------------------- /assets/purchase_page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashroshan/mess-management-system/HEAD/assets/purchase_page.jpg -------------------------------------------------------------------------------- /assets/purchase_history.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashroshan/mess-management-system/HEAD/assets/purchase_history.jpg -------------------------------------------------------------------------------- /frontend/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashroshan/mess-management-system/HEAD/frontend/build/favicon.ico -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashroshan/mess-management-system/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/build/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashroshan/mess-management-system/HEAD/frontend/build/assets/icon.png -------------------------------------------------------------------------------- /frontend/public/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dashroshan/mess-management-system/HEAD/frontend/public/assets/icon.png -------------------------------------------------------------------------------- /frontend/src/utility/fade.module.css: -------------------------------------------------------------------------------- 1 | .outerWrapper { 2 | display: grid; 3 | grid-template-columns: 1fr; 4 | -webkit-tap-highlight-color: transparent; 5 | } 6 | 7 | .innerWrapper { 8 | grid-row-start: 1; 9 | grid-column-start: 1; 10 | } -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /frontend/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.0ec4db39.css", 4 | "main.js": "/static/js/main.d9d36117.js", 5 | "index.html": "/index.html", 6 | "main.0ec4db39.css.map": "/static/css/main.0ec4db39.css.map", 7 | "main.d9d36117.js.map": "/static/js/main.d9d36117.js.map" 8 | }, 9 | "entrypoints": [ 10 | "static/css/main.0ec4db39.css", 11 | "static/js/main.d9d36117.js" 12 | ] 13 | } -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } -------------------------------------------------------------------------------- /frontend/src/routes/Schedule/index.module.css: -------------------------------------------------------------------------------- 1 | .menuBody { 2 | margin: 0 auto; 3 | } 4 | 5 | .menuBody>h1 { 6 | text-align: center; 7 | } 8 | 9 | .menuBody>h1:nth-of-type(1) { 10 | margin-top: 1rem; 11 | margin-bottom: 1.4rem; 12 | } 13 | 14 | .menuBody>h1:nth-of-type(2) { 15 | margin: 1.4rem 0; 16 | } 17 | 18 | .table { 19 | margin: 0.5rem; 20 | } 21 | 22 | @media (min-width:750px) { 23 | .menuBody { 24 | max-width: 80rem; 25 | } 26 | } -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | googleId: { 5 | type: String, 6 | required: true, 7 | }, 8 | displayName: { 9 | type: String, 10 | required: true, 11 | }, 12 | email: { 13 | type: String, 14 | required: true, 15 | } 16 | }) 17 | 18 | // To be used by Passport to manage the google signins 19 | module.exports = mongoose.model('User', UserSchema) 20 | -------------------------------------------------------------------------------- /frontend/src/routes/QRCode/index.module.css: -------------------------------------------------------------------------------- 1 | .qrBody { 2 | height: calc(100vh - 17vh); 3 | display: flex; 4 | flex-direction: column; 5 | gap: 1rem; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .loading { 11 | position: absolute; 12 | display: flex; 13 | background: white; 14 | width: 256px; 15 | height: 256px; 16 | justify-content: center; 17 | align-items: center; 18 | font-size: 7rem; 19 | color: #1f94ff; 20 | transition: .5s; 21 | } -------------------------------------------------------------------------------- /frontend/src/routes/TotalMeals/index.module.css: -------------------------------------------------------------------------------- 1 | .menuBody { 2 | margin: 0 auto; 3 | margin-top: 1.5rem; 4 | } 5 | 6 | .menuBody>h1 { 7 | text-align: center; 8 | margin-top: 1rem; 9 | margin-bottom: 1.4rem; 10 | } 11 | 12 | .table { 13 | margin: 0.5rem; 14 | } 15 | 16 | .buttons { 17 | display: table; 18 | margin: 0 auto; 19 | } 20 | 21 | .textweek { 22 | display: inline; 23 | } 24 | 25 | @media (min-width:750px) { 26 | .menuBody { 27 | max-width: 80rem; 28 | } 29 | } -------------------------------------------------------------------------------- /frontend/src/routes/PurchaseHistory/index.module.css: -------------------------------------------------------------------------------- 1 | .menuBody { 2 | margin: 0 auto; 3 | margin-top: 1.5rem; 4 | } 5 | 6 | .menuBody>h1 { 7 | text-align: center; 8 | margin-top: 1rem; 9 | margin-bottom: 1.4rem; 10 | } 11 | 12 | .table { 13 | margin: 0.5rem; 14 | } 15 | 16 | .buttons { 17 | display: table; 18 | margin: 0 auto; 19 | } 20 | 21 | .textweek { 22 | display: inline; 23 | } 24 | 25 | @media (min-width:750px) { 26 | .menuBody { 27 | max-width: 80rem; 28 | } 29 | } -------------------------------------------------------------------------------- /models/Menu.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const MenuSchema = mongoose.model("menuitem", new mongoose.Schema({ 4 | day: String, 5 | breakfast: String, 6 | lunch: String, 7 | dinner: String 8 | })); 9 | 10 | // Get the weekly menu 11 | module.exports.getMenu = async function () { 12 | const menuItems = await MenuSchema.find({}) 13 | .select({ _id: 0 }); 14 | return menuItems; 15 | } 16 | 17 | // Set the weekly menu 18 | module.exports.setMenus = async function (menus) { 19 | await MenuSchema.deleteMany({}); 20 | await MenuSchema.insertMany(menus); 21 | } -------------------------------------------------------------------------------- /frontend/src/routes/Buy/index.module.css: -------------------------------------------------------------------------------- 1 | .table { 2 | margin: 0.5rem; 3 | } 4 | 5 | .buyBody { 6 | max-width: 80rem; 7 | margin: 0 auto; 8 | text-align: center; 9 | } 10 | 11 | h1:first-of-type { 12 | margin-top: 1.2rem; 13 | } 14 | 15 | h1 { 16 | text-transform: capitalize; 17 | margin-top: 1.7rem; 18 | margin-bottom: 0.8rem; 19 | } 20 | 21 | .buy { 22 | margin-top: -0.4rem; 23 | margin-bottom: 1.5rem; 24 | } 25 | 26 | .bought { 27 | width: 100%; 28 | height: calc(100vh - 17vh); 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | } -------------------------------------------------------------------------------- /models/Order.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const OrderSchema = mongoose.model("order", new mongoose.Schema({ 4 | orderid: String, 5 | selected: Object 6 | })); 7 | 8 | // Save an order in progress throught RazorPay 9 | module.exports.saveOrder = async function (orderid, selected) { 10 | await OrderSchema.create({ orderid: orderid, selected: selected }); 11 | } 12 | 13 | // Get a saved order to update the user after successful payment 14 | module.exports.getOrder = async function (orderid) { 15 | const orderObj = await OrderSchema.findOne({ orderid: orderid }); 16 | return orderObj; 17 | } -------------------------------------------------------------------------------- /models/Time.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const TimeSchema = mongoose.model("time", new mongoose.Schema({ 4 | meal: String, 5 | time: String, 6 | cost: Number 7 | })); 8 | 9 | // Get the cost and time of breakfast, lunch, dinner 10 | module.exports.getTimes = async function () { 11 | const Times = await TimeSchema.find({}) 12 | .select({ _id: 0 }); 13 | return Times; 14 | } 15 | 16 | // Set the cost and time of breakfast, lunch, dinner 17 | module.exports.setTimes = async function (times) { 18 | await TimeSchema.deleteMany({}); 19 | await TimeSchema.insertMany(times); 20 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mess-portal", 3 | "main": "index.js", 4 | "scripts": { 5 | "postinstall": "cd frontend && npm install --force", 6 | "start": "cd frontend && npm run build && cd .. && node index.js" 7 | }, 8 | "license": "ISC", 9 | "dependencies": { 10 | "compression": "^1.7.4", 11 | "connect-mongo": "^3.2.0", 12 | "cors": "^2.8.5", 13 | "dotenv": "^8.2.0", 14 | "express": "^4.17.1", 15 | "express-session": "^1.17.1", 16 | "mongoose": "^5.11.14", 17 | "passport": "^0.4.1", 18 | "passport-google-oauth20": "^2.0.0", 19 | "razorpay": "^2.8.3" 20 | } 21 | } -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import 'antd/dist/antd.min.css'; 5 | import App from './App'; 6 | import { BrowserRouter } from 'react-router-dom'; 7 | 8 | // Global variable. Switched to localhost for testing locally, and to / when 9 | // building and serving from node server 10 | 11 | window.APIROOT = '/'; 12 | // window.APIROOT = 'http://localhost:4000/'; 13 | 14 | const root = ReactDOM.createRoot(document.getElementById('root')); 15 | root.render( 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /frontend/src/utility/removeLoader.js: -------------------------------------------------------------------------------- 1 | // Removes the loader after the site has been fully loaded 2 | export default function RemoveLoader() { 3 | const onPageLoad = () => { 4 | setTimeout(() => { 5 | document.getElementById("loader_block").style.opacity = 0; 6 | setTimeout(() => { 7 | // Doing this without a timeout wouldn't play the fading opacity transition 8 | document.getElementById("loader_block").style.display = "none"; 9 | }, 310); 10 | }, 200); 11 | }; 12 | 13 | if (document.readyState === 'complete') onPageLoad(); 14 | else { 15 | window.addEventListener('load', onPageLoad, false); 16 | return () => window.removeEventListener('load', onPageLoad); 17 | } 18 | } -------------------------------------------------------------------------------- /frontend/src/routes/ScanQR/index.module.css: -------------------------------------------------------------------------------- 1 | .qrreader { 2 | display: none; 3 | } 4 | 5 | .card { 6 | margin: 0.5rem; 7 | max-width: 80rem; 8 | width: 100%; 9 | margin-top: 0.2rem; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .card h1 { 16 | text-align: center; 17 | margin-top: 1rem; 18 | } 19 | 20 | .body { 21 | display: flex; 22 | justify-content: center; 23 | } 24 | 25 | .body:nth-last-of-type(n+2) { 26 | margin-bottom: 1rem; 27 | } 28 | 29 | .icon { 30 | font-size: 10rem; 31 | margin: 0 auto; 32 | width: 100%; 33 | margin-bottom: 0.7rem; 34 | } 35 | 36 | .radio { 37 | display: table; 38 | margin: 0 auto; 39 | } 40 | 41 | .buttons { 42 | display: table; 43 | margin: 0 auto; 44 | } -------------------------------------------------------------------------------- /routes/data.js: -------------------------------------------------------------------------------- 1 | // Import required modules 2 | const express = require("express"); 3 | const router = express.Router(); 4 | 5 | // Import database models 6 | const Menu = require('../models/Menu'); 7 | const Time = require('../models/Time'); 8 | 9 | // Get the weekly menu 10 | router.get( 11 | "/menu", 12 | async (req, res) => { 13 | res.send(await Menu.getMenu()); 14 | } 15 | ); 16 | 17 | // Get the time and cost of breakfast, lunch, dinner 18 | router.get( 19 | "/time", 20 | async (req, res) => { 21 | res.send(await Time.getTimes()); 22 | } 23 | ); 24 | 25 | // Get the logged in and admin status 26 | router.get( 27 | "/status", 28 | async (req, res) => { 29 | res.send({ loggedIn: req.isAuthenticated(), admin: (req.isAuthenticated() && req.user?.email === process.env.ADMIN) }); 30 | } 31 | ); 32 | 33 | module.exports = router; -------------------------------------------------------------------------------- /routes/auth.js: -------------------------------------------------------------------------------- 1 | // Import required modules 2 | const express = require("express"); 3 | const passport = require("passport"); 4 | const router = express.Router(); 5 | 6 | // Signin button link 7 | router.get( 8 | "/signin", 9 | passport.authenticate( 10 | "google", 11 | { 12 | prompt: "select_account", 13 | scope: ["profile", "email"] 14 | } 15 | ) 16 | ); 17 | 18 | // Signout button link 19 | router.get( 20 | "/signout", 21 | (req, res) => { 22 | req.logout(); 23 | res.redirect(process.env.FRONTEND); 24 | } 25 | ); 26 | 27 | // For google redirection handling 28 | router.get( 29 | "/google/callback", 30 | passport.authenticate("google", { failureRedirect: process.env.FRONTEND }), 31 | (req, res) => { 32 | res.redirect(process.env.FRONTEND); 33 | } 34 | ); 35 | 36 | module.exports = router; -------------------------------------------------------------------------------- /SETUP.md: -------------------------------------------------------------------------------- 1 | # Setup Guide 2 | 3 | Create a `config.env` file with the below content and place it inside the `config` directory containing `passport.js` 4 | 5 | ``` 6 | # Port where the server should run 7 | PORT = 4000 8 | 9 | # For redirection after authentication 10 | FRONTEND = http://localhost:4000 11 | 12 | # MongoDB url 13 | MONGO_URI = mongodb+srv://{user}:{pass}@xxxx.xxxx.mongodb.net/mess 14 | 15 | # From google API console 16 | GOOGLE_CLIENT_ID = xxxx-xxxx.apps.googleusercontent.com 17 | GOOGLE_CLIENT_SECRET = xxxx-xxxx_xxxx_xxxx 18 | 19 | # Call back url set in API console 20 | CALLBACK_URL = /api/auth/google/callback 21 | 22 | # Razorpay Details 23 | PAY_ID = rzp_test_xxxx 24 | PAY_SECRET = xxxx 25 | 26 | # Admin GMail ID 27 | ADMIN = user@gmail.com 28 | ``` 29 | 30 | Open the project folder in command prompt and run: 31 | 32 | 1. `npm install` once 33 | 2. `npm run start` to run the portal 34 | -------------------------------------------------------------------------------- /frontend/src/utility/fade.js: -------------------------------------------------------------------------------- 1 | import { motion, AnimatePresence } from "framer-motion"; 2 | import classes from "./fade.module.css"; 3 | 4 | // Fades from prop one to prop two, when prop one2Two is true 5 | export default function Fade(props) { 6 | return ( 7 |
8 | 9 | 17 | {props.one2Two ? 18 | props.one : 19 | props.two 20 | } 21 | 22 | 23 |
24 | ); 25 | } -------------------------------------------------------------------------------- /frontend/src/components/MenuBar/index.module.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | position: fixed; 3 | min-height: 100vh; 4 | display: block; 5 | background: white; 6 | overflow-x: hidden; 7 | width: 15rem; 8 | padding-top: 0.8rem; 9 | } 10 | 11 | .navBtn { 12 | margin-left: 15rem; 13 | background: black; 14 | color: white; 15 | padding: 1.3rem 1.7rem; 16 | font-size: 1.3rem; 17 | width: fit-content; 18 | cursor: pointer; 19 | } 20 | 21 | .navWrap { 22 | position: fixed; 23 | } 24 | 25 | .logo { 26 | width: 100%; 27 | display: fixed; 28 | background: white; 29 | display: flex; 30 | justify-content: center; 31 | align-items: center; 32 | height: 4.63rem; 33 | color: black; 34 | font-size: 1.3rem; 35 | font-weight: bold; 36 | position: fixed; 37 | box-shadow: 0 2px 1rem rgba(0, 0, 0, 0.05); 38 | } 39 | 40 | .gap { 41 | display: block; 42 | margin-top: 5rem; 43 | } 44 | 45 | .menu { 46 | z-index: 999; 47 | } -------------------------------------------------------------------------------- /frontend/src/utility/routeAnimation.js: -------------------------------------------------------------------------------- 1 | // This allows a smooth fading transition between different pages of the site 2 | 3 | import { motion } from "framer-motion"; 4 | import { useLocation, Outlet } from 'react-router-dom'; 5 | 6 | const PageLayout = ({ children }) => children; 7 | 8 | const pageVariants = { 9 | initial: { 10 | opacity: 0 11 | }, 12 | in: { 13 | opacity: 1 14 | }, 15 | out: { 16 | opacity: 0 17 | } 18 | }; 19 | 20 | const pageTransition = { 21 | type: "tween", 22 | ease: "linear", 23 | duration: 0.5 24 | }; 25 | 26 | export default function AnimationLayout() { 27 | const { pathname } = useLocation(); 28 | return ( 29 | 30 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /frontend/src/routes/AdminPanel/index.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | margin: 0.5rem; 3 | max-width: 80rem; 4 | width: 100%; 5 | margin-top: 0.2rem; 6 | } 7 | 8 | .card h1 { 9 | text-align: center; 10 | margin-top: 0.2rem; 11 | margin-bottom: 0.8rem; 12 | } 13 | 14 | .adminBody { 15 | display: flex; 16 | justify-content: center; 17 | } 18 | 19 | .adminBody:nth-last-of-type(n+2) { 20 | margin-bottom: 1rem; 21 | } 22 | 23 | .table { 24 | margin: 0.8rem 0; 25 | } 26 | 27 | .table:first-of-type { 28 | margin-top: 0; 29 | } 30 | 31 | .table:last-of-type { 32 | margin-bottom: 0; 33 | } 34 | 35 | .buttons { 36 | display: table; 37 | margin: 0 auto; 38 | } 39 | 40 | .userList { 41 | height: 9.8rem; 42 | overflow-y: scroll; 43 | } 44 | 45 | .userList::-webkit-scrollbar { 46 | display: none; 47 | } 48 | 49 | .userListIndex { 50 | font-weight: 500; 51 | width: 2rem; 52 | display: inline-block; 53 | text-align: center; 54 | padding-right: 0.9rem; 55 | margin-right: 0.8rem; 56 | border-right: 1px solid #f0f0f0; 57 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mess-management-system", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ant-design/icons": "^4.7.0", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "antd": "^4.24.1", 11 | "axios": "^1.1.3", 12 | "framer-motion": "^7.6.5", 13 | "qrcode.react": "^3.1.0", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-qr-reader": "^3.0.0-beta-1", 17 | "react-razorpay": "^1.2.0", 18 | "react-responsive": "^9.0.0", 19 | "react-router-dom": "^6.4.3", 20 | "react-scripts": "5.0.1", 21 | "web-vitals": "^2.1.4" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | const GoogleStrategy = require('passport-google-oauth20').Strategy 2 | const User = require('../models/User') 3 | 4 | module.exports = function (passport) { 5 | passport.use( 6 | new GoogleStrategy( 7 | { 8 | clientID: process.env.GOOGLE_CLIENT_ID, 9 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 10 | callbackURL: process.env.CALLBACK_URL, 11 | }, 12 | async (accessToken, refreshToken, profile, done) => { 13 | 14 | //get the user data from google 15 | const newUser = { 16 | googleId: profile.id, 17 | displayName: profile.displayName, 18 | email: profile.emails[0].value 19 | } 20 | 21 | try { 22 | //find the user in our database or create user if not found 23 | let user = await User.findOne({ googleId: profile.id }); 24 | if (!user) user = await User.create(newUser); 25 | done(null, user); 26 | } catch (err) { 27 | console.error(err); 28 | } 29 | } 30 | ) 31 | ) 32 | 33 | // used to serialize the user for the session 34 | passport.serializeUser((user, done) => { 35 | done(null, user.id) 36 | }) 37 | 38 | // used to deserialize the user 39 | passport.deserializeUser((id, done) => { 40 | User.findById(id, (err, user) => done(err, user)) 41 | }) 42 | } -------------------------------------------------------------------------------- /frontend/src/routes/TotalMeals/index.js: -------------------------------------------------------------------------------- 1 | import classes from './index.module.css'; 2 | import { Button, Space, message } from 'antd'; 3 | import { useState, useEffect } from 'react'; 4 | import axios from 'axios'; 5 | import WeekMenu from '../../components/WeekMenu'; 6 | 7 | export default function TotalMealsPage() { 8 | const [thisweek, setthisweek] = useState(true); 9 | const [menu, setMenu] = useState([]); 10 | const [loading, setLoading] = useState(true); 11 | 12 | useEffect(() => { 13 | const fetchData = async () => { 14 | setLoading(true); 15 | try { 16 | const response = await axios.post(window.APIROOT + 'api/admin/meals', { week: thisweek ? "this" : "next" }); 17 | setMenu(response.data); 18 | console.log(response.data); 19 | setLoading(false); 20 | } catch (error) { 21 | message.error('Failed to fetch menu from server'); 22 | } 23 | } 24 | fetchData(); 25 | }, [thisweek]); 26 | 27 | return ( 28 |
29 |
30 | 31 | 32 | 33 | 34 |
35 |

{thisweek ? "Total Meals This Week" : "Total Meals Next Week"}

36 | 37 |
38 | ); 39 | } -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | // Modules 2 | import { useEffect } from 'react'; 3 | import { Route, Routes } from 'react-router-dom'; 4 | import { Layout } from 'antd'; 5 | 6 | // Utilities 7 | import AnimationLayout from './utility/routeAnimation'; 8 | import RemoveLoader from './utility/removeLoader'; 9 | 10 | // Components 11 | import MenuBar from './components/MenuBar'; 12 | 13 | // Route Pages 14 | import QRCodePage from './routes/QRCode'; 15 | import SchedulePage from './routes/Schedule'; 16 | import PurchaseHistoryPage from './routes/PurchaseHistory'; 17 | import BuyPage from './routes/Buy'; 18 | import AdminPanel from './routes/AdminPanel'; 19 | import TotalMealsPage from './routes/TotalMeals'; 20 | import ScanQRPage from './routes/ScanQR'; 21 | 22 | export default function App() { 23 | // Removes the loader after the site has been fully loaded 24 | useEffect(RemoveLoader, []); 25 | 26 | return ( 27 |
28 | 29 | 30 | 31 | }> 32 | } /> 33 | } /> 34 | } /> 35 | } /> 36 | } /> 37 | } /> 38 | } /> 39 | 40 | 41 | 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Import required modules 2 | const cors = require('cors'); 3 | const express = require('express'); 4 | const mongoose = require('mongoose'); 5 | const compression = require('compression'); 6 | const dotenv = require('dotenv'); 7 | const passport = require('passport'); 8 | const session = require('express-session'); 9 | const MongoStore = require('connect-mongo')(session); 10 | 11 | // Initialize express app 12 | var app = express(); 13 | 14 | // Load env and set PORT 15 | dotenv.config({ path: './config/config.env' }); 16 | const PORT = process.env.PORT; 17 | 18 | // MongoDB connection 19 | mongoose.connect(process.env.MONGO_URI, { 20 | useNewUrlParser: true, 21 | useUnifiedTopology: true 22 | }); 23 | 24 | // Passport config 25 | require('./config/passport')(passport); 26 | 27 | // Middleware 28 | app.use(cors({ credentials: true })); 29 | app.use(express.json()); 30 | app.use(compression()); 31 | app.use(express.urlencoded({ extended: true })); 32 | app.use( 33 | session({ 34 | secret: 'Mess Portal', 35 | resave: true, 36 | saveUninitialized: true, 37 | store: new MongoStore({ mongooseConnection: mongoose.connection }), 38 | }) 39 | ); 40 | app.use(passport.initialize()); 41 | app.use(passport.session()); 42 | 43 | // Check authentication for admin endpoints 44 | app.use('/api/admin/:all', (req, res, next) => { 45 | if (req.isAuthenticated() && req.user?.email === process.env.ADMIN) next(); 46 | else res.sendStatus(401); 47 | }); 48 | 49 | // Check authentication for user endpoints 50 | app.use('/api/user/:all', (req, res, next) => { 51 | if (req.isAuthenticated()) next(); 52 | else res.sendStatus(401); 53 | }) 54 | 55 | // Routes 56 | app.use('/api/auth', require('./routes/auth')); 57 | app.use('/api/data', require('./routes/data')); 58 | app.use('/api/admin', require('./routes/admin')); 59 | app.use('/api/user', require('./routes/user')); 60 | 61 | // Frontend site 62 | app.use(express.static(__dirname + '/frontend/build')); 63 | app.get('*', (req, res) => res.sendFile(__dirname + '/frontend/build/index.html')); 64 | 65 | // Start server 66 | app.listen(PORT, console.log(`Server started on port ${PORT}`)); 67 | -------------------------------------------------------------------------------- /frontend/src/routes/Schedule/index.js: -------------------------------------------------------------------------------- 1 | import classes from './index.module.css'; 2 | import { Table, message } from 'antd'; 3 | import { useMediaQuery } from 'react-responsive'; 4 | import WeekMenu from '../../components/WeekMenu'; 5 | import axios from "axios"; 6 | import { useState, useEffect } from "react"; 7 | import { motion } from "framer-motion"; 8 | 9 | const timingCol = [ 10 | { 11 | title: 'Meal', 12 | dataIndex: 'meal', 13 | key: 'meal', 14 | render: (text) => ({ 15 | props: { 16 | style: { background: "#FAFAFA" }, 17 | }, 18 | children: {text} 19 | }) 20 | }, 21 | { 22 | title: 'Time', 23 | dataIndex: 'time', 24 | key: 'time' 25 | } 26 | ]; 27 | 28 | export default function SchedulePage() { 29 | const mobile = useMediaQuery({ query: '(max-width: 750px)' }); 30 | const [timingRow, setTimingRow] = useState([]); 31 | const [menu, setMenu] = useState([]); 32 | 33 | useEffect(() => { 34 | const fetchData = async () => { 35 | try { 36 | let response = await axios.get(window.APIROOT + 'api/data/time'); 37 | setTimingRow(response.data); 38 | } catch (error) { 39 | message.error('Failed to fetch timing from server'); 40 | } 41 | } 42 | fetchData(); 43 | }, []); 44 | 45 | useEffect(() => { 46 | const fetchData = async () => { 47 | try { 48 | const response = await axios.get(window.APIROOT + 'api/data/menu'); 49 | setMenu(response.data); 50 | } catch (error) { 51 | message.error('Failed to fetch menu from server'); 52 | } 53 | } 54 | fetchData(); 55 | }, []); 56 | 57 | return ( 58 |
59 |

TIMING

60 | 61 |

MENU

62 | 63 | 64 | 65 | 66 | ); 67 | } -------------------------------------------------------------------------------- /frontend/src/routes/PurchaseHistory/index.js: -------------------------------------------------------------------------------- 1 | import classes from './index.module.css'; 2 | import { Button, Space, message } from 'antd'; 3 | import { useMediaQuery } from 'react-responsive'; 4 | import WeekMenu from '../../components/WeekMenu'; 5 | import axios from "axios"; 6 | import { useState, useEffect } from "react"; 7 | 8 | export default function PurchaseHistoryPage() { 9 | const mobile = useMediaQuery({ query: '(max-width: 750px)' }); 10 | const [thisweek, setthisweek] = useState(true); 11 | const [menu, setMenu] = useState([]); 12 | const [loading, setLoading] = useState(false); 13 | 14 | useEffect(() => { 15 | const fetchData = async () => { 16 | setLoading(true); 17 | try { 18 | const response = await axios.get(window.APIROOT + 'api/data/menu'); 19 | const buyer = await axios.get(window.APIROOT + 'api/user/data'); 20 | let data = []; 21 | for (let r of response.data) { 22 | data.push({ 23 | day: r.day, 24 | breakfast: { text: r.breakfast, selected: buyer.data[thisweek ? "this" : "next"][r.day].breakfast }, 25 | lunch: { text: r.lunch, selected: buyer.data[thisweek ? "this" : "next"][r.day].lunch }, 26 | dinner: { text: r.dinner, selected: buyer.data[thisweek ? "this" : "next"][r.day].dinner }, 27 | }) 28 | } 29 | setMenu(data); 30 | setLoading(false); 31 | } catch (error) { 32 | message.error('Failed to fetch purchase history from server'); 33 | } 34 | } 35 | fetchData(); 36 | }, [thisweek]); 37 | 38 | return ( 39 |
40 |
41 | 42 | 43 | 44 | 45 |
46 |

{thisweek ? "Your Coupons This Week" : "Your Coupons Next Week"}

47 | 48 |
49 | ); 50 | } -------------------------------------------------------------------------------- /frontend/src/routes/QRCode/index.js: -------------------------------------------------------------------------------- 1 | import { QRCodeSVG } from 'qrcode.react'; 2 | import classes from './index.module.css'; 3 | import { ReloadOutlined, QuestionOutlined, LoadingOutlined } from '@ant-design/icons'; 4 | import { Button, notification, Space, message } from 'antd'; 5 | import axios from "axios"; 6 | import { useState, useEffect } from "react"; 7 | 8 | export default function QRCodePage() { 9 | const [loading, setLoading] = useState(false); 10 | const [rawCode, setRawCode] = useState(null); 11 | 12 | useEffect(() => { 13 | const fetchData = async () => { 14 | setLoading(true); 15 | try { 16 | const response = await axios.get(window.APIROOT + 'api/user/data'); 17 | const code = response.data.secret + response.data.email; 18 | setRawCode(code); 19 | setLoading(false); 20 | } catch (error) { 21 | message.error('Failed to fetch QR code'); 22 | } 23 | } 24 | fetchData(); 25 | }, []); 26 | 27 | return ( 28 |
29 |
30 |
31 | 32 |
33 | 34 | 48 |
53 | ); 54 | } -------------------------------------------------------------------------------- /routes/admin.js: -------------------------------------------------------------------------------- 1 | // Import required modules 2 | const express = require("express"); 3 | const router = express.Router(); 4 | 5 | // Import database models 6 | const Menu = require('../models/Menu'); 7 | const Time = require('../models/Time'); 8 | const Buyer = require('../models/Buyer'); 9 | 10 | // Set the time and cost of breakfast, lunch, dinner 11 | router.post( 12 | "/setTime", 13 | async (req, res) => { 14 | await Time.setTimes(req.body.times); 15 | res.send(); 16 | } 17 | ); 18 | 19 | // Set the weekly menu 20 | router.post( 21 | "/setMenu", 22 | async (req, res) => { 23 | await Menu.setMenus(req.body.menus); 24 | res.send(); 25 | } 26 | ); 27 | 28 | // Get the total meals that need to be cooked 29 | router.post( 30 | "/meals", 31 | async (req, res) => { 32 | const buyers = await Buyer.allBuyers(); 33 | let data = { 34 | monday: { breakfast: 0, lunch: 0, dinner: 0 }, 35 | tuesday: { breakfast: 0, lunch: 0, dinner: 0 }, 36 | wednesday: { breakfast: 0, lunch: 0, dinner: 0 }, 37 | thursday: { breakfast: 0, lunch: 0, dinner: 0 }, 38 | friday: { breakfast: 0, lunch: 0, dinner: 0 }, 39 | saturday: { breakfast: 0, lunch: 0, dinner: 0 }, 40 | sunday: { breakfast: 0, lunch: 0, dinner: 0 } 41 | } 42 | for (let buyer of buyers) { 43 | let meals = buyer[req.body.week]; 44 | for (const [day, val] of Object.entries(meals)) { 45 | data[day]["breakfast"] += val.breakfast; 46 | data[day]["lunch"] += val.lunch; 47 | data[day]["dinner"] += val.dinner; 48 | } 49 | } 50 | const processed = [ 51 | { day: "monday", breakfast: data.monday.breakfast, lunch: data.monday.lunch, dinner: data.monday.dinner }, 52 | { day: "tuesday", breakfast: data.tuesday.breakfast, lunch: data.tuesday.lunch, dinner: data.tuesday.dinner }, 53 | { day: "wednesday", breakfast: data.wednesday.breakfast, lunch: data.wednesday.lunch, dinner: data.wednesday.dinner }, 54 | { day: "thursday", breakfast: data.thursday.breakfast, lunch: data.thursday.lunch, dinner: data.thursday.dinner }, 55 | { day: "friday", breakfast: data.friday.breakfast, lunch: data.friday.lunch, dinner: data.friday.dinner }, 56 | { day: "saturday", breakfast: data.saturday.breakfast, lunch: data.saturday.lunch, dinner: data.saturday.dinner }, 57 | { day: "sunday", breakfast: data.sunday.breakfast, lunch: data.sunday.lunch, dinner: data.sunday.dinner } 58 | ] 59 | res.send(processed); 60 | } 61 | ); 62 | 63 | module.exports = router; -------------------------------------------------------------------------------- /routes/user.js: -------------------------------------------------------------------------------- 1 | // Import required modules 2 | const express = require("express"); 3 | const router = express.Router(); 4 | const Razorpay = require('razorpay'); 5 | 6 | // Import database models 7 | const Buyer = require('../models/Buyer'); 8 | const Time = require('../models/Time'); 9 | const Order = require('../models/Order'); 10 | 11 | // Import RazorPay payment validator 12 | var { validatePaymentVerification } = require('razorpay/dist/utils/razorpay-utils'); 13 | 14 | // Get the user secret and meals purchased 15 | router.get( 16 | "/data", 17 | async (req, res) => { 18 | res.send(await Buyer.getBuyer(req.user?.email)); 19 | } 20 | ); 21 | 22 | // Reset the user secret 23 | router.get( 24 | "/resetSecret", 25 | async (req, res) => { 26 | res.send(await Buyer.resetSecret(req.user?.email)); 27 | } 28 | ); 29 | 30 | // Check if the user's coupon is valid for the current day and meal 31 | router.post( 32 | "/checkCoupon", 33 | async (req, res) => { 34 | res.send(await Buyer.checkCoupon(req.body)); 35 | } 36 | ); 37 | 38 | // Check if the user has already bought coupons for the next week 39 | router.get( 40 | "/boughtNextWeek", 41 | async (req, res) => { 42 | res.send(await Buyer.boughtNextWeek(req.user?.email)); 43 | } 44 | ); 45 | 46 | // Check if the payment throught RazorPay is successful 47 | router.post( 48 | "/checkOrder", 49 | async (req, res) => { 50 | const isValid = validatePaymentVerification({ "order_id": req.body.razorpay_order_id, "payment_id": req.body.razorpay_payment_id }, req.body.razorpay_signature, process.env.PAY_SECRET); 51 | if (isValid) { 52 | const orderObj = await Order.getOrder(req.body.razorpay_order_id); 53 | await Buyer.saveOrder(req.user?.email, orderObj.selected); 54 | } 55 | res.send(isValid); 56 | } 57 | ); 58 | 59 | // Create a RazorPay order and send the order id to frontend 60 | router.post( 61 | "/createOrder", 62 | async (req, res) => { 63 | let costs = await Time.getTimes(); 64 | let data = {}; 65 | for (let c of costs) data[c.meal] = c.cost; 66 | let total = 0; 67 | for (const [day, val] of Object.entries(req.body.selected)) { 68 | if (val.breakfast === true) total += data.breakfast; 69 | if (val.lunch === true) total += data.lunch; 70 | if (val.dinner === true) total += data.dinner; 71 | } 72 | 73 | let instance = new Razorpay({ key_id: process.env.PAY_ID, key_secret: process.env.PAY_SECRET }); 74 | let resp = await instance.orders.create({ 75 | amount: total * 100, 76 | currency: "INR" 77 | }); 78 | await Order.saveOrder(resp.id, req.body.selected); 79 | res.send(resp); 80 | } 81 | ); 82 | 83 | module.exports = router; -------------------------------------------------------------------------------- /frontend/src/routes/ScanQR/index.js: -------------------------------------------------------------------------------- 1 | import classes from './index.module.css'; 2 | import { useState, useEffect } from 'react'; 3 | import { QrReader } from 'react-qr-reader'; 4 | import { Radio, Card, Button, message } from 'antd'; 5 | import Fade from '../../utility/fade'; 6 | import { CloseSquareOutlined, CheckSquareOutlined, ReloadOutlined, LoadingOutlined } from '@ant-design/icons'; 7 | import { motion } from 'framer-motion'; 8 | import axios from 'axios'; 9 | 10 | export default function ScanQRPage() { 11 | const [data, setData] = useState(); 12 | const [type, setType] = useState(null); 13 | const [valid, setValid] = useState(null); 14 | 15 | const checkCoupon = async (postData) => { 16 | try { 17 | const response = await axios.post(window.APIROOT + 'api/user/checkCoupon', postData); 18 | setValid(response.data); 19 | } catch (error) { 20 | message.error("Failed to reach server to verify coupon"); 21 | setData(null); 22 | } 23 | } 24 | 25 | useEffect(() => { 26 | if (!data) return; 27 | const secret = data?.substring(0, 4); 28 | const email = data?.substring(4, data.length); 29 | const day = (new Intl.DateTimeFormat('en-US', { weekday: 'long' }).format(new Date())).toLowerCase(); 30 | checkCoupon({ secret: secret, email: email, day: day, type: type }); 31 | }, [data, type]) 32 | 33 | return ( 34 | <> 35 | { 36 | if (!!result) setData(result?.text) 37 | }} /> 38 |
39 | 40 |
41 | setType(e.target.value)}> 42 | Breakfast 43 | Lunch 44 | Dinner 45 | 46 |
47 | { 48 | type ? 49 |

Scan QR Code for {type}

: 50 |

Select the meal

51 | } 52 | 53 | {type ? } two={} two={} />} /> : null} 54 | 55 |
56 | 60 |
61 |
62 |
63 | 64 | ); 65 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [MESS PORTAL](https://mess.dashroshan.com) 2 | 3 | [**A D3 Fest Hackathon submission**](https://d3fest.tech) 4 | 5 | ### Team members: 6 | 7 | - Roshan Dash (Developer) 8 | - Subhajit Chatterjee (UI/UX, Documentation) 9 | 10 | ### ▶️ [View it on YouTube](https://www.youtube.com/watch?v=QqCdYTh8L7o) 11 | 12 | ## 🧱 PROBLEM STATEMENT 13 | 14 | **D3H05** 15 | 16 | Develop a solution for mess management. The solution should provide facilities to the mess admin and the students. For mess admins, they can manage coupons and other necessary details like menu, and pricing in the mess dashboard. Students can buy coupons from the mess dashboard only, deciding their desired meals for the week. The aim of the project will be to remove the hassle of buying coupons and provide a centralized platform for mess management. 17 | 18 | - **Task 1 -** QR codes for each meal a day in place of paper coupons 19 | - **Task 2 -** Razorpay Integration 20 | 21 | ## ✨ SALIENT FEATURES OF OUR SOLUTION 22 | 23 | - For **MESS OWNERS** : 24 | 25 | Has specialised mess dashboard where the admin can : 26 | 27 | - _Manage the menu_ 28 | - _Edit timings_ 29 | - _Regulate prices_ 30 | - _Know total meals to be cooked_ 31 | - _Scan and verify QR codes to provide meal_ 32 | - _Razorpay integration to accept online payments_ 33 | 34 | - For **STUDENTS** : 35 | 36 | They gain access to : 37 | 38 | - _View the weekly menu, timining, and costs_ 39 | - _Decide and purchase their desired meals online_ 40 | - _Review the meals purchased (for both present and next week)_ 41 | - _Using a single QR code instead of paper coupons_ 42 | 43 | ## 🎯 DETAILED DESCRIPTION 44 | 45 | - ### STUDENT SERVICES 46 | 47 | - **Mess time and menu on the home page** 48 | 49 | ![](/assets/time_menu.jpg) 50 | 51 | - **Signing in to the account** 52 | 53 | The students can sign in using their respective Google accounts using the sign in option. Login can be restricted to certain domains like iiit-bh.ac.in 54 | 55 | ![](/assets/google_signin.jpg) 56 | 57 | - **Buying coupons for next week** 58 | 59 | The student can apply for desired meals by selecting among the checkboxes. The final amount is displayed at the bottom for payment. 60 | 61 | ![](/assets/purchase_page.jpg) 62 | 63 | Upon clicking "Continue with Payment", the student is directed to the Payment Gateway of Razorpay to complete their purchase. 64 | 65 | ![](/assets/payment.jpg) 66 | 67 | - **Purchase history** 68 | 69 | The student can check which meal and day coupons they have bought for the current and upcoming week. 70 | 71 | ![](/assets/purchase_history.jpg) 72 | 73 | - **Using QR code** 74 | 75 | The student will be provided a unique (static) QR code which can be used directly using smartphones or can also be printed and used just like an ID. 76 | In the case that the students feel that their QR code has been compromised, they can create a new QR code. 77 | 78 | ![](/assets/qr_code.jpg) 79 | 80 | - ### ADMINISTRATOR SERVICES : 81 | 82 | - **Admin panel** 83 | 84 | This provides interface to edit the cost, time, and items of the weekly menu. 85 | 86 | ![](/assets/admin_panel.jpg) 87 | 88 | - **Total meals** 89 | 90 | This page shows the total meals to be cooked for the present week as well as the upcoming week based on the total coupons purchased. 91 | 92 | ![](/assets/total_meals.jpg) 93 | 94 | - **Scan QR code** 95 | 96 | This allows to scan and verify the mess QR codes. After selecting the meal type, upon hovering the camera over a mess QR code it shows a tick mark if the person has purchased coupon for the given meal on the given day. Or a cross mark if the person has not purchased the meal, or have already claimed it. 97 | 98 | ![](/assets/scan_qr.jpg) 99 | 100 | The admin can press "Scan New" to check a new QR code. 101 | 102 | ## ❤️ Team Zenith 103 | -------------------------------------------------------------------------------- /frontend/src/components/MenuBar/index.js: -------------------------------------------------------------------------------- 1 | import classes from "./index.module.css"; 2 | import { useState, useEffect } from "react"; 3 | import { motion } from "framer-motion"; 4 | import { Link } from "react-router-dom"; 5 | import Fade from "../../utility/fade"; 6 | import axios from "axios"; 7 | import { 8 | MenuOutlined, 9 | CloseOutlined, 10 | UserOutlined, 11 | ShoppingCartOutlined, 12 | QrcodeOutlined, 13 | CalendarOutlined, 14 | TableOutlined, 15 | SettingOutlined, 16 | SolutionOutlined, 17 | ScanOutlined 18 | } from '@ant-design/icons'; 19 | import { Menu, message } from 'antd'; 20 | 21 | // Create and send a menu item with relative link 22 | const getItem = (label, link, icon, key) => { 23 | return ( 24 | 25 | {label} 26 | 27 | ); 28 | } 29 | 30 | // Create and send a menu item with absolute link needed for signin and signout 31 | const getLoginItem = (label, link, icon, key) => { 32 | return ( 33 | 34 | {label} 35 | 36 | ); 37 | } 38 | 39 | export default function MenuBar() { 40 | const [open, setopen] = useState(false); 41 | const [status, setStatus] = useState(false); 42 | 43 | // On load, get the loggedin and admin status and render the menu accordingly 44 | useEffect(() => { 45 | const fetchData = async () => { 46 | try { 47 | const response = await axios.get(window.APIROOT + 'api/data/status'); 48 | setStatus(response.data); 49 | } catch (error) { 50 | message.error('Failed to fetch data from server'); 51 | } 52 | } 53 | fetchData(); 54 | }, []); 55 | 56 | return ( 57 | <> 58 |
59 |
Mess Portal
60 | 61 | setopen(!open)}>{ 62 | <> 63 | {getLoginItem(status.loggedIn ? 'Sign out' : 'Sign in', status.loggedIn ? 'api/auth/signout' : '/api/auth/signin', , '2')} 64 | {status.loggedIn ? <> 65 | 66 | {getItem('Schedule', '/', , '3')} 67 | {getItem('Buy coupons', '/buy-coupons', , '4')} 68 | {getItem('Purchase history', '/purchase-history', , '5')} 69 | {getItem('QR code', '/qr-code', , '6')} 70 | {status.admin ? <> 71 | 72 | {getItem('Admin panel', '/admin', , '7')} 73 | {getItem('Total meals', '/total-meals', , '8')} 74 | {getItem('Scan QR code', '/scan-qr', , '9')} 75 | : null} 76 | : null} 77 | 78 | } 79 |
setopen(!open)}> 80 | { 81 | } two={} /> 82 | } 83 |
84 |
85 |
86 |
87 |
88 | 89 | ); 90 | } -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Mess Portal 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 184 | 185 | 186 | 187 | 188 |
189 |
190 |
191 |
192 | 193 | 194 | -------------------------------------------------------------------------------- /frontend/build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Mess Portal 8 | 9 | 10 | 11 | 12 | 13 | 14 | 182 | 183 | 184 | 185 | 186 | 187 |
188 |
189 |
190 |
191 | 192 | 193 | -------------------------------------------------------------------------------- /frontend/src/components/WeekMenu/index.js: -------------------------------------------------------------------------------- 1 | import classes from './index.module.css'; 2 | import { Table } from 'antd'; 3 | 4 | // menu: the menu object 5 | // mobile: display menu as 7 individual tables for better mobile usability when true 6 | // highlight: when true, it looks for cells to be highlighted in the menu object for purchase history page 7 | // loading: plays a loading spinner when true 8 | export default function WeekMenu(props) { 9 | const menu = props.menu; 10 | const mobile = props.mobile; 11 | const highlight = props.highlight; 12 | const loading = props.loading; 13 | 14 | if (mobile) { 15 | return menu.map(e => 16 |
({ 26 | props: { 27 | style: { background: "#FAFAFA" }, 28 | }, 29 | children: {text}, 30 | }) 31 | }, 32 | { 33 | title: '', 34 | dataIndex: 'Meal', 35 | key: 'Meal', 36 | colSpan: 0, 37 | width: 300, 38 | render: (text, record) => ({ 39 | props: highlight ? { 40 | style: { background: record.selected ? "#ceface" : "unset", color: record.selected ? "unset" : "lightgrey" }, 41 | } : {}, 42 | children: {text}, 43 | }) 44 | }, 45 | ] 46 | } 47 | dataSource={ 48 | [ 49 | { 50 | key: '1', 51 | Type: 'Breakfast', 52 | Meal: highlight ? e.breakfast.text : e.breakfast, 53 | selected: e.breakfast.selected 54 | }, 55 | { 56 | key: '2', 57 | Type: 'Lunch', 58 | Meal: highlight ? e.lunch.text : e.lunch, 59 | selected: e.lunch.selected 60 | }, 61 | { 62 | key: '3', 63 | Type: 'Dinner', 64 | Meal: highlight ? e.dinner.text : e.dinner, 65 | selected: e.dinner.selected 66 | }, 67 | ] 68 | } 69 | pagination={false} bordered /> 70 | ); 71 | } 72 | else { 73 | return (
({ 81 | props: { 82 | style: { background: "#FAFAFA" }, 83 | }, 84 | children: {text}, 85 | }) 86 | }, 87 | { 88 | title: 'Breakfast', 89 | dataIndex: 'breakfast', 90 | key: 'breakfast', 91 | render: (text, record) => ({ 92 | props: highlight ? { 93 | style: { background: record.selected.breakfast ? "#ceface" : "unset", color: record.selected.breakfast ? "unset" : "lightgrey" }, 94 | } : {}, 95 | children: {text}, 96 | }) 97 | }, 98 | { 99 | title: 'Lunch', 100 | dataIndex: 'lunch', 101 | key: 'lunch', 102 | render: (text, record) => ({ 103 | props: highlight ? { 104 | style: { background: record.selected.lunch ? "#ceface" : "unset", color: record.selected.lunch ? "unset" : "lightgrey" }, 105 | } : {}, 106 | children: {text}, 107 | }) 108 | }, 109 | { 110 | title: 'Dinner', 111 | dataIndex: 'dinner', 112 | key: 'dinner', 113 | render: (text, record) => ({ 114 | props: highlight ? { 115 | style: { background: record.selected.dinner ? "#ceface" : "unset", color: record.selected.dinner ? "unset" : "lightgrey" }, 116 | } : {}, 117 | children: {text}, 118 | }) 119 | }, 120 | ] 121 | } 122 | dataSource={ 123 | menu.map((e, i) => { 124 | return { 125 | key: i, 126 | day: e.day, 127 | breakfast: highlight ? e.breakfast.text : e.breakfast, 128 | lunch: highlight ? e.lunch.text : e.lunch, 129 | dinner: highlight ? e.dinner.text : e.dinner, 130 | selected: { 131 | breakfast: e.breakfast.selected, 132 | lunch: e.lunch.selected, 133 | dinner: e.dinner.selected 134 | }, 135 | }; 136 | }) 137 | } 138 | pagination={false} bordered />); 139 | } 140 | } -------------------------------------------------------------------------------- /frontend/build/static/js/main.d9d36117.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2018 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /*! ./Component */ 14 | 15 | /*! ./Context */ 16 | 17 | /*! ./checkPropTypes */ 18 | 19 | /*! ./cjs/react-is.development.js */ 20 | 21 | /*! ./factoryWithTypeCheckers */ 22 | 23 | /*! ./lib/ReactPropTypesSecret */ 24 | 25 | /*! ./lib/has */ 26 | 27 | /*! ./mediaQuery */ 28 | 29 | /*! ./toQuery */ 30 | 31 | /*! ./useMediaQuery */ 32 | 33 | /*! css-mediaquery */ 34 | 35 | /*! hyphenate-style-name */ 36 | 37 | /*! matchmediaquery */ 38 | 39 | /*! object-assign */ 40 | 41 | /*! prop-types */ 42 | 43 | /*! react */ 44 | 45 | /*! react-is */ 46 | 47 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 48 | 49 | /*! shallow-equal */ 50 | 51 | /*!**********************!*\ 52 | !*** ./src/index.ts ***! 53 | \**********************/ 54 | 55 | /*!************************!*\ 56 | !*** ./src/Context.ts ***! 57 | \************************/ 58 | 59 | /*!************************!*\ 60 | !*** ./src/toQuery.ts ***! 61 | \************************/ 62 | 63 | /*!**************************!*\ 64 | !*** ./src/Component.ts ***! 65 | \**************************/ 66 | 67 | /*!***************************!*\ 68 | !*** ./src/mediaQuery.ts ***! 69 | \***************************/ 70 | 71 | /*!******************************!*\ 72 | !*** ./src/useMediaQuery.ts ***! 73 | \******************************/ 74 | 75 | /*!****************************************!*\ 76 | !*** ./node_modules/react-is/index.js ***! 77 | \****************************************/ 78 | 79 | /*!******************************************!*\ 80 | !*** ./node_modules/prop-types/index.js ***! 81 | \******************************************/ 82 | 83 | /*!********************************************!*\ 84 | !*** ./node_modules/prop-types/lib/has.js ***! 85 | \********************************************/ 86 | 87 | /*!*********************************************!*\ 88 | !*** ./node_modules/object-assign/index.js ***! 89 | \*********************************************/ 90 | 91 | /*!**********************************************!*\ 92 | !*** ./node_modules/css-mediaquery/index.js ***! 93 | \**********************************************/ 94 | 95 | /*!***********************************************!*\ 96 | !*** ./node_modules/matchmediaquery/index.js ***! 97 | \***********************************************/ 98 | 99 | /*!***************************************************!*\ 100 | !*** ./node_modules/prop-types/checkPropTypes.js ***! 101 | \***************************************************/ 102 | 103 | /*!****************************************************!*\ 104 | !*** ./node_modules/hyphenate-style-name/index.js ***! 105 | \****************************************************/ 106 | 107 | /*!******************************************************!*\ 108 | !*** ./node_modules/shallow-equal/dist/index.esm.js ***! 109 | \******************************************************/ 110 | 111 | /*!***********************************************************!*\ 112 | !*** ./node_modules/react-is/cjs/react-is.development.js ***! 113 | \***********************************************************/ 114 | 115 | /*!************************************************************!*\ 116 | !*** ./node_modules/prop-types/factoryWithTypeCheckers.js ***! 117 | \************************************************************/ 118 | 119 | /*!*************************************************************!*\ 120 | !*** ./node_modules/prop-types/lib/ReactPropTypesSecret.js ***! 121 | \*************************************************************/ 122 | 123 | /*!**************************************************************************************!*\ 124 | !*** external {"commonjs":"react","commonjs2":"react","amd":"react","root":"React"} ***! 125 | \**************************************************************************************/ 126 | 127 | /** 128 | * @license React 129 | * react-dom.production.min.js 130 | * 131 | * Copyright (c) Facebook, Inc. and its affiliates. 132 | * 133 | * This source code is licensed under the MIT license found in the 134 | * LICENSE file in the root directory of this source tree. 135 | */ 136 | 137 | /** 138 | * @license React 139 | * react-jsx-runtime.production.min.js 140 | * 141 | * Copyright (c) Facebook, Inc. and its affiliates. 142 | * 143 | * This source code is licensed under the MIT license found in the 144 | * LICENSE file in the root directory of this source tree. 145 | */ 146 | 147 | /** 148 | * @license React 149 | * react.production.min.js 150 | * 151 | * Copyright (c) Facebook, Inc. and its affiliates. 152 | * 153 | * This source code is licensed under the MIT license found in the 154 | * LICENSE file in the root directory of this source tree. 155 | */ 156 | 157 | /** 158 | * @license React 159 | * scheduler.production.min.js 160 | * 161 | * Copyright (c) Facebook, Inc. and its affiliates. 162 | * 163 | * This source code is licensed under the MIT license found in the 164 | * LICENSE file in the root directory of this source tree. 165 | */ 166 | 167 | /** 168 | * @license qrcode.react 169 | * Copyright (c) Paul O'Shannessy 170 | * SPDX-License-Identifier: ISC 171 | */ 172 | 173 | /** 174 | * @remix-run/router v1.0.3 175 | * 176 | * Copyright (c) Remix Software Inc. 177 | * 178 | * This source code is licensed under the MIT license found in the 179 | * LICENSE.md file in the root directory of this source tree. 180 | * 181 | * @license MIT 182 | */ 183 | 184 | /** 185 | * React Router DOM v6.4.3 186 | * 187 | * Copyright (c) Remix Software Inc. 188 | * 189 | * This source code is licensed under the MIT license found in the 190 | * LICENSE.md file in the root directory of this source tree. 191 | * 192 | * @license MIT 193 | */ 194 | 195 | /** 196 | * React Router v6.4.3 197 | * 198 | * Copyright (c) Remix Software Inc. 199 | * 200 | * This source code is licensed under the MIT license found in the 201 | * LICENSE.md file in the root directory of this source tree. 202 | * 203 | * @license MIT 204 | */ 205 | 206 | /** @license React v16.13.1 207 | * react-is.development.js 208 | * 209 | * Copyright (c) Facebook, Inc. and its affiliates. 210 | * 211 | * This source code is licensed under the MIT license found in the 212 | * LICENSE file in the root directory of this source tree. 213 | */ 214 | 215 | /** @license React v16.13.1 216 | * react-is.production.min.js 217 | * 218 | * Copyright (c) Facebook, Inc. and its affiliates. 219 | * 220 | * This source code is licensed under the MIT license found in the 221 | * LICENSE file in the root directory of this source tree. 222 | */ 223 | -------------------------------------------------------------------------------- /models/Buyer.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const BuyerSchema = mongoose.model("buyer", new mongoose.Schema({ 4 | email: String, 5 | secret: String, 6 | bought: Boolean, 7 | this: { 8 | monday: { 9 | breakfast: { type: Boolean, default: false }, 10 | lunch: { type: Boolean, default: false }, 11 | dinner: { type: Boolean, default: false } 12 | }, 13 | tuesday: { 14 | breakfast: { type: Boolean, default: false }, 15 | lunch: { type: Boolean, default: false }, 16 | dinner: { type: Boolean, default: false } 17 | }, 18 | wednesday: { 19 | breakfast: { type: Boolean, default: false }, 20 | lunch: { type: Boolean, default: false }, 21 | dinner: { type: Boolean, default: false } 22 | }, 23 | thursday: { 24 | breakfast: { type: Boolean, default: false }, 25 | lunch: { type: Boolean, default: false }, 26 | dinner: { type: Boolean, default: false } 27 | }, 28 | friday: { 29 | breakfast: { type: Boolean, default: false }, 30 | lunch: { type: Boolean, default: false }, 31 | dinner: { type: Boolean, default: false } 32 | }, 33 | saturday: { 34 | breakfast: { type: Boolean, default: false }, 35 | lunch: { type: Boolean, default: false }, 36 | dinner: { type: Boolean, default: false } 37 | }, 38 | sunday: { 39 | breakfast: { type: Boolean, default: false }, 40 | lunch: { type: Boolean, default: false }, 41 | dinner: { type: Boolean, default: false } 42 | } 43 | }, 44 | next: { 45 | monday: { 46 | breakfast: { type: Boolean, default: false }, 47 | lunch: { type: Boolean, default: false }, 48 | dinner: { type: Boolean, default: false } 49 | }, 50 | tuesday: { 51 | breakfast: { type: Boolean, default: false }, 52 | lunch: { type: Boolean, default: false }, 53 | dinner: { type: Boolean, default: false } 54 | }, 55 | wednesday: { 56 | breakfast: { type: Boolean, default: false }, 57 | lunch: { type: Boolean, default: false }, 58 | dinner: { type: Boolean, default: false } 59 | }, 60 | thursday: { 61 | breakfast: { type: Boolean, default: false }, 62 | lunch: { type: Boolean, default: false }, 63 | dinner: { type: Boolean, default: false } 64 | }, 65 | friday: { 66 | breakfast: { type: Boolean, default: false }, 67 | lunch: { type: Boolean, default: false }, 68 | dinner: { type: Boolean, default: false } 69 | }, 70 | saturday: { 71 | breakfast: { type: Boolean, default: false }, 72 | lunch: { type: Boolean, default: false }, 73 | dinner: { type: Boolean, default: false } 74 | }, 75 | sunday: { 76 | breakfast: { type: Boolean, default: false }, 77 | lunch: { type: Boolean, default: false }, 78 | dinner: { type: Boolean, default: false } 79 | } 80 | } 81 | })); 82 | 83 | // Get the user details, or if it doesn't exists, create a new user object 84 | module.exports.getBuyer = async function (email) { 85 | let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789"; 86 | let randomStr = ""; 87 | for (let i = 0; i < 4; i++) 88 | randomStr += charset[Math.floor(Math.random() * charset.length)]; 89 | 90 | const Buyer = await BuyerSchema.findOneAndUpdate( 91 | { email: email }, 92 | { 93 | $setOnInsert: { 94 | bought: false, 95 | secret: randomStr, 96 | this: { 97 | monday: { 98 | breakfast: false, 99 | lunch: false, 100 | dinner: false 101 | }, 102 | tuesday: { 103 | breakfast: false, 104 | lunch: false, 105 | dinner: false 106 | }, 107 | wednesday: { 108 | breakfast: false, 109 | lunch: false, 110 | dinner: false 111 | }, 112 | thursday: { 113 | breakfast: false, 114 | lunch: false, 115 | dinner: false 116 | }, 117 | friday: { 118 | breakfast: false, 119 | lunch: false, 120 | dinner: false 121 | }, 122 | saturday: { 123 | breakfast: false, 124 | lunch: false, 125 | dinner: false 126 | }, 127 | sunday: { 128 | breakfast: false, 129 | lunch: false, 130 | dinner: false 131 | } 132 | }, 133 | next: { 134 | monday: { 135 | breakfast: false, 136 | lunch: false, 137 | dinner: false 138 | }, 139 | tuesday: { 140 | breakfast: false, 141 | lunch: false, 142 | dinner: false 143 | }, 144 | wednesday: { 145 | breakfast: false, 146 | lunch: false, 147 | dinner: false 148 | }, 149 | thursday: { 150 | breakfast: false, 151 | lunch: false, 152 | dinner: false 153 | }, 154 | friday: { 155 | breakfast: false, 156 | lunch: false, 157 | dinner: false 158 | }, 159 | saturday: { 160 | breakfast: false, 161 | lunch: false, 162 | dinner: false 163 | }, 164 | sunday: { 165 | breakfast: false, 166 | lunch: false, 167 | dinner: false 168 | } 169 | } 170 | } 171 | }, 172 | { new: true, upsert: true } 173 | ).select({ _id: 0 }); 174 | return Buyer; 175 | } 176 | 177 | // Resets the user secret and returns the updated user object 178 | module.exports.resetSecret = async function (email) { 179 | let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789"; 180 | let randomStr = ""; 181 | for (let i = 0; i < 4; i++) 182 | randomStr += charset[Math.floor(Math.random() * charset.length)]; 183 | 184 | const Buyer = await BuyerSchema.findOneAndUpdate( 185 | { email: email }, 186 | { secret: randomStr }).select({ _id: 0 }); 187 | return Buyer; 188 | } 189 | 190 | // Check if the user's coupon is valid for the current day and meal 191 | module.exports.checkCoupon = async function (data) { 192 | const Buyer = await BuyerSchema.findOne({ email: data.email, secret: data.secret }); 193 | if (Buyer == null) return false; 194 | if (Buyer.this[data.day][data.type]) { 195 | await BuyerSchema.updateOne({ email: data.email }, { ["this." + data.day + "." + data.type]: false }); 196 | return true; 197 | } 198 | return false; 199 | } 200 | 201 | // Save the purchased coupons after a successful payment 202 | module.exports.saveOrder = async function (email, data) { 203 | await BuyerSchema.updateOne({ email: email }, { next: data, bought: true }); 204 | } 205 | 206 | // Check if the user has already bought the coupons for the coming week 207 | module.exports.boughtNextWeek = async function (email) { 208 | await module.exports.getBuyer(email); 209 | const Buyer = await BuyerSchema.findOne({ email: email }); 210 | return Buyer.bought; 211 | } 212 | 213 | // Returns details of all the users 214 | module.exports.allBuyers = async function () { 215 | const Buyers = await BuyerSchema.find({}); 216 | return Buyers; 217 | } -------------------------------------------------------------------------------- /frontend/src/routes/AdminPanel/index.js: -------------------------------------------------------------------------------- 1 | import classes from './index.module.css'; 2 | import { Button, Input, Card, Table, message } from 'antd'; 3 | import { SaveOutlined } from '@ant-design/icons' 4 | import axios from "axios"; 5 | import { useState, useEffect } from "react"; 6 | import { motion } from "framer-motion"; 7 | 8 | function WeekMenu(menu, setMenu) { 9 | return menu.map(e => 10 |
({ 20 | props: { 21 | style: { background: "#FAFAFA" }, 22 | }, 23 | children: {text}, 24 | }) 25 | }, 26 | { 27 | title: '', 28 | dataIndex: 'Meal', 29 | key: 'Meal', 30 | colSpan: 0, 31 | width: 300, 32 | render: (text, record) => ({ 33 | props: { 34 | style: { padding: 0 }, 35 | }, 36 | children: { 38 | for (let i = 0; i < menu.length; i++) { 39 | let temp = menu; 40 | if (menu[i].day === e.day) { 41 | temp[i][record.Type.toLowerCase()] = ele.target.value; 42 | setMenu(temp); 43 | console.log(temp); 44 | break; 45 | } 46 | } 47 | }} 48 | />, 49 | }) 50 | }, 51 | ] 52 | } 53 | dataSource={ 54 | [ 55 | { 56 | key: '1', 57 | Type: 'Breakfast', 58 | Meal: e.breakfast, 59 | }, 60 | { 61 | key: '2', 62 | Type: 'Lunch', 63 | Meal: e.lunch, 64 | }, 65 | { 66 | key: '3', 67 | Type: 'Dinner', 68 | Meal: e.dinner, 69 | }, 70 | ] 71 | } 72 | pagination={false} bordered /> 73 | ); 74 | } 75 | 76 | export default function AdminPanel() { 77 | // TIME AND COST START 78 | const [timingRow, setTimingRow] = useState([]); 79 | const [savingTime, setSavingTime] = useState(false); 80 | 81 | const timingCol = [ 82 | { 83 | title: 'Meal', 84 | dataIndex: 'meal', 85 | key: 'meal', 86 | render: (text) => ({ 87 | props: { 88 | style: { background: "#FAFAFA" }, 89 | }, 90 | children: {text} 91 | }) 92 | }, 93 | { 94 | title: 'Rs', 95 | dataIndex: 'cost', 96 | key: 'cost', 97 | render: (text, record) => ({ 98 | props: { 99 | style: { padding: 0 }, 100 | }, 101 | children: { 103 | for (let i = 0; i < timingRow.length; i++) { 104 | let temp = timingRow; 105 | if (timingRow[i].meal === record.meal) { 106 | temp[i].cost = parseInt(e.target.value, 10); 107 | setTimingRow(temp); 108 | break; 109 | } 110 | } 111 | }} 112 | />, 113 | }), 114 | width: 100 115 | }, 116 | { 117 | title: 'Time', 118 | dataIndex: 'time', 119 | key: 'time', 120 | render: (text, record) => ({ 121 | props: { 122 | style: { padding: 0 }, 123 | }, 124 | children: { 126 | for (let i = 0; i < timingRow.length; i++) { 127 | let temp = timingRow; 128 | if (timingRow[i].meal === record.meal) { 129 | temp[i].time = e.target.value; 130 | setTimingRow(temp); 131 | break; 132 | } 133 | } 134 | }} 135 | />, 136 | }) 137 | } 138 | ]; 139 | 140 | const fetchTime = async () => { 141 | try { 142 | let response = await axios.get(window.APIROOT + 'api/data/time'); 143 | setTimingRow(response.data); 144 | console.log(response.data); 145 | } catch (error) { 146 | message.error('Failed to fetch timing from server'); 147 | } 148 | } 149 | 150 | const setTime = async () => { 151 | setSavingTime(true); 152 | try { 153 | await axios.post(window.APIROOT + 'api/admin/setTime', { times: timingRow }); 154 | message.success('Changes saved successfully') 155 | } catch (error) { 156 | message.error('Failed to save changes'); 157 | } 158 | setSavingTime(false); 159 | } 160 | 161 | useEffect(() => { 162 | fetchTime(); 163 | }, []); 164 | // TIME AND COST END 165 | 166 | 167 | // MENU START 168 | const [menu, setMenu] = useState([]); 169 | const [savingMenu, setSavingMenu] = useState(false); 170 | 171 | const fetchMenu = async () => { 172 | try { 173 | const response = await axios.get(window.APIROOT + 'api/data/menu'); 174 | setMenu(response.data); 175 | } catch (error) { 176 | message.error('Failed to fetch menu from server'); 177 | } 178 | } 179 | 180 | const saveMenu = async () => { 181 | setSavingMenu(true); 182 | try { 183 | await axios.post(window.APIROOT + 'api/admin/setMenu', { menus: menu }); 184 | message.success('Changes saved successfully') 185 | } catch (error) { 186 | message.error('Failed to save changes'); 187 | } 188 | setSavingMenu(false); 189 | } 190 | 191 | useEffect(() => { 192 | fetchMenu(); 193 | }, []); 194 | // MENU END 195 | 196 | return ( 197 | <> 198 |
199 | 200 |

TIME & COST

201 |
202 |
203 | 204 |
205 | 206 | 207 | 208 | 209 | {menu.length ? 210 |

MENU

211 | {WeekMenu(menu, setMenu)} 212 |
213 | 214 |
215 |
: null} 216 |
217 | 218 | ); 219 | } -------------------------------------------------------------------------------- /frontend/src/routes/Buy/index.js: -------------------------------------------------------------------------------- 1 | import classes from './index.module.css'; 2 | import { Table, Button, message, Card } from 'antd'; 3 | import { ShoppingCartOutlined } from '@ant-design/icons'; 4 | import { useState, useEffect, useCallback } from 'react'; 5 | import axios from "axios"; 6 | import useRazorpay from "react-razorpay"; 7 | 8 | const dayToNum = { "monday": 0, "tuesday": 1, "wednesday": 2, "thursday": 3, "friday": 4, "saturday": 5, "sunday": 6 }; 9 | 10 | const columns = [ 11 | { 12 | title: 'Meal', 13 | dataIndex: 'meal', 14 | width: 90, 15 | }, 16 | { 17 | title: 'Rs', 18 | dataIndex: 'cost', 19 | width: 50, 20 | }, 21 | { 22 | title: 'Menu', 23 | dataIndex: 'menu', 24 | }, 25 | ]; 26 | 27 | async function createOrder(selected) { 28 | let response = await axios.post(window.APIROOT + 'api/user/createOrder', { selected: selected }); 29 | return response.data; 30 | } 31 | 32 | async function CheckOrder(resp, setBought) { 33 | let response = await axios.post(window.APIROOT + 'api/user/checkOrder', resp); 34 | if (response.data) { 35 | message.success("Coupons bought!"); 36 | setBought(true); 37 | } 38 | else { 39 | message.error("Failed to buy coupons!"); 40 | } 41 | } 42 | 43 | function PayButton(props) { 44 | const Razorpay = useRazorpay(); 45 | const handlePayment = useCallback(async () => { 46 | const order = await createOrder(props.selected); 47 | const options = { 48 | key: "rzp_test_LfhDxl3w4SvQr8", 49 | amount: order.amount.toString(), 50 | currency: "INR", 51 | name: "Mess Portal", 52 | order_id: order.id, 53 | handler: async (res) => { 54 | await CheckOrder(res, props.setBought); 55 | } 56 | }; 57 | 58 | const rzpay = new Razorpay(options); 59 | rzpay.open(); 60 | }, [Razorpay, props]); 61 | 62 | return ( 63 | 64 | ); 65 | } 66 | 67 | export default function BuyPage() { 68 | const [cost, setCost] = useState(0); 69 | const [loading, setLoading] = useState(true); 70 | const [bought, setBought] = useState(false); 71 | const [menu, setMenu] = useState([ 72 | { 73 | day: "monday", 74 | breakfast: { text: "", cost: "" }, 75 | lunch: { text: "", cost: "" }, 76 | dinner: { text: "", cost: "" }, 77 | }, 78 | { 79 | day: 'tuesday', 80 | breakfast: { text: "", cost: "" }, 81 | lunch: { text: "", cost: "" }, 82 | dinner: { text: "", cost: "" }, 83 | }, 84 | { 85 | day: 'wednesday', 86 | breakfast: { text: "", cost: "" }, 87 | lunch: { text: "", cost: "" }, 88 | dinner: { text: "", cost: "" }, 89 | }, 90 | { 91 | day: 'thursday', 92 | breakfast: { text: "", cost: "" }, 93 | lunch: { text: "", cost: "" }, 94 | dinner: { text: "", cost: "" }, 95 | }, 96 | { 97 | day: 'friday', 98 | breakfast: { text: "", cost: "" }, 99 | lunch: { text: "", cost: "" }, 100 | dinner: { text: "", cost: "" }, 101 | }, 102 | { 103 | day: 'saturday', 104 | breakfast: { text: "", cost: "" }, 105 | lunch: { text: "", cost: "" }, 106 | dinner: { text: "", cost: "" }, 107 | }, 108 | { 109 | day: 'sunday', 110 | breakfast: { text: "", cost: "" }, 111 | lunch: { text: "", cost: "" }, 112 | dinner: { text: "", cost: "" }, 113 | } 114 | ]); 115 | const [selected, setSelected] = useState( 116 | { 117 | monday: { breakfast: false, lunch: false, dinner: false }, 118 | tuesday: { breakfast: false, lunch: false, dinner: false }, 119 | wednesday: { breakfast: false, lunch: false, dinner: false }, 120 | thursday: { breakfast: false, lunch: false, dinner: false }, 121 | friday: { breakfast: false, lunch: false, dinner: false }, 122 | saturday: { breakfast: false, lunch: false, dinner: false }, 123 | sunday: { breakfast: false, lunch: false, dinner: false }, 124 | } 125 | ); 126 | 127 | useEffect(() => { 128 | let cost = 0; 129 | for (const [day, val] of Object.entries(selected)) { 130 | for (const [meal, mealV] of Object.entries(val)) { 131 | if (mealV) 132 | cost += menu[dayToNum[day]][meal].cost; 133 | } 134 | } 135 | setCost(cost); 136 | }, [menu, selected]); 137 | 138 | useEffect(() => { 139 | const fetchData = async () => { 140 | try { 141 | let response = await axios.get(window.APIROOT + 'api/data/menu'); 142 | let cost = await axios.get(window.APIROOT + 'api/data/time'); 143 | let data = {}; 144 | for (let c of cost.data) data[c.meal] = c.cost; 145 | for (let i = 0; i < response.data.length; i++) { 146 | response.data[i].breakfast = { text: response.data[i].breakfast, cost: data.breakfast } 147 | response.data[i].lunch = { text: response.data[i].lunch, cost: data.lunch } 148 | response.data[i].dinner = { text: response.data[i].dinner, cost: data.dinner } 149 | } 150 | setMenu(response.data); 151 | setLoading(false); 152 | } catch (error) { 153 | console.log(error); 154 | message.error('Failed to fetch menu from server'); 155 | } 156 | } 157 | fetchData(); 158 | }, []); 159 | 160 | useEffect(() => { 161 | const fetchData = async () => { 162 | try { 163 | let response = await axios.get(window.APIROOT + 'api/user/boughtNextWeek'); 164 | setBought(response.data); 165 | } catch (error) { 166 | message.error('Failed to fetch data from server'); 167 | } 168 | } 169 | fetchData(); 170 | }, []); 171 | 172 | return (<>{bought ? 173 |
174 | 181 | You can buy coupons for a week, the week before. You have already bought coupons for the next week. 182 | 183 |
184 | : 185 |
186 | { 187 | menu.map(e => 188 | <> 189 |

{e.day}

190 |
{ 193 | setSelected({ 194 | ...selected, 195 | [e.day]: { 196 | breakfast: skeys.includes('breakfast'), 197 | lunch: skeys.includes('lunch'), 198 | dinner: skeys.includes('dinner'), 199 | } 200 | }); 201 | } 202 | }} columns={columns} 203 | dataSource={ 204 | [ 205 | { 206 | key: 'breakfast', 207 | meal: 'Breakfast', 208 | menu: e.breakfast.text, 209 | cost: e.breakfast.cost 210 | }, 211 | { 212 | key: 'lunch', 213 | meal: 'Lunch', 214 | menu: e.lunch.text, 215 | cost: e.lunch.cost 216 | }, 217 | { 218 | key: 'dinner', 219 | meal: 'Dinner', 220 | menu: e.dinner.text, 221 | cost: e.dinner.cost 222 | } 223 | ] 224 | } 225 | /> 226 | 227 | ) 228 | } 229 |

Total Cost: ₹{cost}

230 | 231 | 232 | }); 233 | } --------------------------------------------------------------------------------