diff --git a/.env b/.env index abab530..65d5854 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ REACT_APP_API_URL=https://express-server-1.fly.dev +ADMIN_PIN=003342 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8d92677..a6d5572 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,10 +16,12 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@mui/icons-material": "^5.14.19", "@mui/material": "^5.14.19", + "@simplewebauthn/browser": "^10.0.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.6.5", + "chart.js": "^4.4.4", "cors": "^2.8.5", "express": "^4.18.2", "passport": "^0.7.0", @@ -27,6 +29,7 @@ "pg": "^8.11.3", "pg-promise": "^11.5.4", "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.20.1", "react-scripts": "5.0.1", @@ -4006,6 +4009,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -4469,6 +4477,19 @@ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz", "integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==" }, + "node_modules/@simplewebauthn/browser": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-10.0.0.tgz", + "integrity": "sha512-hG0JMZD+LiLUbpQcAjS4d+t4gbprE/dLYop/CkE01ugU/9sKXflxV5s0DRjdz3uNMFecatRfb4ZLG3XvF8m5zg==", + "dependencies": { + "@simplewebauthn/types": "^10.0.0" + } + }, + "node_modules/@simplewebauthn/types": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-10.0.0.tgz", + "integrity": "sha512-SFXke7xkgPRowY2E+8djKbdEznTVnD5R6GO7GPTthpHrokLvNKw8C3lFZypTxLI7KkCfGPfhtqB3d7OVGGa9jQ==" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -6986,6 +7007,17 @@ "node": ">=10" } }, + "node_modules/chart.js": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz", + "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -17379,6 +17411,15 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", diff --git a/package.json b/package.json index a8dab07..5cd9275 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,12 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@mui/icons-material": "^5.14.19", "@mui/material": "^5.14.19", + "@simplewebauthn/browser": "^10.0.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.6.5", + "chart.js": "^4.4.4", "cors": "^2.8.5", "express": "^4.18.2", "passport": "^0.7.0", @@ -21,6 +23,7 @@ "pg": "^8.11.3", "pg-promise": "^11.5.4", "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.20.1", "react-scripts": "5.0.1", diff --git a/public/EDIC.jpg b/public/EDIC.jpg new file mode 100644 index 0000000..5b3f251 Binary files /dev/null and b/public/EDIC.jpg differ diff --git a/public/EDIC2.jpg b/public/EDIC2.jpg new file mode 100644 index 0000000..1016cb4 Binary files /dev/null and b/public/EDIC2.jpg differ diff --git a/public/assets/automatic_wire_stripper-rs_pro.jpg b/public/assets/automatic_wire_stripper-rs_pro.jpg index 1e2f585..aa7b18f 100644 Binary files a/public/assets/automatic_wire_stripper-rs_pro.jpg and b/public/assets/automatic_wire_stripper-rs_pro.jpg differ diff --git a/public/assets/automatic_wire_stripper-rs_pro2.jpg b/public/assets/automatic_wire_stripper-rs_pro2.jpg new file mode 100644 index 0000000..1e2f585 Binary files /dev/null and b/public/assets/automatic_wire_stripper-rs_pro2.jpg differ diff --git a/public/assets/default.jpg b/public/assets/default.jpg index dadb951..658ecd2 100644 Binary files a/public/assets/default.jpg and b/public/assets/default.jpg differ diff --git a/public/assets/default2.jpg b/public/assets/default2.jpg new file mode 100644 index 0000000..dadb951 Binary files /dev/null and b/public/assets/default2.jpg differ diff --git a/public/assets/wire_wrap_tool-jonard_tools.jpg b/public/assets/wire_wrap_tool-jonard_tools.jpg index d8e75a5..5012eb7 100644 Binary files a/public/assets/wire_wrap_tool-jonard_tools.jpg and b/public/assets/wire_wrap_tool-jonard_tools.jpg differ diff --git a/public/assets/wire_wrap_tool-jonard_tools2.jpg b/public/assets/wire_wrap_tool-jonard_tools2.jpg new file mode 100644 index 0000000..d8e75a5 Binary files /dev/null and b/public/assets/wire_wrap_tool-jonard_tools2.jpg differ diff --git a/public/desktop_screenshot.png b/public/desktop_screenshot.png new file mode 100644 index 0000000..0fbdd53 Binary files /dev/null and b/public/desktop_screenshot.png differ diff --git a/public/hub.jpg b/public/hub.jpg new file mode 100644 index 0000000..9d1c153 Binary files /dev/null and b/public/hub.jpg differ diff --git a/public/hub_logo_white.png b/public/hub_logo_white.png index a9c2a3c..aeab4de 100644 Binary files a/public/hub_logo_white.png and b/public/hub_logo_white.png differ diff --git a/public/index.html b/public/index.html index 5385e5d..ef45253 100644 --- a/public/index.html +++ b/public/index.html @@ -3,7 +3,9 @@ + + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..d7313df --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,32 @@ +{ + "short_name": "Hub App", + "name": "Book Equipment, Makers and Spaces", + "icons": [ + { + "src": "/hub_logo_white.png", + "type": "image/png", + "sizes": "585x585" + } + ], + "id": "/?source=pwa", + "start_url": "/?source=pwa", + "background_color": "#FFF", + "display": "standalone", + "scope": "/", + "theme_color": "#FFF", + "description": "Weather forecast information", + "screenshots": [ + { + "src": "/phone_screenshot.png", + "type": "image/png", + "sizes": "1290x2795", + "form_factor": "narrow" + }, + { + "src": "/desktop_screenshot.png", + "type": "image/png", + "sizes": "1920x1080", + "form_factor": "wide" + } + ] + } \ No newline at end of file diff --git a/public/phone_screenshot.png b/public/phone_screenshot.png new file mode 100644 index 0000000..dcaf799 Binary files /dev/null and b/public/phone_screenshot.png differ diff --git a/src/App.js b/src/App.js index ca0e9c1..f1d3608 100644 --- a/src/App.js +++ b/src/App.js @@ -1,22 +1,65 @@ import './styles/App.css'; import Navbar from './components/Navbar'; import Home from './pages/Home'; -import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; +import Catalogue from './pages/Catalogue'; +import { Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import NewBorrowForm from './components/NewBorrowForm'; +import NewCollectForm from './components/NewCollectForm'; +import NewReturnForm from './components/NewReturnForm'; import { CartProvider } from './components/CartContext'; // Import the provider - +import { LocationProvider } from './components/LocationContext'; +import LoanDashboard from './pages/Dashboard'; +import OutlookBooking from './pages/Booking'; +import { useEffect, useState } from 'react'; +import VerifyPIN from './components/VerifyPIN'; function App() { + // Verify PIN logic for all situations + const [verifiedByStaff, setVerifiedByStaff] = useState(false); + const [verifying, setVerifying] = useState(false); + const location = useLocation(); + const navigate = useNavigate(); + + const startVerificationProcess = () => { + setVerifying(true); + } + + const handleVerificationResponse = (verified) => { + console.log('Verification response:', verified); + setVerifiedByStaff(verified); + setVerifying(false); + if (location.pathname=='/catalogue' && verified) navigate('/dashboard'); + } + + + useEffect(() => { + console.log('Location:', location.pathname); + if (location.pathname=='/dashboard') return; + setVerifiedByStaff(false); + setVerifying(false); + }, [location.pathname]); + return (
- + - - } /> - } /> - - + + + } /> + } /> + } /> + /*not used*/} /> + + {window.location.host != 'edic.vercel.app' /*only on edic-vercel.app*/ && + + } /> + } /> + alert('hi')} verifiedByStaff={verifiedByStaff} />} /> + + } + +
); diff --git a/src/components/InventoryList.js b/src/components/InventoryList.js index e8b782b..f1cfda4 100644 --- a/src/components/InventoryList.js +++ b/src/components/InventoryList.js @@ -3,6 +3,7 @@ import Modal from './Modal'; import '../styles/App.css'; import SearchBar from './SearchBar'; import { useCart } from './CartContext'; +import { useWhichLocation } from './LocationContext'; function InventoryItem({ item, onAddToCart }) { const [modalOpen, setModalOpen] = useState(false); @@ -31,8 +32,7 @@ function InventoryItem({ item, onAddToCart }) {
{item.item_name}

{item.item_name}

- {item.brand &&

{item.brand}

} -

Qty: {item.qty_available}

+

{item.brand}‌

setModalOpen(false)}> @@ -57,6 +57,7 @@ function InventoryItem({ item, onAddToCart }) { function InventoryList() { + const { whichLocation } = useWhichLocation(); const [items, setItems] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const [selectedCategories, setSelectedCategories] = useState([]); @@ -82,14 +83,15 @@ function InventoryList() { useEffect(() => { - fetch(`${API_URL}/api/inventory`) + console.log(whichLocation); + fetch(`${API_URL}/api/inventory`+ (whichLocation=='e2a' ? 'E2A' : '')) .then(response => response.json()) .then(data => { const groupedItems = groupAndSumItems(data); setItems(groupedItems); }) .catch(error => console.error('Error fetching data:', error)); - }, [API_URL]); + }, [API_URL, whichLocation]); const handleSearchChange = (newSearchTerm) => { @@ -157,9 +159,8 @@ function InventoryList() { .sort((a, b) => a.item_name.localeCompare(b.item_name)); // This line adds sorting by item_name return ( -
-
- + ) )}
-
); } diff --git a/src/components/LocationContext.js b/src/components/LocationContext.js new file mode 100644 index 0000000..a0279c2 --- /dev/null +++ b/src/components/LocationContext.js @@ -0,0 +1,17 @@ +import React, { createContext, useContext, useState } from 'react'; + +const LocationContext = createContext(); + +export function useWhichLocation() { + return useContext(LocationContext); +} + +export const LocationProvider = ({ children }) => { + const [whichLocation, setWhichLocation] = useState('hub'); + + return ( + + {children} + + ); +}; diff --git a/src/components/Navbar.js b/src/components/Navbar.js index b186833..512826b 100644 --- a/src/components/Navbar.js +++ b/src/components/Navbar.js @@ -5,18 +5,88 @@ import { Link } from "react-router-dom"; import "../styles/Navbar.css"; import { useNavigate, useLocation } from 'react-router-dom'; import { useCart } from "./CartContext"; +import Modal from "./Modal"; const defaultImageUrl = `/assets/default.jpg`; function Navbar() { const [isCartOpen, setIsCartOpen] = useState(false); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [loanID, setLoanID] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [actionType, setActionType] = useState(null); + let navigate = useNavigate(); + + const toggleCollections = () => { + setActionType('collect'); + setIsModalOpen(true); + setError(''); + setLoanID(''); + setIsCartOpen(false); + }; + + const toggleReturns = () => { + setActionType('return'); + setIsModalOpen(true); + setError(''); + setLoanID(''); + setIsCartOpen(false); + }; + + const handleCollectionsOrReturns = async (action, loanID) => { + try { + // Set loading state to true + setLoading(true); + + // Call the API to check loan details + const response = await fetch(`https://express-server-1.fly.dev/api/loan-details/${loanID}`); + setLoading(false); + + if (response.status === 404) { + // Loan not found, handle the error (e.g., display a message) + setError('Loan not found'); + // You might want to display an error message to the user here + return; // Stop further execution + } + + // Parse the response as JSON + const loanDetails = await response.json(); + + // Now you have the loan details in the `loanDetails` variable + console.log('Loan Details:', loanDetails); + + // If the loan items are already collected + if (action == 'collect' && loanDetails.status != 'Reserved') { + setError('Loan items are already collected.'); + return; // Stop further execution + } + + // If the loan items are already returned, or not collected yet + if (action == 'return' && loanDetails.status != 'Borrowed') { + setError('Loan items are already returned / not collected yet.'); + return; // Stop further execution + } + + // If the API call is successful (status code is not 404) + navigate(action=='collect' ? '/new-collect-form': '/new-return-form', { state: { loanDetails: loanDetails } }); + setIsModalOpen(false); + + } catch (error) { + // Handle any errors that occur during the API call + console.error('Error checking loan details:', error); + // You might want to display a generic error message to the user here + } + }; + const { cart, setCart } = useCart(); // Use useCart here let location = useLocation(); - const toggleCart = () => { setIsCartOpen(!isCartOpen); + setIsModalOpen(false); }; const removeFromCart = (indexToRemove) => { @@ -33,42 +103,74 @@ function Navbar() { }; return ( -
-
- - NUS - -
-
- Home -
- Cart ({cart.length}) +
+ setIsModalOpen(false)}> +

{actionType === 'collect' ? '๐Ÿ“ฆ Collections' : 'โ†ฉ๏ธ Returns'}

+
+ {error ?

{error}

: + <>

Enter the Loan ID from your email.

+ setLoanID(e.target.value)} + disabled={loading} + style={{margin:0, width:'100%'}} + />} + +
+
+
+
+ + NUS +
- {isCartOpen && ( -
-
My Cart
- {cart.map((item, index) => ( -
- {item.item_name} -
-
{item.item_name}
-
Qty: {item.qty_borrowed}
-
- {location.pathname !== '/new-borrow-form' && ( - - )} -
- ))} - +
+ {window.location.pathname=='/'?<>:๐Ÿ  Home} + + {window.location.host != 'edic.vercel.app' && <> +
+ ๐Ÿ“ฆ Collect +
+
+ โ†ฉ๏ธ Return +
+ } +
+ ๐Ÿ›’ Cart ({cart.length})
- )} + + + {isCartOpen && ( +
+
๐Ÿ›’ My Cart
+ {cart.map((item, index) => ( +
+ {item.item_name} +
+
{item.item_name}
+
Qty: {item.qty_borrowed}
+
+ {location.pathname !== '/new-borrow-form' && ( + + )} +
+ ))} + {cart.length === 0 &&

No items in cart.

} + +
+ )} +
); diff --git a/src/components/NewBorrowForm.js b/src/components/NewBorrowForm.js index d5d9a5c..7433c7d 100644 --- a/src/components/NewBorrowForm.js +++ b/src/components/NewBorrowForm.js @@ -3,9 +3,11 @@ import '../styles/NewBorrowForm.css'; import { useLocation } from 'react-router-dom'; import axios from 'axios'; import { useCart } from './CartContext'; +import { useWhichLocation } from './LocationContext'; function NewBorrowForm() { const location = useLocation(); + const { whichLocation } = useWhichLocation(); const selectedItems = useMemo(() => location.state?.selectedItems || [], [location.state?.selectedItems]); const [errors, setErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); @@ -14,7 +16,7 @@ function NewBorrowForm() { const [requiresApproval, setRequiresApproval] = useState(false); const submitButtonRef = useRef(null); - + console.log(whichLocation); const [formData, setFormData] = useState({ name: '', email: '', @@ -74,6 +76,11 @@ function NewBorrowForm() { isValid = false; } + if ((key === 'phone_number') && formData[key].length !== 8) { + newErrors[key] = 'Invalid phone number'; + isValid = false; + } + if ((key === 'start_usage_date' || key === 'end_usage_date') && formData[key]) { const date = new Date(formData[key]); const dayOfWeek = date.getDay(); @@ -109,6 +116,7 @@ function NewBorrowForm() { const formDataToSend = { ...formData, ...itemsData, + location: whichLocation || 'hub', completion_time: new Date().toISOString() }; @@ -117,6 +125,7 @@ function NewBorrowForm() { formDataToSend.project_supervisor_name = ''; formDataToSend.supervisor_email = ''; } + console.log(formDataToSend); await axios.post('https://express-server-1.fly.dev/api/submit-form', formDataToSend); setIsSubmitted(true); // Set this on successful submission @@ -169,7 +178,7 @@ function NewBorrowForm() {
); })} +

We collect your personal data to contact you regarding your loan transaction. Your data may be disclosed to third parties solely for this purpose.

+ +

By submitting this form, you consent to the collection, use, and disclosure of your data as described above. Please review your information for accuracy before clicking "Submit."

+
diff --git a/src/components/NewCollectForm.js b/src/components/NewCollectForm.js new file mode 100644 index 0000000..0410885 --- /dev/null +++ b/src/components/NewCollectForm.js @@ -0,0 +1,176 @@ +import React, { useState, useEffect, useMemo, useRef } from 'react'; +import '../styles/NewBorrowForm.css'; +import { useLocation } from 'react-router-dom'; +import axios from 'axios'; + +function NewCollectForm({verifiedByStaff, startVerification}) { + const location = useLocation(); + const loanDetails = useMemo(() => location.state?.loanDetails || {}); + const [errors, setErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(false); + + const [formData, setFormData] = useState({ + date: new Date().toISOString().split('T')[0], + staff_name: '', + serial_numbers: '', + }); + + const handleChange = (e) => { + const { name, value } = e.target; + + // Ensure 8 digit phone number + if (name === 'phone' && value.length > 8) return; + + let updatedErrors = { ...errors, [name]: '' }; + const updatedFormData = { ...formData, [name]: value }; + + // Check for weekend dates + if (name === 'start_usage_date' || name === 'end_usage_date') { + const date = new Date(value); + const dayOfWeek = date.getDay(); + if (dayOfWeek === 0 || dayOfWeek === 6) { // 0 = Sunday, 6 = Saturday + updatedErrors[name] = 'Weekend dates are not allowed'; + } + } + + setFormData(updatedFormData); + setErrors(updatedErrors); + }; + + const validateForm = () => { + let isValid = true; + let newErrors = {}; + + if (!verifiedByStaff) { + newErrors['verify'] = 'Get a staff to verify your collection'; + isValid = false; + } + + Object.keys(formData).forEach(key => { + if (!formData[key].trim() && key !== 'additional_remarks') { + newErrors[key] = 'Field cannot be blank'; + isValid = false; + } + + if ((key === 'date') && formData[key]) { + const date = new Date(formData[key]); + const dayOfWeek = date.getDay(); + if (dayOfWeek === 0 || dayOfWeek === 6) { + newErrors[key] = 'Weekend dates are not allowed'; + isValid = false; + } + } + }); + + // Log the current validation state for debugging + console.log("Validation Errors:", newErrors); + console.log("Is Form Valid:", isValid); + + setErrors(newErrors); + return isValid; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + if (isSubmitting) return; // Prevent further execution if already submitting + setIsSubmitting(true); // Set early to prevent multiple submissions + + if (validateForm()) { + try { + const formDataToSend = { + ...formData, + status: 'Borrowed', + loan_id: loanDetails.transaction_id, + completion_time: new Date().toISOString() + }; + + console.log('Submitting form with data:', formDataToSend); + await axios.post('https://express-server-1.fly.dev/api/loan-status/update', formDataToSend); + setIsSubmitted(true); // Set this on successful submission + } catch (error) { + console.error('Error submitting form:', error); + setIsSubmitting(false); // Reset on error as well + } finally { + setIsSubmitting(false); // Always reset submitting state after the operation + } + } else { + setIsSubmitting(false); // Reset if validation fails + } + }; + + useEffect(() => window.scrollTo(0, 0), []); + + if (isSubmitting) return
Submitting...
; + else if (isSubmitted) return
Form submitted successfully!
; + + return ( +
+

Items to Collect:

+

{loanDetails.student_name}

+
+ {loanDetails?.loan_items?.length > 0 ? ( +
    + {loanDetails.loan_items.map((item, index) => ( +
  1. {item.item_name} (Qty: {item.quantity})
  2. + ))} +
+ ) : ( +

No items selected.

+ )} +
+

๐Ÿ“†: {new Date(loanDetails.start_usage_date).toLocaleDateString()} to {new Date(loanDetails.end_usage_date).toLocaleDateString()}

+ +
+
+ {Object.keys(formData).slice(0,-2).map((key, index) => { + + return ( +
+ + + {errors[key] &&

{errors[key]}

} +
+ ); + })} +
+ + +