diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..f912847 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,18 @@ +{ + "configurations": [ + { + "name": "windows-gcc-x86", + "includePath": [ + "${workspaceFolder}/**" + ], + "compilerPath": "C:/MinGW/bin/gcc.exe", + "cStandard": "${default}", + "cppStandard": "${default}", + "intelliSenseMode": "windows-gcc-x86", + "compilerArgs": [ + "" + ] + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a658df5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "C/C++ Runner: Debug Session", + "type": "cppdbg", + "request": "launch", + "args": [], + "stopAtEntry": false, + "externalConsole": true, + "cwd": "c:/Users/khush/AppData/Local/Microsoft/Windows/INetCache/IE/DKU7278T", + "program": "c:/Users/khush/AppData/Local/Microsoft/Windows/INetCache/IE/DKU7278T/build/Debug/outDebug", + "MIMode": "gdb", + "miDebuggerPath": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 3b66410..baf8220 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,60 @@ { - "git.ignoreLimitWarning": true + "git.ignoreLimitWarning": true, + "C_Cpp_Runner.cCompilerPath": "gcc", + "C_Cpp_Runner.cppCompilerPath": "g++", + "C_Cpp_Runner.debuggerPath": "gdb", + "C_Cpp_Runner.cStandard": "", + "C_Cpp_Runner.cppStandard": "", + "C_Cpp_Runner.msvcBatchPath": "C:/Program Files/Microsoft Visual Studio/VR_NR/Community/VC/Auxiliary/Build/vcvarsall.bat", + "C_Cpp_Runner.useMsvc": false, + "C_Cpp_Runner.warnings": [ + "-Wall", + "-Wextra", + "-Wpedantic", + "-Wshadow", + "-Wformat=2", + "-Wcast-align", + "-Wconversion", + "-Wsign-conversion", + "-Wnull-dereference" + ], + "C_Cpp_Runner.msvcWarnings": [ + "/W4", + "/permissive-", + "/w14242", + "/w14287", + "/w14296", + "/w14311", + "/w14826", + "/w44062", + "/w44242", + "/w14905", + "/w14906", + "/w14263", + "/w44265", + "/w14928" + ], + "C_Cpp_Runner.enableWarnings": true, + "C_Cpp_Runner.warningsAsError": false, + "C_Cpp_Runner.compilerArgs": [], + "C_Cpp_Runner.linkerArgs": [], + "C_Cpp_Runner.includePaths": [], + "C_Cpp_Runner.includeSearch": [ + "*", + "**/*" + ], + "C_Cpp_Runner.excludeSearch": [ + "**/build", + "**/build/**", + "**/.*", + "**/.*/**", + "**/.vscode", + "**/.vscode/**" + ], + "C_Cpp_Runner.useAddressSanitizer": false, + "C_Cpp_Runner.useUndefinedSanitizer": false, + "C_Cpp_Runner.useLeakSanitizer": false, + "C_Cpp_Runner.showCompilationTime": false, + "C_Cpp_Runner.useLinkTimeOptimization": false, + "C_Cpp_Runner.msvcSecureNoWarnings": false } \ No newline at end of file diff --git a/App.js b/App.js index b0e9721..aab9751 100644 --- a/App.js +++ b/App.js @@ -5,6 +5,7 @@ import { createStackNavigator } from '@react-navigation/stack'; import { AuthProvider, useAuth } from './context/AuthContext'; import { ThemeProvider } from './context/ThemeContext'; // Make sure to import ThemeProvider import MainTabNavigator from './navigation/MainTabNavigator'; +import WardenTabNavigator from './navigation/WardenTabNavigator'; import LoginScreen from './screens/LoginScreen'; import RegisterScreen from './screens/RegisterScreen'; import LoadingScreen from './screens/LoadingScreen'; @@ -15,6 +16,12 @@ import LibraryScreen from './screens/LibraryScreen'; import LogBook from './screens/LogBookScreen'; import { StackScreen } from 'react-native-screens'; import GuardDashboardScreen from './screens/GuardScreen'; +import WardenDashboardScreen from './screens/WardenDashboardScreen.'; +import OutpassScreen from './screens/OutpassScreen'; +// import EmergencyScreen from './screens/EmergencyScreen'; +import ProfileScreen from './screens/ProfileScreen'; + + const Stack = createStackNavigator(); function RootNavigator() { @@ -23,6 +30,20 @@ function RootNavigator() { if (loading) { return ; } + + // if user is not logged in + + if (!user) { + return ( + + + + + ) + } + + // user is logged in so its safe to use user.role now otherwise error would have came + return ( {user ? ( @@ -34,8 +55,18 @@ function RootNavigator() { - ): - (<> + ) : user.role == 'warden' ? ( + <> + + {/* */} + {/* */} + {/* */} + + + + + ) : ( + <> diff --git a/UPDATE.md b/UPDATE.md index bea1d5e..17252ac 100644 --- a/UPDATE.md +++ b/UPDATE.md @@ -42,3 +42,14 @@ - Your password will get updated and next time when you try to login you have to enter the newly setted password. + +*** WARDEN SIDE ACTIVE *** + +1. The warden is now able to register without any fail. +2. The warden dashboard fails to load the page but it appears with 3 tabs- Dashboard, Approvals and Profile + +3. Profile page is not getting loaded still but. + + + + diff --git a/components/OutpassCard.js b/components/OutpassCard.js index 7a3c6eb..ae8dd6c 100644 --- a/components/OutpassCard.js +++ b/components/OutpassCard.js @@ -258,3 +258,5 @@ const styles = StyleSheet.create({ marginTop: SPACING.xs, }, }) + + diff --git a/components/StudentRegisterCard.js b/components/StudentRegisterCard.js index fd73dcc..8d35e16 100644 --- a/components/StudentRegisterCard.js +++ b/components/StudentRegisterCard.js @@ -22,7 +22,7 @@ export default function StudentRegisterCard({ formData, updateFormData, departme updateFormData('studentId', value)} diff --git a/components/WardenOutpassCard.js b/components/WardenOutpassCard.js new file mode 100644 index 0000000..e601521 --- /dev/null +++ b/components/WardenOutpassCard.js @@ -0,0 +1,282 @@ +"use client" + +import { View, Text, StyleSheet, TouchableOpacity, Alert } from "react-native" +import { Ionicons } from "@expo/vector-icons" +import { commonAPI } from "../services/api" +import { COLORS, FONTS, SIZES, SPACING, OUTPASS_STATUS } from "../utils/constants" + +export default function WardenOutpassCard({ outpass, onUpdate }) { + const getStatusColor = (status) => { + switch (status) { + case OUTPASS_STATUS.PENDING: + return COLORS.warning + case OUTPASS_STATUS.APPROVED: + return COLORS.success + case OUTPASS_STATUS.REJECTED: + return COLORS.error + case OUTPASS_STATUS.ACTIVE: + return COLORS.primary + case OUTPASS_STATUS.COMPLETED: + return COLORS.gray[500] + default: + return COLORS.gray[400] + } + } + + const getStatusIcon = (status) => { + switch (status) { + case OUTPASS_STATUS.PENDING: + return "time-outline" + case OUTPASS_STATUS.APPROVED: + return "checkmark-circle-outline" + case OUTPASS_STATUS.REJECTED: + return "close-circle-outline" + case OUTPASS_STATUS.ACTIVE: + return "play-circle-outline" + case OUTPASS_STATUS.COMPLETED: + return "checkmark-done-outline" + default: + return "help-circle-outline" + } + } + + const formatDate = (dateString) => + new Date(dateString).toLocaleDateString("en-GB", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }) + + const formatTime = (dateString) => + new Date(dateString).toLocaleTimeString("en-GB", { + hour: "2-digit", + minute: "2-digit", + }) + + const updateStatus = (status) => { + Alert.alert( + "Confirm Action", + `Are you sure you want to ${status} this outpass?`, + [ + { text: "Cancel", style: "cancel" }, + { + text: "Yes", + onPress: async () => { + try { + const response = await commonAPI.updateOutpass(outpass._id, { + status, + }) + onUpdate(response.data) + Alert.alert("Success", `Outpass ${status} successfully`) + } catch (error) { + Alert.alert("Error", "Failed to update outpass") + } + }, + }, + ] + ) + } + + const canTakeAction = outpass.status === OUTPASS_STATUS.PENDING + + return ( + + {/* HEADER */} + + + + + {outpass.status.toUpperCase()} + + + + + {/* CONTENT */} + + {/* Student Info (warden-specific) */} + {outpass.student?.name} + + ID: {outpass.student?.studentId} | Room: {outpass.student?.roomNumber} + + + {outpass.purpose || outpass.reason} + {outpass.destination} + + + + + From + + {formatDate(outpass.fromDate || outpass.outDate)} at{" "} + {formatTime(outpass.fromTime || outpass.outDate)} + + + + + + To + + {formatDate(outpass.toDate || outpass.expectedReturnDate)} at{" "} + {formatTime(outpass.toTime || outpass.expectedReturnDate)} + + + + + {outpass.remarks && ( + + Remarks: + {outpass.remarks} + + )} + + + {/* ACTIONS */} + {canTakeAction && ( + + updateStatus("approved")} + > + + Approve + + + updateStatus("rejected")} + > + + Reject + + + )} + + + {/* FOOTER */} + + + Requested on {formatDate(outpass.createdAt)} + + + + ) +} + + + + +const styles = StyleSheet.create({ + card: { + backgroundColor: COLORS.white, + borderRadius: 12, + padding: SPACING.lg, + marginBottom: SPACING.md, + elevation: 3, + }, + cardHeader: { + marginBottom: SPACING.md, + }, + statusContainer: { + flexDirection: "row", + alignItems: "center", + }, + statusText: { + fontSize: SIZES.sm, + fontFamily: FONTS.bold, + marginLeft: SPACING.xs, + }, + cardContent: { + marginBottom: SPACING.md, + }, + studentName: { + fontSize: SIZES.md, + fontFamily: FONTS.bold, + color: COLORS.gray[800], + }, + studentMeta: { + fontSize: SIZES.sm, + color: COLORS.gray[600], + marginBottom: SPACING.sm, + }, + purpose: { + fontSize: SIZES.lg, + fontFamily: FONTS.bold, + color: COLORS.gray[800], + }, + destination: { + fontSize: SIZES.md, + color: COLORS.gray[600], + marginBottom: SPACING.md, + }, + timeContainer: { + marginBottom: SPACING.md, + }, + timeItem: { + flexDirection: "row", + alignItems: "center", + marginBottom: SPACING.xs, + }, + timeLabel: { + fontSize: SIZES.sm, + marginLeft: SPACING.xs, + marginRight: SPACING.xs, + minWidth: 30, + color: COLORS.gray[600], + }, + timeValue: { + fontSize: SIZES.sm, + color: COLORS.gray[800], + }, + remarksContainer: { + backgroundColor: COLORS.gray[50], + padding: SPACING.sm, + borderRadius: 8, + }, + remarksLabel: { + fontSize: SIZES.sm, + fontFamily: FONTS.bold, + }, + remarksText: { + fontSize: SIZES.sm, + color: COLORS.gray[600], + }, + actionRow: { + flexDirection: "row", + justifyContent: "space-between", + marginBottom: SPACING.sm, + }, + actionButton: { + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + padding: SPACING.sm, + borderRadius: 8, + flex: 1, + }, + approve: { + backgroundColor: COLORS.success, + marginRight: SPACING.xs, + }, + reject: { + backgroundColor: COLORS.error, + marginLeft: SPACING.xs, + }, + actionText: { + color: COLORS.white, + marginLeft: SPACING.xs, + fontFamily: FONTS.bold, + }, + cardFooter: { + borderTopWidth: 1, + borderTopColor: COLORS.gray[200], + paddingTop: SPACING.sm, + }, + createdAt: { + fontSize: SIZES.xs, + color: COLORS.gray[500], + }, +}) diff --git a/components/WardenRegisterCard.js b/components/WardenRegisterCard.js index 6e36e28..6152c42 100644 --- a/components/WardenRegisterCard.js +++ b/components/WardenRegisterCard.js @@ -1,4 +1,3 @@ - import React from 'react'; import { View, TextInput, StyleSheet } from 'react-native'; import { Picker } from '@react-native-picker/picker'; diff --git a/models/GuardUser.js b/models/GuardUser.js index 94f9e95..834be63 100644 --- a/models/GuardUser.js +++ b/models/GuardUser.js @@ -39,11 +39,12 @@ const userSchema = new mongoose.Schema( type: String, trim: true, }, - // deviceId: { - // type: String, - // required: true, - // unique: true, - // }, + deviceId: { + type: String, + required: true, + unique: true, + sparse: true, + }, guardId: { type: String, sparse: true, diff --git a/models/WardenUser.js b/models/WardenUser.js index 8b6327d..8046196 100644 --- a/models/WardenUser.js +++ b/models/WardenUser.js @@ -41,11 +41,10 @@ const userSchema = new mongoose.Schema( type: String, trim: true, }, - // deviceId: { - // type: String, - // required: true, - // unique: true, - // }, + deviceId: { + type: String, + required: true, + }, isActive: { type: Boolean, default: true, diff --git a/navigation/WardenTabNavigator.js b/navigation/WardenTabNavigator.js new file mode 100644 index 0000000..09e53b2 --- /dev/null +++ b/navigation/WardenTabNavigator.js @@ -0,0 +1,44 @@ +import { createBottomTabNavigator } from "@react-navigation/bottom-tabs" +import { Ionicons } from "@expo/vector-icons" + +// Import screens +import WardenDashboardScreen from "../screens/WardenDashboardScreen." +import ProfileScreen from "../screens/ProfileScreen" +import OutpassRequestsScreen from "../screens/OutpassRequestsScreen" +// import StudentsScreen from "../screens/StudentsScreen" + +const Tab = createBottomTabNavigator() + +export default function WardenTabNavigator() { + return ( + ({ + tabBarIcon: ({ focused, color, size }) => { + let iconName + + if (route.name === "Dashboard") { + iconName = focused ? "home" : "home-outline" + } else if (route.name === "Approvals") { + iconName = focused ? "checkmark-circle" : "checkmark-circle-outline"; + } else if (route.name === "Profile") { + iconName = focused ? "person" : "person-outline" + } else if (route.name === "Students") { + iconName = focused ? "people" : "people-outline"; + } + + + + return + }, + tabBarActiveTintColor: "#2563eb", + tabBarInactiveTintColor: "gray", + headerShown: false, + })} + > + + + {/* */} + + + ) + } \ No newline at end of file diff --git a/routes/authRoutes.js b/routes/authRoutes.js index 5e50dcc..ed7ea48 100644 --- a/routes/authRoutes.js +++ b/routes/authRoutes.js @@ -7,11 +7,12 @@ const GuardUser = require("../models/GuardUser") const { authenticate } = require("../middleware/auth"); const router = express.Router() + // Register new user router.post("/register", async (req, res) => { try { console.log("INCOMING REGISTRATION DATA:", req.body); - const { name, email, password, studentId, guardId, wardenId, hostel, roomNumber, phoneNumber, securityPost, emergencyContact, gender, year, department, role} = req.body + const { name, email, password, studentId, guardId, wardenId, hostel, roomNumber, phoneNumber, securityPost, gender, year, department, role, deviceId} = req.body let user; const hashPassword = await bcrypt.hash(password, 10) @@ -37,7 +38,6 @@ router.post("/register", async (req, res) => { hostel, roomNumber, phoneNumber, - emergencyContact, gender, year, department, @@ -48,7 +48,7 @@ router.post("/register", async (req, res) => { else if(role == "warden") { // Check if user already exists const existingUser = await WardenUser.findOne({ - $or: [{ email }, { Id }], + $or: [{ email }], }) if (existingUser) { @@ -63,20 +63,20 @@ router.post("/register", async (req, res) => { password: hashPassword, hostel, phoneNumber, - gender, + deviceId, }) } else { // Create new user - // Check if user already exists + // Check if user already exists const existingUser = await GuardUser.findOne({ - $or: [{ email }], + $or: [{ email }, { guardId }], }) if (existingUser) { return res.status(400).json({ - message: "Guard with this ID already exists", + message: "Guard with this email or ID already exists", }) } @@ -87,9 +87,8 @@ router.post("/register", async (req, res) => { location : securityPost, guardId: guardId, phoneNumber, - emergencyContact, - gender, + deviceId, }) } @@ -186,7 +185,7 @@ router.post("/login", async (req, res) => { }, }); } catch (error) { - console.error("Login error:", error);1 + console.error("Login error:", error); res.status(500).json({ message: "Server error during login" }); } }) diff --git a/routes/forgotRoute.js b/routes/forgotRoute.js index ee3938d..5c2b4a7 100644 --- a/routes/forgotRoute.js +++ b/routes/forgotRoute.js @@ -31,7 +31,7 @@ router.post('/', async (req , res) => { } user.password = await bcrypt.hash(newPassword, 10); - await user.save(); + let mailOptions = { from: process.env.GMAIL_ID, @@ -43,11 +43,14 @@ router.post('/', async (req , res) => { transporter.sendMail(mailOptions, function(error, info){ if (error) { console.log(error); + return; } else { console.log('Email sent: ' + info.response); } }); + await user.save(); + return res.status(200).json({ message: 'Password reset email sent' }); } catch(err){ diff --git a/routes/outpassRoutes.js b/routes/outpassRoutes.js index 12d18b4..ff4cf4a 100644 --- a/routes/outpassRoutes.js +++ b/routes/outpassRoutes.js @@ -1,6 +1,7 @@ const express = require("express") const Outpass = require("../models/Outpass") const User = require("../models/User") +const WardenUser = require("../models/WardenUser") const Log = require("../models/Log") const { authenticate } = require("../middleware/auth"); const adminAuth = require("../middleware/adminAuth") @@ -379,6 +380,52 @@ router.get("/history", [authenticate, checkOutpassExpiry], async (req, res) => { } }) + +// Warden: Get outpass requests for students of warden's hostel +router.get("/warden/requests", authenticate, async (req, res) => { + try { + // Ensure only wardens can access + if (req.user.role !== "warden") { + return res.status(403).json({ message: "Access denied" }) + } + + const wardenHostel = req.user.hostel + + if (!wardenHostel) { + return res.status(400).json({ message: "Warden hostel not assigned" }) + } + + // Expire old outpasses before fetching + await expireOldOutpasses() + + const outpasses = await Outpass.find({ + status: { $in: ["pending", "approved", "rejected", "expired", "cancelled"] }, + }) + .populate({ + path: "userId", + select: "name studentId hostel roomNumber", + match: { hostel: wardenHostel }, + }) + .sort({ createdAt: -1 }) + + // Remove outpasses whose users didn't match hostel + const filteredOutpasses = outpasses + .filter((op) => op.userId) + .map((op) => ({ + ...op.toObject(), + student: op.userId, // 🔑 aligns with WardenOutpassCard + })) + + res.json({ + outpasses: filteredOutpasses, + }) + } catch (error) { + console.error("Warden outpass fetch error:", error) + res.status(500).json({ message: "Server error fetching outpass requests" }) + } +}) + + // Get current day's outpass for user router.get("/today", [authenticate, checkOutpassExpiry], async (req, res) => { try { diff --git a/screens/CreateOutpassScreen.js b/screens/CreateOutpassScreen.js index c348d97..8cd886a 100644 --- a/screens/CreateOutpassScreen.js +++ b/screens/CreateOutpassScreen.js @@ -19,7 +19,6 @@ import { outpass } from "../services/api" import {useAuth} from "../context/AuthContext" import { COLORS, FONTS, SIZES, SPACING } from "../utils/constants" import LoadingSpinner from "../components/LoadingSpinner" - import styles from "../styles/CreateOutpassStyles" export default function CreateOutpassScreen({ navigation }) { @@ -190,7 +189,6 @@ export default function CreateOutpassScreen({ navigation }) { placeholderTextColor={colors.subText} value={formData.emergencyName} onChangeText={(value) => updateFormData("emergencyName", value)} - keyboardType="phone-pad" /> diff --git a/screens/GuardScreen.js b/screens/GuardScreen.js index b3c6a0d..3c80323 100644 --- a/screens/GuardScreen.js +++ b/screens/GuardScreen.js @@ -31,7 +31,7 @@ export default function GuardDashboardScreen({ navigation }) { }, []) // Ensure a default location is selected (use profile.location if available, - // otherwise fall back to the first entry in AllLocations) + // otherwise fall back to the first entry in All Locations) useEffect(() => { if (!loc) { if (profile?.location) { diff --git a/screens/LibraryScreen.js b/screens/LibraryScreen.js index 0b1c937..5234edb 100644 --- a/screens/LibraryScreen.js +++ b/screens/LibraryScreen.js @@ -255,7 +255,7 @@ export default function LibraryScreen({ navigation }) { {book.name} {book.isDue && ( - (DUE) + (DUE) )} diff --git a/screens/LogBookScreen.js b/screens/LogBookScreen.js index dec8422..0838ffa 100644 --- a/screens/LogBookScreen.js +++ b/screens/LogBookScreen.js @@ -92,10 +92,10 @@ export default function LogBook({ navigation, route }) { ]} > {actionLabel} - Resident: {residentName} - Guard: {guardOnDuty} - Location: {item.location || "-"} - Time: {timestamp} + Resident: {residentName} + Guard: {guardOnDuty} + Location: {item.location || "-"} + Time: {timestamp} ) }, diff --git a/screens/OutpassRequestsScreen.js b/screens/OutpassRequestsScreen.js new file mode 100644 index 0000000..22a0505 --- /dev/null +++ b/screens/OutpassRequestsScreen.js @@ -0,0 +1,147 @@ +"use client" + +import { View, Text, TouchableOpacity, RefreshControl, Alert, FlatList } from "react-native" +import { useState, useEffect, useMemo } from "react" +import { Ionicons } from "@expo/vector-icons" +import { useTheme } from "../context/ThemeContext" +import { useNavigation } from "@react-navigation/native" +import { outpass } from "../services/api" +import { COLORS } from "../utils/constants" + +import styles from "../styles/OutpassStyles" + +import LoadingSpinner from "../components/LoadingSpinner" +import FilterTabs from "../components/FilterTabs" +import WardenOutpassCard from "../components/WardenOutpassCard" + +export default function OutpassRequestsScreen() { + const { isDarkMode, toggleTheme, colors } = useTheme() + const navigation = useNavigation() + const [outpasses, setOutpasses] = useState([]) + const [filteredOutpasses, setFilteredOutpasses] = useState([]) + const [loading, setLoading] = useState(true) + const [refreshing, setRefreshing] = useState(false) + const [activeFilter, setActiveFilter] = useState("pending") + + // Count statuses + const statusCounts = useMemo(() => { + const counts = { + pending: 0, + approved: 0, + rejected: 0, + cancelled: 0, + expired: 0, + } + + outpasses.forEach((item) => { + if (counts[item.status] !== undefined) { + counts[item.status] += 1 + } + }) + + return counts + }, [outpasses]) + + const filterOptions = useMemo( + () => [ + { key: "pending", label: "Pending", count: statusCounts.pending }, + { key: "approved", label: "Approved", count: statusCounts.approved }, + { key: "rejected", label: "Rejected", count: statusCounts.rejected }, + { key: "cancelled", label: "Cancelled", count: statusCounts.cancelled }, + { key: "expired", label: "Expired", count: statusCounts.expired }, + ], + [statusCounts] + ) + + useEffect(() => { + loadOutpassRequests() + }, []) + + useEffect(() => { + filterOutpasses() + }, [outpasses, activeFilter]) + + const loadOutpassRequests = async () => { + try { + const response = await outpass.getPendingRequests() // WARDEN API written inside outpassRoute.js + setOutpasses(response.data?.outpasses || []) + } catch (error) { + console.log("Warden outpass load error:", error) + Alert.alert("Error", "Failed to load outpass requests") + } finally { + setLoading(false) + } + } + + const onRefresh = async () => { + setRefreshing(true) + await loadOutpassRequests() + setRefreshing(false) + } + + const filterOutpasses = () => { + if (activeFilter === "all") { + setFilteredOutpasses(outpasses) + } else { + setFilteredOutpasses(outpasses.filter((o) => o.status === activeFilter)) + } + } + + // Update local state after approve/reject + const handleStatusUpdate = (updatedOutpass) => { + setOutpasses((prev) => + prev.map((op) => (op._id === updatedOutpass._id ? updatedOutpass : op)) + ) + } + + const renderOutpassCard = ({ item }) => ( + + ) + + const renderEmptyState = () => ( + + + No Requests + No outpass requests in this category + + ) + + if (loading) { + return + } + + return ( + + {/* Header */} + + + Outpass Requests + + + + + + + + + item._id} + contentContainerStyle={styles.listContainer} + refreshControl={} + ListEmptyComponent={renderEmptyState} + showsVerticalScrollIndicator={false} + /> + + ) +} diff --git a/screens/OutpassScreen.js b/screens/OutpassScreen.js index 7076f4b..f8f724b 100644 --- a/screens/OutpassScreen.js +++ b/screens/OutpassScreen.js @@ -112,7 +112,9 @@ export default function OutpassScreen() { setOutpasses((prev) => prev.map((op) => (op._id === updatedOutpass._id ? updatedOutpass : op))) } - const renderOutpassCard = ({ item }) => + const renderOutpassCard = ({ item }) => ( + + ) const renderEmptyState = () => ( @@ -125,7 +127,7 @@ export default function OutpassScreen() { ? "Create your first outpass request to get started" : `You don't have any ${activeFilter} outpasses`} - {activeFilter === "all" && ( + { ( Create Outpass diff --git a/screens/ProfileScreen.js b/screens/ProfileScreen.js index fc00ab2..ed6a7b2 100644 --- a/screens/ProfileScreen.js +++ b/screens/ProfileScreen.js @@ -14,11 +14,17 @@ import { Picker } from "@react-native-picker/picker" export default function ProfileScreen() { const { isDarkMode, toggleTheme, colors } = useTheme(); const { user, logout } = useAuth() + + const isStudent = user.role === "student" + const isWarden = user.role === "warden" + const [profile, setProfile] = useState(null) const [editing, setEditing] = useState(false) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) + // password update state + const [currentPassword, setCurrentPassword] = useState("") const [newPassword, setNewPassword] = useState("") const [confirmPassword, setConfirmPassword] = useState("") @@ -69,27 +75,35 @@ export default function ProfileScreen() { setProfile((prev) => ({ ...prev, [key]: value })) } - if (loading) { - return - } + if (loading) return + return ( + + {/* HEADER */} + + + - {profile?.name?.charAt(0)?.toUpperCase() || "U"} + {profile?.name?.charAt(0)?.toUpperCase() || "YOU"} {profile?.name} {profile?.role?.toUpperCase()} + + + {/* PERSONAL INFO */} + Personal Information @@ -128,7 +142,8 @@ export default function ProfileScreen() { {profile?.email} )} - + + {isStudent && ( Student ID {editing ? ( @@ -141,6 +156,7 @@ export default function ProfileScreen() { {profile?.studentId} )} + )} Phone Number @@ -157,6 +173,8 @@ export default function ProfileScreen() { + + {isStudent && ( Academic Information @@ -217,12 +235,12 @@ export default function ProfileScreen() { ))} + ) } - - + Room Number @@ -237,8 +255,11 @@ export default function ProfileScreen() { )} + )} + + {/* CHANGE PASSWORD */} + - Change Password @@ -349,6 +370,10 @@ export default function ProfileScreen() { + + {/* ACCOUNT STATUS */} + + Account Status @@ -372,6 +397,8 @@ export default function ProfileScreen() { + {/* LOGOUT */} + Logout diff --git a/screens/RegisterScreen.js b/screens/RegisterScreen.js index 1d4609d..6f7679a 100644 --- a/screens/RegisterScreen.js +++ b/screens/RegisterScreen.js @@ -71,9 +71,9 @@ export default function RegisterScreen({ navigation }) { } const validateForm = () => { - const { name, email, password, confirmPassword, studentId, department, year, hostel, phone, gender, deviceId } = formData + const { name, email, password, confirmPassword, studentId, wardenId, guardId, department, year, hostel, phone, gender, role, securityPost, roomNumber } = formData - if (!name || !email || !password || !studentId || !department || !year || !hostel || !phone || !gender) { + if (!name || !email || !password || !confirmPassword || !role || !phone || !gender) { Alert.alert("Error", "Please fill in all required fields") return false } @@ -88,14 +88,33 @@ export default function RegisterScreen({ navigation }) { return false } - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { - Alert.alert("Error", "Please enter a valid email address") - return false + Alert.alert("Error", "Please enter a valid email address"); + return false; } - return true - } + // role based check + + if(role === 'student') { + if(!department || !studentId || !year || !hostel || !roomNumber) { + Alert.alert("Error", "Please fill in all the required fields for student"); + return false; + } + } else if(role === 'warden') { + if(!hostel) { + Alert.alert("Error", "Please fill in all the required fields for warden"); + return false; + } + } else if (role === 'security') { + if (!guardId || !securityPost) { + Alert.alert("Error", "Please fill in all required fields for a security guard."); + return false; + } + } + + return true; + }; const handleRegister = async () => { if (!validateForm()) return diff --git a/screens/WardenDashboardScreen..js b/screens/WardenDashboardScreen..js new file mode 100644 index 0000000..39d380b --- /dev/null +++ b/screens/WardenDashboardScreen..js @@ -0,0 +1,149 @@ +"use client" + +import { View, Text, StyleSheet, ScrollView, TouchableOpacity, RefreshControl, Alert } from "react-native" +import { useState, useEffect } from "react" +import { useTheme } from "../context/ThemeContext" +import { Ionicons } from "@expo/vector-icons" +import { useAuth } from "../context/AuthContext" +import { outpass, commonAPI } from "../services/api" +import styles from "../styles/DashboardStyles" + +import LoadingSpinner from "../components/LoadingSpinner" +import PasskeyCard from "../components/PasskeyCard" + +export default function WardenDashboardScreen({ navigation }) { + const { isDarkMode, toggleTheme, colors } = useTheme(); + const { user, logout } = useAuth() + const [passkey, setPasskey] = useState(null) + const [stats, setStats] = useState({ + totalOutpasses: 0, + activeOutpasses: 0, + pendingOutpasses: 0, + }) + const [loading, setLoading] = useState(true) + const [refreshing, setRefreshing] = useState(false) + + useEffect(() => { + loadDashboardData() + }, []) + + const loadDashboardData = async () => { + try { + const [passkeyResponse, outpassesResponse] = await Promise.all([ + commonAPI.getDailyPasskey(), + outpass.getOutpasses(), + ]) + + setPasskey(passkeyResponse.data) + + if (outpassesResponse.data.outpass){ + const outpasses = outpassesResponse.data.outpass.auditTrail + setStats({ + totalOutpasses: outpasses.length, + activeOutpasses: outpasses.filter((op) => op.status === "approved").length, + pendingOutpasses: outpasses.filter((op) => op.status === "pending").length, + }) + } + } catch (error) { + console.log("Dashboard load error:", error) + Alert.alert("Error", "Failed to load dashboard data") + } finally { + setLoading(false) + } + } + + const onRefresh = async () => { + setRefreshing(true) + await loadDashboardData() + setRefreshing(false) + } + + const handleLogout = () => { + Alert.alert("Logout", "Are you sure you want to logout?", [ + { text: "Cancel", style: "cancel" }, + { text: "Logout", style: "destructive", onPress: logout }, + ]) + } + + if (loading) { + return + } + + return ( + } + > + + + + + + + Good {getGreeting()} + {user?.name} + {user?.hostel} + + + + + + + + + + + + + + + + + {/* Daily Passkey Card */} + + + {/* Quick Actions */} + + Quick Actions + + navigation.navigate("Scan")}> + + + + Scan + + + navigation.navigate("Library")}> + + + + Library + + + navigation.navigate("SAC")}> + + + + SAC + + + navigation.navigate("Profile")}> + + + + Profile + + + + + + ) +} + +const getGreeting = () => { + const hour = new Date().getHours() + if (hour < 12) return "Morning" + if (hour < 17) return "Afternoon" + return "Evening" +} + diff --git a/server.js b/server.js index a558358..f732370 100644 --- a/server.js +++ b/server.js @@ -17,6 +17,8 @@ const adminRoutes = require("./routes/adminRoutes") const securityRoutes = require("./routes/securityRoutes") const studentRoutes = require("./routes/studentRoutes") const forgotRoutes = require("./routes/forgotRoute") +// const wardenRoutes = require('./routes/wardenRoutes'); + const app = express() const PORT = process.env.PORT || 5000 @@ -68,6 +70,9 @@ app.use("/api/admin", adminRoutes) app.use("/api/security", securityRoutes) app.use("/api/student", studentRoutes) app.use("/api/forgot", forgotRoutes) +// app.use("/api/warden", wardenRoutes); + + // Health check endpoint app.get("/api/health", (req, res) => { diff --git a/services/api.js b/services/api.js index 3bbd4db..f6e3487 100644 --- a/services/api.js +++ b/services/api.js @@ -63,6 +63,7 @@ export const outpass = { createOutpass: (data) => api.post("/outpass/generate", data), updateOutpass: (id, data) => api.put(`/outpass/${id}`, data), getHistory: (params) => api.get("/outpass/history", { params }), + getPendingRequests: () => api.get("/outpass/warden/requests"), } // Emergency API endpoints diff --git a/styles/CreateOutpassStyles.js b/styles/CreateOutpassStyles.js index a0ad88a..52cb2d8 100644 --- a/styles/CreateOutpassStyles.js +++ b/styles/CreateOutpassStyles.js @@ -10,7 +10,6 @@ export default StyleSheet.create({ flexDirection: "row", justifyContent: "space-between", alignItems: "center", - paddingHorizontal: SPACING.lg, paddingTop: 50, paddingBottom: SPACING.md, backgroundColor: COLORS.white,