diff --git a/project.txt b/project.txt deleted file mode 100644 index 3e95e1a..0000000 --- a/project.txt +++ /dev/null @@ -1,10273 +0,0 @@ - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\(auth)\admin\employees\employee.css - -/* Update Inventory Page Styles */ - - -/* Buttons Above Table */ -.button-group { - display: flex; - gap: 20px; - margin-bottom: 20px; - justify-content: center; - } - - button { - padding: 10px 20px; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 16px; - transition: background-color 0.3s ease; - } - - button:hover { - opacity: 0.8; - } - - .btn-add { - background-color: #4CAF50; - color: white; - } - - .btn-add:hover { - background-color: #45a049; - } - - .btn-update { - background-color: #ff9800; - color: white; - } - - .btn-update:hover { - background-color: #f57c00; - } - - .btn-remove { - background-color: #f44336; - color: white; - } - - .btn-remove:hover { - background-color: #d32f2f; - } - - /* Table Styles */ - .menu-table { - width: 100%; - border-collapse: collapse; - margin-top: 20px; - margin-bottom: 40px; - } - - .menu-table th, - .menu-table td { - padding: 12px 15px; - text-align: center; - border: 1px solid #ddd; - } - - .menu-table th { - background-color: #4CAF50; - color: white; - } - - .menu-table tbody tr:nth-child(even) { - background-color: #f9f9f9; - } - - .menu-table tbody tr:hover { - background-color: #f1f1f1; - cursor: pointer; - } - - /* Highlight the selected row */ - .menu-table tbody tr.selected { - background-color: #333; - color: white; - } - /* Add this to your CSS file */ - .selected { - background-color: #4caf50; /* Highlight color for selected row */ - color: white; /* Text color for selected row */ - } - - /* Modal Styles */ - .modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - z-index: 1050; /* Make sure modal stays on top */ - } - - .modal-container { - background-color: white; - padding: 30px; - border-radius: 10px; - width: 400px; - box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); - } - - .modal h2 { - margin-bottom: 20px; - } - - .modal .form-group { - margin-bottom: 15px; - } - - .modal label { - display: block; - margin-bottom: 5px; - font-weight: bold; - } - - .modal input[type="text"], - .modal input[type="number"], - .modal input[type="checkbox"] { - width: 100%; - padding: 10px; - font-size: 14px; - border: 1px solid #ddd; - border-radius: 5px; - margin-top: 5px; - } - - .modal input[type="checkbox"] { - width: auto; - } - - .modal-actions { - display: flex; - gap: 20px; - justify-content: center; - } - - .modal-actions button { - padding: 10px 20px; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 16px; - } - - .modal-actions button:hover { - opacity: 0.8; - } - - .modal-actions button:first-child { - background-color: #4CAF50; - color: white; - } - - .modal-actions button:last-child { - background-color: #f44336; - color: white; - } - - /* Overlay to grey out background */ - .overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); /* Grey overlay */ - z-index: 1040; /* Ensure overlay is beneath the modal */ - } - /* Modal Overlay */ - .modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 9999; - } - - /* Modal */ - .modal { - background: #fff; - padding: 20px; - border-radius: 8px; - width: 400px; - max-width: 100%; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - } - - /* Close Button */ - .close-btn { - position: absolute; - top: 10px; - right: 10px; - background: none; - border: none; - font-size: 24px; - cursor: pointer; - } - - /* Form Fields */ - input { - width: 100%; - padding: 8px; - margin: 8px 0; - border: 1px solid #ddd; - border-radius: 4px; - } - - button:hover { - background-color: #0056b3; - } - - button:disabled { - background-color: #ccc; - cursor: not-allowed; - } - - form { - display: flex; - flex-direction: column; - } - - /* Centering the tab buttons and action buttons */ - .tab-navigation, - .button-group { - display: flex; - justify-content: center; - margin-bottom: 20px; - } - - /* Styling for the tab buttons */ - .tab-navigation button { - background-color: #007bff; /* Primary color */ - color: white; - border: none; - padding: 10px 20px; - margin: 0 5px; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s; - } - - .tab-navigation button.active { - background-color: #0056b3; /* Darker shade for active tab */ - } - - .tab-navigation button:hover { - background-color: #0056b3; - } - - /* Styling for the action buttons (Add, Update, Remove) */ - .button-group .btn-add { - background-color: #28a745; /* Green for add */ - color: white; - border: none; - padding: 10px 15px; - margin: 0 5px; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s; - } - - .button-group .btn-update { - background-color: #ffc107; /* Yellow for update */ - color: black; - border: none; - padding: 10px 15px; - margin: 0 5px; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s; - } - - .button-group .btn-remove { - background-color: #dc3545; /* Red for remove */ - color: white; - border: none; - padding: 10px 15px; - margin: 0 5px; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s; - } - - .button-group .btn-add:hover { - background-color: #218838; - } - - .button-group .btn-update:hover { - background-color: #e0a800; - } - - .button-group .btn-remove:hover { - background-color: #c82333; - } - - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\(auth)\admin\employees\Modal.tsx - -import React, { useState, useEffect } from 'react'; - - -interface Employee { - employee_id: string; - name: string; - salary: number; - position: string; - } - -interface ModalProps { - onClose: () => void; - onSave: (data: Partial) => void; - onConfirmRemove: () => void; - initialData?: Partial; - action: 'add' | 'update' | 'remove' | null; - } - - const Modal: React.FC = ({ - onClose, - onSave, - onConfirmRemove, - initialData, - action, - }) => { - const [formData, setFormData] = useState>({ - employee_id: '', - name: '', - salary: 0, - position: '', - ...initialData, // Populate form data if updating - }); - - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setFormData((prevData) => ({ - ...prevData, - [name]: value, - })); - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - const filteredData = { ...formData }; - - onSave(filteredData); - }; - - - const handleRemove = () => { - onConfirmRemove(); - }; - - useEffect(() => { - if (action === 'update' && initialData) { - setFormData({ ...initialData }); - } - }, [action, initialData]); - - const renderForm = () => { - if (action === 'add' || action === 'update') { - return ( -
-
- - -
-
- - -
- -
- - -
- - -
- - -
- - -
- -
-
- ); - - } - else if (action === 'remove' && initialData) { - return ( -
-

Are you sure you want to remove {initialData.name}?

-
- - -
-
- ); - } - - return null; - }; - - return ( -
-
- - {renderForm()} -
-
- ); - }; - - export default Modal; - - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\(auth)\admin\employees\page.tsx - -// app/update-inventory/page.tsx -'use client'; - -import Modal from './Modal'; // Assuming you have a Modal component - -import { useEffect, useState } from 'react'; -import './employee.css'; // Make sure to import the necessary CSS - - - -interface Employee { - employee_id: string; - name: string; - salary: number; - position: string; - } - -export default function UpdateEmployeesPage() { - const [Employees, setEmployees] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [modalAction, setModalAction] = useState<'add' | 'update' | 'remove' | null>(null); - const [selectedItem, setSelectedItem] = useState(null); - const [modalVisible, setModalVisible] = useState(false); // Track modal visibility - const [activeTab, setActiveTab] = useState('entree_side'); // Tracks the selected category tab - - - - // Function to fetch the menu items - const fetchEmployees = async () => { - setLoading(true); - try { - const response = await fetch('http://localhost:4000/api/employees'); - if (!response.ok) { - throw new Error('Failed to fetch employees'); - } - const data: Employee[] = await response.json(); - setEmployees(data); - } catch (error) { - setError((error as Error).message); - } finally { - setLoading(false); - } - }; - -// useEffect to load items on component mount -useEffect(() => { - fetchEmployees(); -}, []); - - // Handle add, update, and remove actions - const handleAddItem = async () => { - setModalAction('add'); - setModalVisible(true); - }; - - // Modify handleUpdateItem to show the modal for updating - const handleUpdateItem = () => { - if (!selectedItem) { - alert('Please select an item to update'); - return; - } - - setModalAction('update'); // Action for updating - setModalVisible(true); // Open the modal with selected item data - }; - - // Modify handleRemoveItem to show the modal for removing - const handleRemoveItem = () => { - if (!selectedItem) { - alert('Please select an item to remove'); - return; - } - - setModalAction('remove'); // Action for removing - setModalVisible(true); // Open the modal for confirmation - }; - - const handleSelectItem = (item: Employee) => { - if (selectedItem && selectedItem.name === item.name) { - // If the clicked item is already selected, deselect it - setSelectedItem(null); - } else { - // Otherwise, select the clicked item - setSelectedItem(item); - } - }; - - - // Modal component for adding, updating, or removing items - const handleSaveItem = async (data: Partial) => { - let response; - //try this delete later if no work - // Check and format price if it's a number - // if (data.price !== undefined && typeof data.price === 'number') { - // console.log('Formatting price:', data.price); // Log the original price - // data.price = parseFloat(data.price.toFixed(2)); // Format to 2 decimal places - // } else { - // console.log('Price is not a number or is undefined:', data.price); // Log if not a number - // } - if (modalAction === 'add') { - response = await fetch('http://localhost:4000/api/employees', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }); - } else if (modalAction === 'update' && selectedItem) { - response = await fetch('http://localhost:4000/api/employees', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ...selectedItem, ...data }) - }); - console.log(JSON.stringify(data)); - } else if (modalAction === 'remove' && selectedItem) { - response = await fetch(`http://localhost:4000/api/employees`, { - method: 'DELETE', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ...selectedItem, ...data }) - }); - } - - if (response?.ok) { - // Refetch data from backend after the action - await fetchEmployees(); - setModalVisible(false); // Close modal after saving - setSelectedItem(null); // Deselect item - } else { - console.error(`Failed to ${modalAction} item`); - } - }; - - - - - - return ( -
-

Manage Employees

- - {loading &&

Loading employees...

} - {error &&

{error}

} - - - - - {/* Action Buttons */} -
- - - -
- - {/* Table displaying menu items for the active category */} - - - - - - - - - - - {Employees - .map((item) => ( - handleSelectItem(item)} - > - - - - - - ))} - -
IdNameSalaryPosition
{item.employee_id}{item.name}{item.salary}{item.position}
- - {/* Modal */} - {modalVisible && ( - <> -
setModalVisible(false)} /> - setModalVisible(false)} - onSave={handleSaveItem} - onConfirmRemove={() => handleSaveItem(selectedItem!)} // for removing item - initialData={(modalAction === 'update' || modalAction === 'remove') && selectedItem ? selectedItem : undefined} - action={modalAction} - /> - - )} -
- ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\(auth)\admin\reports\page.tsx - -// src/app/(auth)/admin/reports/page.tsx -'use client'; - -import { useState } from 'react'; -import { Calendar, BarChart3, DollarSign, ClipboardList } from 'lucide-react'; -import { format } from 'date-fns'; -import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; -import { useRouter } from 'next/navigation'; - -interface SalesData { - itemized_sales: Array<{ - name: string; - type: string; - quantity_sold: number; - sales: number; - }>; - summary: { - total_sales: number; - total_orders: number; - average_order_value: number; - }; - daily_sales?: Array<{ - date: string; - num_orders: number; - total: number; - }>; -} - -interface ProductUsageData { - entrees: Array<{ name: string; usage: number; }>; - sides: Array<{ name: string; usage: number; }>; - drinks: Array<{ name: string; usage: number; }>; - appetizers: Array<{ name: string; usage: number; }>; -} - -interface XReportData { - hourly_sales: Array<{ - hour: number; - total_sales: number; - num_orders: number; - }>; - summary: { - total_sales: number; - total_orders: number; - average_order_value: number; - }; -} - -interface ZReportData { - summary: { - total_sales: number; - total_orders: number; - }; - items_sold: Array<{ - name: string; - quantity: number; - }>; -} - -type ReportData = SalesData | ProductUsageData | XReportData | ZReportData | null; - -const SalesReportsPage = () => { - const [activeTab, setActiveTab] = useState('product-usage'); - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); - const [reportData, setReportData] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const router = useRouter(); - - const renderProductUsageChart = (data: ProductUsageData) => { - if (!data) return null; - const categories = ['entrees', 'sides', 'drinks', 'appetizers'] as const; - - return ( -
- {categories.map(category => ( -
-

{category} Usage

- {/* Increased height to accommodate labels */} -
- - - - - - - - - - -
-
- ))} -
- ); -}; - - const renderXReport = (data: XReportData) => { - if (!data?.hourly_sales) return null; - - return ( -
- {/* Summary Cards */} -
-
-

Total Sales

-

- ${data.summary.total_sales.toFixed(2)} -

-
-
-

Orders

-

- {data.summary.total_orders} -

-
-
-

Average Order

-

- ${data.summary.average_order_value.toFixed(2)} -

-
-
- - {/* Hourly Chart */} -
-

Hourly Sales

-
- - - - `${hour}:00`} - /> - - - { - if (name === "total_sales") return `$${Number(value).toFixed(2)}`; - return value; - }} - /> - - - - - -
-
-
- ); - }; - - const renderZReport = (data: ZReportData) => { - if (!data?.summary) return null; - - return ( -
-
-
-

Total Sales

-

- ${data.summary.total_sales.toFixed(2)} -

-
-
-

Total Orders

-

- {data.summary.total_orders} -

-
-
- -
-

Items Sold

-
- - - - - - - - - {data.items_sold.map((item, index) => ( - - - - - ))} - -
- Item Name - - Quantity Sold -
{item.name}{item.quantity}
-
-
-
- ); - }; - - const renderSalesReport = (data: SalesData) => { - if (!data?.itemized_sales) return null; - - return ( -
- {/* Summary Cards */} -
-
-

Total Sales

-

- ${data.summary.total_sales.toFixed(2)} -

-
-
-

Total Orders

-

- {data.summary.total_orders} -

-
-
-

Average Order

-

- ${data.summary.average_order_value.toFixed(2)} -

-
-
- - {/* Sales Table */} -
-
- - - - - - - - - - - {data.itemized_sales.map((item, index) => ( - - - - - - - ))} - -
- Item - - Category - - Quantity Sold - - Total Sales -
{item.name}{item.type}{item.quantity_sold}${item.sales.toFixed(2)}
-
-
- - {/* Daily Sales Chart */} - {data.daily_sales && data.daily_sales.length > 0 && ( -
-

Daily Sales Trend

-
- - - - format(new Date(date), 'MMM d')} - /> - - - { - if (name === "total") return `$${Number(value).toFixed(2)}`; - return value; - }} - labelFormatter={(date) => format(new Date(date), 'MMM d, yyyy')} - /> - - - - - -
-
- )} -
- ); - }; - - const handleGenerateReport = async () => { - setIsLoading(true); - try { - let response; - switch (activeTab) { - case 'product-usage': - response = await fetch('/api/reports/product-usage', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ startDate, endDate }), - }); - break; - case 'x-report': - response = await fetch('/api/reports/x-report'); - break; - case 'z-report': - response = await fetch('/api/reports/z-report'); - break; - case 'sales-report': - response = await fetch('/api/reports/sales', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ startDate, endDate }), - }); - break; - } - - if (response?.ok) { - const data = await response.json(); - setReportData(data); - } else { - console.error('Failed to fetch report data'); - } - } catch (error) { - console.error('Error generating report:', error); - } finally { - setIsLoading(false); - } - }; - - const renderContent = () => { - if (isLoading) { - return ( -
-
-
- ); - } - - const needsDateRange = activeTab === 'product-usage' || activeTab === 'sales-report'; - - return ( -
- {needsDateRange && ( -
-
- - setStartDate(e.target.value)} - /> -
-
- - setEndDate(e.target.value)} - /> -
-
- )} - - - - {reportData && ( -
- {activeTab === 'product-usage' && renderProductUsageChart(reportData as ProductUsageData)} - {activeTab === 'x-report' && renderXReport(reportData as XReportData)} - {activeTab === 'z-report' && renderZReport(reportData as ZReportData)} - {activeTab === 'sales-report' && renderSalesReport(reportData as SalesData)} -
- )} -
- ); - }; - - return ( - -
- {/* Add this button */} - -
-

Sales Reports

-

Generate and view various sales reports

-
- -
- - - - - - - -
- -
- {renderContent()} -
-
- ); -}; - -export default SalesReportsPage; - - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\(auth)\admin\styles\globals.css - -/* styles/globals.css or your preferred global CSS file */ - -/* General layout */ -.container { - max-width: 1200px; - margin: 0 auto; - padding: 2rem; - } - - /* Title */ - .title { - font-size: 2rem; - font-weight: 700; - margin-bottom: 1.5rem; - } - - /* Table Styles */ - .table { - width: 100%; - border-collapse: collapse; - margin-top: 2rem; - } - - .table th, - .table td { - padding: 0.75rem; - border: 1px solid #ddd; - text-align: left; - } - - /* Header Styles */ - .table th { - background-color: #f4f4f4; - font-size: 1.1rem; - font-weight: 600; - } - - /* Row Styles */ - .table td { - font-size: 1rem; - color: #333; - } - - .table tr:nth-child(even) { - background-color: #f9f9f9; - } - - /* Loading and Error Messages */ - .loading { - font-size: 1.2rem; - color: #666; - } - - .error { - font-size: 1.2rem; - color: red; - } - - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\(auth)\admin\update-inventory\Modal.tsx - -import React, { useState, useEffect } from 'react'; - - -interface MenuItem { - id: number; - name: string; - category: string; - price: number; - count: number; - type: boolean | null; - } - -interface ModalProps { - onClose: () => void; - onSave: (data: Partial) => void; - onConfirmRemove: () => void; - initialData?: Partial; - action: 'add' | 'update' | 'remove' | null; - } - - const Modal: React.FC = ({ - onClose, - onSave, - onConfirmRemove, - initialData, - action, - }) => { - const [formData, setFormData] = useState>({ - name: '', - category: '', - price: 0, - count: 0, - type: false, - ...initialData, // Populate form data if updating - }); - - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setFormData((prevData) => ({ - ...prevData, - [name]: value, - })); - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - const filteredData = { ...formData }; - if (filteredData.category === 'free_items' || filteredData.category === 'raw_items') { - delete filteredData.price; - delete filteredData.type; - } - if (filteredData.category === 'drink_table' || filteredData.category === 'appetizers'){ - delete filteredData.type; - } - - onSave(filteredData); - }; - - - const handleRemove = () => { - onConfirmRemove(); - }; - - useEffect(() => { - if (action === 'update' && initialData) { - setFormData({ ...initialData }); - } - }, [action, initialData]); - - const renderForm = () => { - if (action === 'add' || action === 'update') { - return ( -
-
- - -
-
- - -
- {/* Show Price input only if category is not "free_items" or "raw_items" */} - {(formData.category !== 'free_items' && formData.category !== 'raw_items') && ( -
- - -
- )} - -
- - -
- - {/* Show Type checkbox only if category is "entree_side" */} - {formData.category === 'entree_side' && ( -
- - - setFormData((prevData) => ({ - ...prevData, - type: e.target.checked, - })) - } - /> -
- )} -
- -
-
- ); - - } - else if (action === 'remove' && initialData) { - return ( -
-

Are you sure you want to remove {initialData.name}?

-
- - -
-
- ); - } - - return null; - }; - - return ( -
-
- - {renderForm()} -
-
- ); - }; - - export default Modal; - - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\(auth)\admin\update-inventory\page.tsx - -// app/update-inventory/page.tsx -'use client'; - -import Modal from './Modal'; // Assuming you have a Modal component - -import { useEffect, useState } from 'react'; -import './update-inventory.css'; // Make sure to import the necessary CSS - -// Define the MenuItem type (adjust based on your actual type structure) -interface MenuItem { - name: string; - category: string; - price: number; - count: number; - type: boolean | null; -} - -export default function UpdateInventoryPage() { - const [menuItems, setMenuItems] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [modalAction, setModalAction] = useState<'add' | 'update' | 'remove' | null>(null); - const [selectedItem, setSelectedItem] = useState(null); - const [modalVisible, setModalVisible] = useState(false); // Track modal visibility - const [activeTab, setActiveTab] = useState('entree_side'); // Tracks the selected category tab - - const categories = ['entree_side', 'free_items', 'drink_table', 'appetizers', 'raw_items']; - - // Function to fetch the menu items - const fetchMenuItems = async () => { - setLoading(true); - try { - const response = await fetch('http://localhost:4000/api/menu-items'); - if (!response.ok) { - throw new Error('Failed to fetch menu items'); - } - const data: MenuItem[] = await response.json(); - setMenuItems(data); - } catch (error) { - setError((error as Error).message); - } finally { - setLoading(false); - } - }; - -// useEffect to load items on component mount -useEffect(() => { - fetchMenuItems(); -}, []); - - // Handle add, update, and remove actions - const handleAddItem = async () => { - setModalAction('add'); - setModalVisible(true); - }; - - // Modify handleUpdateItem to show the modal for updating - const handleUpdateItem = () => { - if (!selectedItem) { - alert('Please select an item to update'); - return; - } - - setModalAction('update'); // Action for updating - setModalVisible(true); // Open the modal with selected item data - }; - - // Modify handleRemoveItem to show the modal for removing - const handleRemoveItem = () => { - if (!selectedItem) { - alert('Please select an item to remove'); - return; - } - - setModalAction('remove'); // Action for removing - setModalVisible(true); // Open the modal for confirmation - }; - - const handleSelectItem = (item: MenuItem) => { - if (selectedItem && selectedItem.name === item.name) { - // If the clicked item is already selected, deselect it - setSelectedItem(null); - } else { - // Otherwise, select the clicked item - setSelectedItem(item); - } - }; - - - // Modal component for adding, updating, or removing items - const handleSaveItem = async (data: Partial) => { - let response; - //try this delete later if no work - // Check and format price if it's a number - // if (data.price !== undefined && typeof data.price === 'number') { - // console.log('Formatting price:', data.price); // Log the original price - // data.price = parseFloat(data.price.toFixed(2)); // Format to 2 decimal places - // } else { - // console.log('Price is not a number or is undefined:', data.price); // Log if not a number - // } - if (modalAction === 'add') { - response = await fetch('http://localhost:4000/api/menu-items', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }); - } else if (modalAction === 'update' && selectedItem) { - response = await fetch('http://localhost:4000/api/menu-items', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ...selectedItem, ...data }) - }); - console.log(JSON.stringify(data)); - } else if (modalAction === 'remove' && selectedItem) { - response = await fetch(`http://localhost:4000/api/menu-items`, { - method: 'DELETE', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ ...selectedItem, ...data }) - }); - } - - if (response?.ok) { - // Refetch data from backend after the action - await fetchMenuItems(); - setModalVisible(false); // Close modal after saving - setSelectedItem(null); // Deselect item - } else { - console.error(`Failed to ${modalAction} item`); - } - }; - - const handleConfirmRemove = async () => { - if (!selectedItem) return; - - // Send DELETE request to backend to remove the item by name and category - const response = await fetch('http://localhost:4000/api/menu-items', { - method: 'DELETE', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: selectedItem.name, category: selectedItem.category }), // Send name and category for identification - }); - - if (response.ok) { - // Remove item locally and refresh list - setMenuItems((prevItems) => prevItems.filter((item) => item.name !== selectedItem.name)); - } else { - console.error('Failed to remove item'); - } - - setModalVisible(false); // Close modal after removing - setSelectedItem(null); // Clear selection - }; - - const handleTabClick = (category: string) => { - setActiveTab(category); - setSelectedItem(null); // Clear selection when switching tabs - }; - - return ( -
-

Update Inventory

- - {loading &&

Loading menu items...

} - {error &&

{error}

} - - {/* Tab Navigation */} -
- {categories.map((category) => ( - - ))} -
- - {/* Action Buttons */} -
- - - -
- - {/* Table displaying menu items for the active category */} - - - - - - - - - - - - {menuItems - .filter((item) => item.category === activeTab) - .map((item) => ( - handleSelectItem(item)} - > - - - - - - - ))} - -
NameCategoryPriceInventoryEntree?
{item.name}{item.category}{item.price}{item.count}{item.type === true ? 't' : item.type === false ? 'f' : ''}
- - {/* Modal */} - {modalVisible && ( - <> -
setModalVisible(false)} /> - setModalVisible(false)} - onSave={handleSaveItem} - onConfirmRemove={() => handleSaveItem(selectedItem!)} // for removing item - initialData={(modalAction === 'update' || modalAction === 'remove') && selectedItem ? selectedItem : undefined} - action={modalAction} - /> - - )} -
- ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\(auth)\admin\update-inventory\update-inventory.css - -/* Update Inventory Page Styles */ - - -/* Buttons Above Table */ -.button-group { - display: flex; - gap: 20px; - margin-bottom: 20px; - justify-content: center; -} - -button { - padding: 10px 20px; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 16px; - transition: background-color 0.3s ease; -} - -button:hover { - opacity: 0.8; -} - -.btn-add { - background-color: #4CAF50; - color: white; -} - -.btn-add:hover { - background-color: #45a049; -} - -.btn-update { - background-color: #ff9800; - color: white; -} - -.btn-update:hover { - background-color: #f57c00; -} - -.btn-remove { - background-color: #f44336; - color: white; -} - -.btn-remove:hover { - background-color: #d32f2f; -} - -/* Table Styles */ -.menu-table { - width: 100%; - border-collapse: collapse; - margin-top: 20px; - margin-bottom: 40px; -} - -.menu-table th, -.menu-table td { - padding: 12px 15px; - text-align: center; - border: 1px solid #ddd; -} - -.menu-table th { - background-color: #4CAF50; - color: white; -} - -.menu-table tbody tr:nth-child(even) { - background-color: #f9f9f9; -} - -.menu-table tbody tr:hover { - background-color: #f1f1f1; - cursor: pointer; -} - -/* Highlight the selected row */ -.menu-table tbody tr.selected { - background-color: #333; - color: white; -} -/* Add this to your CSS file */ -.selected { - background-color: #4caf50; /* Highlight color for selected row */ - color: white; /* Text color for selected row */ -} - -/* Modal Styles */ -.modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - z-index: 1050; /* Make sure modal stays on top */ -} - -.modal-container { - background-color: white; - padding: 30px; - border-radius: 10px; - width: 400px; - box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); -} - -.modal h2 { - margin-bottom: 20px; -} - -.modal .form-group { - margin-bottom: 15px; -} - -.modal label { - display: block; - margin-bottom: 5px; - font-weight: bold; -} - -.modal input[type="text"], -.modal input[type="number"], -.modal input[type="checkbox"] { - width: 100%; - padding: 10px; - font-size: 14px; - border: 1px solid #ddd; - border-radius: 5px; - margin-top: 5px; -} - -.modal input[type="checkbox"] { - width: auto; -} - -.modal-actions { - display: flex; - gap: 20px; - justify-content: center; -} - -.modal-actions button { - padding: 10px 20px; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 16px; -} - -.modal-actions button:hover { - opacity: 0.8; -} - -.modal-actions button:first-child { - background-color: #4CAF50; - color: white; -} - -.modal-actions button:last-child { - background-color: #f44336; - color: white; -} - -/* Overlay to grey out background */ -.overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); /* Grey overlay */ - z-index: 1040; /* Ensure overlay is beneath the modal */ -} -/* Modal Overlay */ -.modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - justify-content: center; - align-items: center; - z-index: 9999; -} - -/* Modal */ -.modal { - background: #fff; - padding: 20px; - border-radius: 8px; - width: 400px; - max-width: 100%; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); -} - -/* Close Button */ -.close-btn { - position: absolute; - top: 10px; - right: 10px; - background: none; - border: none; - font-size: 24px; - cursor: pointer; -} - -/* Form Fields */ -input { - width: 100%; - padding: 8px; - margin: 8px 0; - border: 1px solid #ddd; - border-radius: 4px; -} - -button:hover { - background-color: #0056b3; -} - -button:disabled { - background-color: #ccc; - cursor: not-allowed; -} - -form { - display: flex; - flex-direction: column; -} - -/* Centering the tab buttons and action buttons */ -.tab-navigation, -.button-group { - display: flex; - justify-content: center; - margin-bottom: 20px; -} - -/* Styling for the tab buttons */ -.tab-navigation button { - background-color: #007bff; /* Primary color */ - color: white; - border: none; - padding: 10px 20px; - margin: 0 5px; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s; -} - -.tab-navigation button.active { - background-color: #0056b3; /* Darker shade for active tab */ -} - -.tab-navigation button:hover { - background-color: #0056b3; -} - -/* Styling for the action buttons (Add, Update, Remove) */ -.button-group .btn-add { - background-color: #28a745; /* Green for add */ - color: white; - border: none; - padding: 10px 15px; - margin: 0 5px; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s; -} - -.button-group .btn-update { - background-color: #ffc107; /* Yellow for update */ - color: black; - border: none; - padding: 10px 15px; - margin: 0 5px; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s; -} - -.button-group .btn-remove { - background-color: #dc3545; /* Red for remove */ - color: white; - border: none; - padding: 10px 15px; - margin: 0 5px; - border-radius: 5px; - cursor: pointer; - transition: background-color 0.3s; -} - -.button-group .btn-add:hover { - background-color: #218838; -} - -.button-group .btn-update:hover { - background-color: #e0a800; -} - -.button-group .btn-remove:hover { - background-color: #c82333; -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\(auth)\admin\page.tsx - -'use client'; - -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; - - -export default function ManagerPage() { - const router = useRouter(); - const [message, setMessage] = useState(null); - - const handleButtonClick = (action: string) => { - if (action === 'Update Menu Items and Inventory') { - router.push('/admin/update-inventory'); // navigate to the new page - } - else if (action === 'View Sales Reports') { - router.push('/admin/reports'); // navigate to the new page - } - else if (action === 'Manage Staff'){ - router.push('/admin/employees'); - } - else { - setMessage(`You clicked "${action}"!`); - setTimeout(() => setMessage(null), 3000); - } - }; - - - return ( -
-

Manager Dashboard

- -
- - - - - - - - - - - {/* */} -
- - {message && ( -
- {message} -
- )} -
- ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\(auth)\cashier\page.tsx - -// client/src/app/(auth)/cashier/page.tsx -'use client'; - -import { useState, useEffect } from 'react'; -import OrderList from '@/components/cashier/OrderList'; -import EnhancedCheckout from '@/components/cashier/Checkout'; -import Checkout from '@/components/cashier/Checkout'; -import { MenuItem, Order } from '@/types'; -import { fetchMenuItems } from '@/utils/menuItems' -import api from "@/lib/api"; - -export default function CashierPage() { - const [orders, setOrders] = useState([]); - const [menuItems, setMenuItems] = useState([]); - const [activeOrder, setActiveOrder] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - // Temporary data for testing - const dummyData: MenuItem[] = [ - { - id: '41', - name: 'Bowl', - description: '1 Side & 1 Entree', - price: 8.30, - category: 'combo', - imageUrl: '/images/combos/bowl.png', - available: true, - }, - { - id: '42', - name: 'Plate', - description: '1 Side & 2 Entrees', - price: 9.80, - category: 'combo', - imageUrl: '/images/combos/plate.png', - available: true, - }, - { - id: '43', - name: 'Bigger Plate', - description: '1 Side & 3 Entrees', - price: 11.30, - category: 'combo', - imageUrl: '/images/combos/biggerPlate.png', - available: true, - }, - { - id: '1', - name: 'Orange Chicken', - description: 'Crispy chicken wok-tossed in a sweet and spicy orange sauce', - price: 5.00, - category: 'entree', - imageUrl: '/images/entrees/the_original_orange_chicken.png', - available: true, - }, - { - id: '2', - name: 'Beijing Beef', - description: 'Crispy beef wok-tossed with bell peppers and onions', - price: 5.00, - category: 'entree', - imageUrl: '/images/entrees/beijing_beef.png', - available: true, - }, - { - id: '3', - name: 'Black Pepper Chicken', - description: 'Marinated chicken, celery, and onions in a bold black pepper sauce', - price: 5.00, - category: 'entree', - imageUrl: '/images/entrees/black_pepper_chicken.png', - available: true, - }, - { - id: '4', - name: 'Black Pepper Sirloin Steak', - description: 'Sirloin steak wok-seared with baby broccoli, onions, red bell peppers, and mushrooms in a savory black pepper sauce', - price: 6.00, - category: 'entree', - imageUrl: '/images/entrees/black_pepper_sirloin_steak.png', - available: true, - }, - { - id: '5', - name: 'Broccoli Beef', - description: 'Tender beef and fresh broccoli in a ginger soy sauce', - price: 5.00, - category: 'entree', - imageUrl: '/images/entrees/broccoli_beef.png', - available: true, - }, - { - id: '6', - name: 'Beijing Beef', - description: 'Crispy beef wok-tossed with bell peppers and onions', - price: 5.00, - category: 'entree', - imageUrl: '/images/entrees/beijing_beef.png', - available: true, - }, - { - id: '7', - name: 'Grilled Teriyaki Chicken', - description: 'Grilled Chicken hand-sliced to order and served with teriyaki sauce', - price: 5.00, - category: 'entree', - imageUrl: '/images/entrees/grilled_teriyaki_chicken.png', - available: true, - }, - { - id: '8', - name: 'Honey Sesame Chicken Breast', - description: 'Crispy strips of white-meat chicken with veggies ina mildly sweet sauce with organic honey', - price: 5.00, - category: 'entree', - imageUrl: '/images/entrees/honey_sesame_chicken_breast.png', - available: true, - }, - { - id: '9', - name: 'Honey Walnut Shrimp', - description: 'Large tempura-battered shrimp, work-tossed in a honey sauce and topped with glazed walnuts', - price: 6.00, - category: 'entree', - imageUrl: '/images/entrees/honey_walnut_shrimp.png', - available: true, - }, - { - id: '10', - name: 'Hot Ones Blazing Bourbon Chicken', - description: 'Crispy boneless chicken bites and veggies wok-tossed in an extra spicy and sweet bourbon sauce', - price: 5.00, - category: 'entree', - imageUrl: '/images/entrees/hot_ones_blazing_bourbon_chicken.png', - available: true, - }, - { - id: '11', - name: 'Kung Pao Chicken', - description: 'A Sichuan-inspired dish with chicken, peanuts and vegetables, finished with chili pepppers', - price: 5.00, - category: 'entree', - imageUrl: '/images/entrees/kung_pao_chicken.png', - available: true, - }, - { - id: '12', - name: 'Mushroom Chicken', - description: 'A delicate combination of chicken, mushrooms and zucchini wok-tossed with a light ginger soy sauce', - price: 5.00, - category: 'entree', - imageUrl: '/images/entrees/mushroom_chicken.png', - available: true, - }, - { - id: '13', - name: 'String Bean Chicken Breast', - description: 'Chicken breast, string beans and onions wok-tossed in a mild ginger soy sauce', - price: 5.00, - category: 'entree', - imageUrl: '/images/entrees/string_bean_chicken_breast.png', - available: true, - }, - { - id: '14', - name: 'SweetFire Chicken Breast', - description: 'Crispy, white-meat chicken, red bell peppers, onions and pineapples in a bright and sweet chili sauce', - price: 5.00, - category: 'entree', - imageUrl: '/images/entrees/sweetfire_chicken_breast.png', - available: true, - }, - { - id: '15', - name: 'Chow Mein', - description: 'Stir-fried wheat noodles with onions and celery', - price: 5.00, - category: 'side', - imageUrl: '/images/sides/chow_mein.png', - available: true, - }, - { - id: '16', - name: 'Fried Rice', - description: 'Prepared steamed white rice with soy sauce, eggs, peas, carrots and green onions', - price: 5.00, - category: 'side', - imageUrl: '/images/sides/fried_rice.png', - available: true, - }, - { - id: '17', - name: 'Super Greens', - description: 'A healthful medley of broccoli, kale, and cabbage', - price: 5.00, - category: 'side', - imageUrl: '/images/sides/super_greens.png', - available: true, - }, - { - id: '18', - name: 'White Steamed Rice', - description: 'White rice', - price: 5.00, - category: 'side', - imageUrl: '/images/sides/white_steamed_rice.png', - available: true, - }, - { - id: '19', - name: 'Apple Pie Roll', - description: 'Juicy apples and fall spices in a crispy rolled pastry, finished with cinnamon sugar', - price: 2.80, - category: 'appetizer', - imageUrl: '/images/appetizers/apple_pie_roll.png', - available: true, - }, - { - id: '20', - name: 'Chicken Egg Roll', - description: 'Cabbage, carrots, green onions and chicken in a crispy wonton wrapper', - price: 2.5, - category: 'appetizer', - imageUrl: '/images/appetizers/chicken_egg_roll.png', - available: true, - }, - { - id: '21', - name: 'Cream Cheese Rangoon', - description: 'Wonton wrappers filled with cream cheese and served with sweet and sour sauce', - price: 3.00, - category: 'appetizer', - imageUrl: '/images/appetizers/cream_cheese_rangoon.png', - available: true, - }, - { - id: '22', - name: 'Veggie Sprint Roll', - description: 'Cabbage, celery, carrots, green onions and Chinese noodles in a crispy wonton wrapper', - price: 2.00, - category: 'appetizer', - imageUrl: '/images/appetizers/veggie_spring_roll.png', - available: true, - }, - { - id: '23', - name: 'Barqs Root Beer', - description: '', - price: 1.50, - category: 'drink', - imageUrl: '/images/drinks/barqs_root_beer.png', - available: true, - }, - { - id: '24', - name: 'Coca Cola', - description: '', - price: 1.50, - category: 'drink', - imageUrl: '/images/drinks/coca_cola.png', - available: true, - }, - { - id: '25', - name: 'Coca Cola Cherry', - description: '', - price: 1.50, - category: 'drink', - imageUrl: '/images/drinks/coke_mexico.png', - available: true, - }, - { - id: '26', - name: 'Coke Zero', - description: '', - price: 1.50, - category: 'drink', - imageUrl: '/images/drinks/coke_zero.png', - available: true, - }, - { - id: '27', - name: 'Dasani', - description: '', - price: 1.00, - category: 'drink', - imageUrl: '/images/drinks/dasani.png', - available: true, - }, - { - id: '28', - name: 'Diet Coke', - description: '', - price: 1.98, - category: 'drink', - imageUrl: '/images/drinks/diet_coke.png', - available: true, - }, - { - id: '29', - name: 'Dr Pepper', - description: '', - price: 1.99, - category: 'drink', - imageUrl: '/images/drinks/dr_pepper.png', - available: true, - }, - { - id: '30', - name: 'Fanta', - description: '', - price: 1.50, - category: 'drink', - imageUrl: '/images/drinks/fanta_orange.png', - available: true, - }, - { - id: '31', - name: 'Fuze Raspberry Iced Tea', - description: '', - price: 1.50, - category: 'drink', - imageUrl: '/images/drinks/fize_raspberry_iced_tea.png', - available: true, - }, - { - id: '32', - name: 'Minute Maid Apple Juice', - description: '', - price: 1.50, - category: 'drink', - imageUrl: '/images/drinks/minute_maid_apple_juice.png', - available: true, - }, - { - id: '33', - name: 'Minute Maid Lemonade', - description: '', - price: 1.50, - category: 'drink', - imageUrl: '/images/drinks/minute_maid_lemonade.png', - available: true, - }, - { - id: '34', - name: 'Mango Tea', - description: '', - price: 2.00, - category: 'drink', - imageUrl: '/images/drinks/passion_mango_black_tea.png', - available: true, - }, - { - id: '35', - name: 'Peach Lychee Refresher', - description: '', - price: 2.00, - category: 'drink', - imageUrl: '/images/drinks/peach_lychee_flavored_refresher.png', - available: true, - }, - { - id: '36', - name: 'Pomegranate Pineapple Flavored Lemonade', - description: '', - price: 2.00, - category: 'drink', - imageUrl: '/images/drinks/pomegranite_pineapple_flavored_lemonade.png', - available: true, - }, - { - id: '37', - name: 'Smartwater', - description: '', - price: 2.00, - category: 'drink', - imageUrl: '/images/drinks/smartwater.png', - available: true, - }, - { - id: '38', - name: 'Sprite', - description: '', - price: 2.00, - category: 'drink', - imageUrl: '/images/drinks/sprite.png', - available: true, - }, - { - id: '39', - name: 'Watermelon Mange Flavored Refresher', - description: '', - price: 2.00, - category: 'drink', - imageUrl: '/images/drinks/watermelon_mango_flavored_refresher.png', - available: true, - }, - { - id: '40', - name: 'Sprite', - description: '', - price: 2.00, - category: 'drink', - imageUrl: '/images/drinks/sprite.png', - available: true, - }, - - ]; - - useEffect(() => { - const fetchInitialData = async () => { - try { - /*const [ordersRes, menuRes] = await Promise.all([ - api.get('/orders/active'), - api.get('/menu') - ]); - - setOrders(ordersRes.data); - setMenuItems(menuRes.data); */ - //setMenuItems(dummyData); - const items = await fetchMenuItems(); - setMenuItems(items); - } catch (error) { - console.error('Failed to fetch initial data:', error); - } finally { - setIsLoading(false); - } - }; - - fetchInitialData(); - }, []); - - const handleCreateOrder = async (order: Partial) => { - try { - const response = await api.post('/orders', order); - setOrders(prev => [...prev, response.data]); - setActiveOrder(response.data); - } catch (error) { - console.error('Failed to create order:', error); - } - }; - - const handleUpdateOrderStatus = async (orderId: string, status: Order['status']) => { - try { - await api.put(`/orders/${orderId}/status`, { status }); - setOrders(prev => - prev.map(order => - order.id === orderId ? { ...order, status } : order - ) - ); - } catch (error) { - console.error('Failed to update order status:', error); - } - }; - - if (isLoading) { - return ( -
-
-
- ); - } - - return ( -
- -
- ); - -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\(auth)\login\page.tsx - -// client/src/app/(auth)/login/page.tsx -'use client'; - -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; -import Image from 'next/image'; -import { api } from '@/lib/api'; - -interface LoginForm { - username: string; - password: string; -} - -export default function LoginPage() { - const router = useRouter(); - const [formData, setFormData] = useState({ - username: '', - password: '', - }); - const [error, setError] = useState(''); - const [isLoading, setIsLoading] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - setError(''); - - try { - const response = await api.post('/auth/login', formData); - localStorage.setItem('token', response.data.token); - - // Redirect based on role - if (response.data.role === 'admin') { - router.push('/admin'); - } else { - router.push('/cashier'); - } - } catch (err) { - setError('Invalid credentials. Please try again.'); - } finally { - setIsLoading(false); - } - }; - - return ( -
-
-
- Panda Express Logo -
-

- Sign in to your account -

-
- -
-
-
- {error && ( -
-

{error}

-
- )} - -
- - setFormData({ ...formData, username: e.target.value })} - /> -
- -
- - setFormData({ ...formData, password: e.target.value })} - /> -
- -
- -
-
-
-
-
- ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\api\customer-orders\route.ts - -// src/app/api/customer-orders/route.ts -import { NextResponse } from 'next/server'; -import { Pool, PoolConfig } from 'pg'; -import { OrderItem } from '@/types'; - -const dbConfig: PoolConfig = { - user: process.env.POSTGRES_USER || 'team_6b', - host: process.env.POSTGRES_HOST || 'csce-315-db.engr.tamu.edu', - database: process.env.POSTGRES_DB || 'team_6b_db', - password: process.env.POSTGRES_PASSWORD || 'kartana', - port: parseInt(process.env.POSTGRES_PORT || '5432'), - ssl: { - rejectUnauthorized: false - } -}; - -const pool = new Pool(dbConfig); - -interface CustomerOrderRequest { - items: OrderItem[]; - subtotal: number; - tax: number; - total: number; -} - -export async function POST(req: Request) { - const client = await pool.connect(); - - try { - const orderData: CustomerOrderRequest = await req.json(); - - await client.query('BEGIN'); - - // Group items by type and combine quantities - const groupedItems = orderData.items.reduce((acc, item) => { - const key = `${item.menuItemId}_${item.category}`; - if (!acc[key]) { - acc[key] = { ...item, quantity: 0 }; - } - acc[key].quantity += item.quantity; - return acc; - }, {} as Record); - - // Format order details - const orderDetails = { - free_items: [ - { id: 9, name: "Napkins", quantity: 2 }, - { id: 2, name: "Soy Sauce Packet", quantity: 1 }, - { id: 5, name: "Fortune Cookies", quantity: 1 }, - { id: 6, name: "Utensils (Forks)", quantity: 1 }, - { id: 7, name: "Utensils (Knives)", quantity: 1 }, - { id: 8, name: "Utensils (Spoons)", quantity: 1 }, - { id: 12, name: "Takeout Cartons", quantity: 1 } - ], - entree_side: [] as any[], - drink_table: [] as any[] - }; - - // Insert order - const orderResult = await client.query( - `INSERT INTO orders (datetime, sale, items) - VALUES (CURRENT_TIMESTAMP, $1, $2::jsonb) - RETURNING id`, - [orderData.total, orderDetails] - ); - - const orderId = orderResult.rows[0].id; - - // Process each grouped item - for (const item of Object.values(groupedItems)) { - let tableName; - switch (item.category) { - case 'entree': - case 'side': - tableName = 'entree_side'; - orderDetails.entree_side.push({ - id: parseInt(item.menuItemId), - name: item.name.toLowerCase(), - quantity: item.quantity - }); - break; - case 'drink': - tableName = 'drink_table'; - orderDetails.drink_table.push({ - id: parseInt(item.menuItemId), - name: item.name.toLowerCase(), - quantity: item.quantity - }); - break; - case 'appetizer': - tableName = 'appetizers'; - break; - default: - continue; - } - - // Update inventory with combined quantity - await client.query( - `UPDATE ${tableName} - SET inventory = inventory - $1 - WHERE id = $2`, - [item.quantity, parseInt(item.menuItemId)] - ); - - // Single insert into order_items for each unique item - await client.query( - `INSERT INTO order_items (order_id, item_id, item_type) - VALUES ($1, $2, $3)`, - [orderId, parseInt(item.menuItemId), tableName] - ); - } - - // Insert free items - for (const freeItem of orderDetails.free_items) { - await client.query( - `INSERT INTO order_items (order_id, item_id, item_type) - VALUES ($1, $2, 'free_items')`, - [orderId, freeItem.id] - ); - } - - // Update the order with final details - await client.query( - `UPDATE orders - SET items = $1::jsonb - WHERE id = $2`, - [orderDetails, orderId] - ); - - await client.query('COMMIT'); - - return NextResponse.json({ - success: true, - orderId, - message: 'Order processed successfully' - }); - - } catch (error) { - await client.query('ROLLBACK'); - console.error('Order processing error:', error); - return NextResponse.json( - { success: false, message: 'Failed to process order' }, - { status: 500 } - ); - } finally { - client.release(); - } -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\api\orders\route.ts - -import { NextResponse } from 'next/server'; -import { Pool, PoolConfig } from 'pg'; -import { OrderItem } from '@/types'; - -const dbConfig: PoolConfig = { - user: process.env.POSTGRES_USER || 'team_6b', - host: process.env.POSTGRES_HOST || 'csce-315-db.engr.tamu.edu', - database: process.env.POSTGRES_DB || 'team_6b_db', - password: process.env.POSTGRES_PASSWORD || 'kartana', - port: parseInt(process.env.POSTGRES_PORT || '5432'), - ssl: { - rejectUnauthorized: false - } -}; - -const pool = new Pool(dbConfig); - -async function executeQuery(query: string, params?: any[]) { - try { - const result = await pool.query(query, params); - return result.rows; - } catch (error) { - console.error('Database query error:', error); - throw error; - } -} - -interface OrderRequest { - items: OrderItem[]; - subtotal: number; - tax: number; - tip?: number; - total: number; -} - -export async function POST(req: Request) { - try { - const orderData: OrderRequest = await req.json(); - - // Base free items that every order gets - const baseOrderItems = { - free_items: [ - { id: 9, name: "Napkins", quantity: 2 }, - { id: 2, name: "Soy Sauce Packet", quantity: 1 }, - { id: 5, name: "Fortune Cookies", quantity: 1 }, - { id: 6, name: "Utensils (Forks)", quantity: 1 }, - { id: 7, name: "Utensils (Knives)", quantity: 1 }, - { id: 8, name: "Utensils (Spoons)", quantity: 1 } - ], - entree_side: orderData.items - .filter(item => item.category === 'entree' || item.category === 'side') - .map(item => ({ - id: parseInt(item.menuItemId), - name: item.name.toLowerCase(), - quantity: item.quantity - })) - }; - - // Add drink_table only if there are drinks in the order - const drinkItems = orderData.items.filter(item => item.category === 'drink'); - if (drinkItems.length > 0) { - baseOrderItems['drink_table'] = drinkItems.map(item => ({ - id: parseInt(item.menuItemId), - name: item.name, - quantity: item.quantity - })); - } - - // Add takeout cartons for entrees - if (baseOrderItems.entree_side.length > 0) { - baseOrderItems.free_items.push({ id: 12, name: "Takeout Cartons", quantity: 1 }); - } - - // Start transaction - await executeQuery('BEGIN'); - - try { - // Insert main order and return the inserted id - const orderResult = await executeQuery( - `INSERT INTO orders (datetime, sale, items) - VALUES (CURRENT_TIMESTAMP, $1, $2) - RETURNING id`, - [ - Number(orderData.total.toFixed(2)), // Ensure consistent decimal places - JSON.stringify(baseOrderItems) - ] - ); - - const orderId = orderResult[0].id; - - // Insert all items into order_items table - for (const item of orderData.items) { - let itemType = ''; - if (item.category === 'entree' || item.category === 'side') { - itemType = 'entree_side'; - } else if (item.category === 'drink') { - itemType = 'drink_table'; - } - - if (itemType) { - // Insert item reference - await executeQuery( - `INSERT INTO order_items (order_id, item_id, item_type) - VALUES ($1, $2, $3)`, - [orderId, parseInt(item.menuItemId), itemType] - ); - - // Update inventory - await executeQuery( - `UPDATE ${itemType} - SET inventory = inventory - $1 - WHERE id = $2`, - [item.quantity, parseInt(item.menuItemId)] - ); - } - } - - // Insert free items - for (const freeItem of baseOrderItems.free_items) { - await executeQuery( - `INSERT INTO order_items (order_id, item_id, item_type) - VALUES ($1, $2, 'free_items')`, - [orderId, freeItem.id] - ); - } - - await executeQuery('COMMIT'); - - return NextResponse.json({ - success: true, - orderId, - message: 'Order created successfully' - }); - - } catch (error) { - await executeQuery('ROLLBACK'); - throw error; - } - } catch (error) { - console.error('Order creation error:', error); - return NextResponse.json( - { success: false, message: 'Failed to create order' }, - { status: 500 } - ); - } -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\api\reports\product-usage\route.ts - -// src/app/api/reports/product-usage/route.ts -import { NextResponse } from 'next/server'; -import { executeQuery } from '@/lib/db'; - -export async function POST(req: Request) { - try { - const { startDate, endDate } = await req.json(); - - const [entrees, sides, drinks, appetizers] = await Promise.all([ - executeQuery(` - SELECT i.name, COUNT(*) as total_used - FROM order_items oi - JOIN orders o ON oi.order_id = o.id - JOIN entree_side i ON oi.item_id = i.id - WHERE o.datetime BETWEEN $1 AND $2 - AND i.type = true - GROUP BY i.name - ORDER BY total_used DESC - `, [startDate, endDate]), - - executeQuery(` - SELECT i.name, COUNT(*) as total_used - FROM order_items oi - JOIN orders o ON oi.order_id = o.id - JOIN entree_side i ON oi.item_id = i.id - WHERE o.datetime BETWEEN $1 AND $2 - AND i.type = false - GROUP BY i.name - ORDER BY total_used DESC - `, [startDate, endDate]), - - executeQuery(` - SELECT i.name, COUNT(*) as total_used - FROM order_items oi - JOIN orders o ON oi.order_id = o.id - JOIN drink_table i ON oi.item_id = i.id - WHERE o.datetime BETWEEN $1 AND $2 - GROUP BY i.name - ORDER BY total_used DESC - `, [startDate, endDate]), - - executeQuery(` - SELECT i.name, COUNT(*) as total_used - FROM order_items oi - JOIN orders o ON oi.order_id = o.id - JOIN appetizers i ON oi.item_id = i.id - WHERE o.datetime BETWEEN $1 AND $2 - GROUP BY i.name - ORDER BY total_used DESC - `, [startDate, endDate]) - ]); - - return NextResponse.json({ - entrees: entrees.map(row => ({ - name: row.name, - usage: parseInt(row.total_used) - })), - sides: sides.map(row => ({ - name: row.name, - usage: parseInt(row.total_used) - })), - drinks: drinks.map(row => ({ - name: row.name, - usage: parseInt(row.total_used) - })), - appetizers: appetizers.map(row => ({ - name: row.name, - usage: parseInt(row.total_used) - })) - }); - - } catch (error) { - console.error('Error generating product usage report:', error); - return NextResponse.json( - { error: 'Failed to generate product usage report' }, - { status: 500 } - ); - } -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\api\reports\sales\route.ts - -// src/app/api/reports/sales/route.ts -import { NextResponse } from 'next/server'; -import { executeQuery } from '@/lib/db'; - -export async function POST(req: Request) { - try { - const { startDate, endDate } = await req.json(); - - // Validate dates - if (!startDate || !endDate) { - return NextResponse.json({ - error: 'Start date and end date are required', - data: null - }, { status: 400 }); - } - - // Get sales data with multiple breakdowns - const [salesSummary, itemizedSales, dailySales] = await Promise.all([ - executeQuery(` - SELECT - COALESCE(SUM(sale), 0) as total_sales, - COUNT(*) as total_orders, - CASE - WHEN COUNT(*) > 0 THEN COALESCE(SUM(sale), 0) / COUNT(*) - ELSE 0 - END as average_order_value, - MIN(sale) as min_order_value, - MAX(sale) as max_order_value - FROM orders - WHERE datetime BETWEEN $1 AND $2 - `, [startDate, endDate]), - - executeQuery(` - SELECT - i.name, - i.type, - COUNT(*) as quantity_sold, - COALESCE(SUM(o.sale / NULLIF( - (SELECT COUNT(*) FROM order_items oi2 WHERE oi2.order_id = o.id), 0 - )), 0) as estimated_sales - FROM order_items oi - JOIN orders o ON oi.order_id = o.id - JOIN ( - SELECT id, name, 'Entree' as type FROM entree_side WHERE type = true - UNION ALL - SELECT id, name, 'Side' as type FROM entree_side WHERE type = false - UNION ALL - SELECT id, name, 'Drink' as type FROM drink_table - UNION ALL - SELECT id, name, 'Appetizer' as type FROM appetizers - ) i ON oi.item_id = i.id - WHERE datetime BETWEEN $1 AND $2 - GROUP BY i.name, i.type - ORDER BY quantity_sold DESC - `, [startDate, endDate]), - - executeQuery(` - SELECT - DATE(datetime) as sale_date, - COUNT(*) as num_orders, - COALESCE(SUM(sale), 0) as daily_total - FROM orders - WHERE datetime BETWEEN $1 AND $2 - GROUP BY DATE(datetime) - ORDER BY sale_date - `, [startDate, endDate]) - ]); - - const responseData = { - date_range: { - start: startDate, - end: endDate - }, - summary: { - total_sales: parseFloat(salesSummary[0]?.total_sales ?? '0'), - total_orders: parseInt(salesSummary[0]?.total_orders ?? '0'), - average_order_value: parseFloat(salesSummary[0]?.average_order_value ?? '0'), - min_order_value: parseFloat(salesSummary[0]?.min_order_value ?? '0'), - max_order_value: parseFloat(salesSummary[0]?.max_order_value ?? '0') - }, - itemized_sales: itemizedSales.map(row => ({ - name: row.name, - type: row.type, - quantity_sold: parseInt(row.quantity_sold), - sales: parseFloat(row.estimated_sales) - })), - daily_sales: dailySales.map(row => ({ - date: row.sale_date, - num_orders: parseInt(row.num_orders), - total: parseFloat(row.daily_total) - })) - }; - - return NextResponse.json(responseData); - - } catch (error) { - console.error('Error generating sales report:', error); - return NextResponse.json({ - error: 'Failed to generate sales report', - data: null - }, { status: 500 }); - } - } - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\api\reports\x-report\route.ts - -// src/app/api/reports/x-report/route.ts -import { NextResponse } from 'next/server'; -import { executeQuery } from '@/lib/db'; - -export async function GET() { - try { - const hourly_sales = await executeQuery(` - SELECT - EXTRACT(HOUR FROM datetime) as hour, - COALESCE(SUM(sale), 0) as total_sales, - COUNT(*) as num_orders - FROM orders - WHERE DATE(datetime) = CURRENT_DATE - GROUP BY EXTRACT(HOUR FROM datetime) - ORDER BY hour - `); - - if (!hourly_sales.length) { - // Return structured data even when no sales - return NextResponse.json({ - hourly_sales: [], - summary: { - total_sales: 0, - total_orders: 0, - average_order_value: 0 - } - }); - } - - // Calculate summary statistics - const summary = await executeQuery(` - SELECT - COALESCE(SUM(sale), 0) as total_sales, - COUNT(*) as total_orders, - CASE - WHEN COUNT(*) > 0 THEN COALESCE(SUM(sale), 0) / COUNT(*) - ELSE 0 - END as average_order_value - FROM orders - WHERE DATE(datetime) = CURRENT_DATE - `); - - const formattedData = { - hourly_sales: hourly_sales.map(row => ({ - hour: parseInt(row.hour), - total_sales: parseFloat(row.total_sales), - num_orders: parseInt(row.num_orders) - })), - summary: { - total_sales: parseFloat(summary[0].total_sales), - total_orders: parseInt(summary[0].total_orders), - average_order_value: parseFloat(summary[0].average_order_value) - } - }; - - return NextResponse.json(formattedData); - - } catch (error) { - console.error('Error generating X report:', error); - return NextResponse.json({ - error: 'Failed to generate X report', - hourly_sales: [], - summary: { - total_sales: 0, - total_orders: 0, - average_order_value: 0 - } - }, { status: 500 }); - } -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\api\reports\z-report\route.ts - -// src/app/api/reports/z-report/route.ts -import { NextResponse } from 'next/server'; -import { executeQuery } from '@/lib/db'; - -export async function GET() { - try { - const [totals, itemSales, categorySales] = await Promise.all([ - executeQuery(` - SELECT - COALESCE(SUM(sale), 0) as total_sales, - COUNT(*) as total_orders, - CASE - WHEN COUNT(*) > 0 THEN COALESCE(SUM(sale), 0) / COUNT(*) - ELSE 0 - END as average_order_value - FROM orders - WHERE DATE(datetime) = CURRENT_DATE - `), - - // Modified to properly extract and count individual items from combos - executeQuery(` - WITH RECURSIVE extracted_items AS ( - -- First, get all regular items and their quantities - SELECT - o.id as order_id, - o.sale as order_total, - LOWER(i->>'name') as item_name, - (i->>'quantity')::integer as quantity - FROM orders o, - jsonb_array_elements(o.items->'entree_side') as i - WHERE DATE(o.datetime) = CURRENT_DATE - AND NOT (LOWER(i->>'name') LIKE '%plate%' OR LOWER(i->>'name') LIKE '%bowl%') - - UNION ALL - - -- Then extract individual items from combos (plates and bowls) - SELECT - o.id as order_id, - o.sale / NULLIF( - array_length( - string_to_array( - regexp_replace( - regexp_replace(LOWER(i->>'name'), '^(bigger plate|plate|bowl)\\s*\\((.*)\\).*$', '\\2'), - '\\s*,\\s*', ',' - ), - ',' - ), - 1 - ), - 0 - ) as order_total, - trim(both ' ' from unnest( - string_to_array( - regexp_replace( - regexp_replace(LOWER(i->>'name'), '^(bigger plate|plate|bowl)\\s*\\((.*)\\).*$', '\\2'), - '\\s*,\\s*', ',' - ), - ',' - ) - )) as item_name, - (i->>'quantity')::integer as quantity - FROM orders o, - jsonb_array_elements(o.items->'entree_side') as i - WHERE DATE(o.datetime) = CURRENT_DATE - AND (LOWER(i->>'name') LIKE '%plate%' OR LOWER(i->>'name') LIKE '%bowl%') - - UNION ALL - - -- Get drink items - SELECT - o.id, - o.sale, - LOWER(i->>'name') as item_name, - (i->>'quantity')::integer as quantity - FROM orders o, - jsonb_array_elements(o.items->'drink_table') as i - WHERE DATE(o.datetime) = CURRENT_DATE - - UNION ALL - - -- Get appetizer items - SELECT - o.id, - o.sale, - LOWER(i->>'name') as item_name, - (i->>'quantity')::integer as quantity - FROM orders o, - jsonb_array_elements(o.items->'appetizers') as i - WHERE DATE(o.datetime) = CURRENT_DATE - ) - SELECT - item_name as name, - SUM(quantity) as quantity, - SUM(order_total * quantity) as estimated_sales - FROM extracted_items - WHERE item_name IS NOT NULL - AND item_name != '' - AND NOT item_name LIKE '%plate%' - AND NOT item_name LIKE '%bowl%' - GROUP BY item_name - ORDER BY SUM(quantity) DESC - `), - - // Category summary - executeQuery(` - WITH RECURSIVE extracted_items AS ( - -- Same CTE as above for consistency - SELECT - o.id as order_id, - o.sale as order_total, - LOWER(i->>'name') as item_name, - (i->>'quantity')::integer as quantity - FROM orders o, - jsonb_array_elements(o.items->'entree_side') as i - WHERE DATE(o.datetime) = CURRENT_DATE - AND NOT (LOWER(i->>'name') LIKE '%plate%' OR LOWER(i->>'name') LIKE '%bowl%') - - UNION ALL - - SELECT - o.id as order_id, - o.sale / NULLIF( - array_length( - string_to_array( - regexp_replace( - regexp_replace(LOWER(i->>'name'), '^(bigger plate|plate|bowl)\\s*\\((.*)\\).*$', '\\2'), - '\\s*,\\s*', ',' - ), - ',' - ), - 1 - ), - 0 - ) as order_total, - trim(both ' ' from unnest( - string_to_array( - regexp_replace( - regexp_replace(LOWER(i->>'name'), '^(bigger plate|plate|bowl)\\s*\\((.*)\\).*$', '\\2'), - '\\s*,\\s*', ',' - ), - ',' - ) - )) as item_name, - (i->>'quantity')::integer as quantity - FROM orders o, - jsonb_array_elements(o.items->'entree_side') as i - WHERE DATE(o.datetime) = CURRENT_DATE - AND (LOWER(i->>'name') LIKE '%plate%' OR LOWER(i->>'name') LIKE '%bowl%') - - UNION ALL - - SELECT - o.id, - o.sale, - LOWER(i->>'name') as item_name, - (i->>'quantity')::integer as quantity - FROM orders o, - jsonb_array_elements(o.items->'drink_table') as i - WHERE DATE(o.datetime) = CURRENT_DATE - - UNION ALL - - SELECT - o.id, - o.sale, - LOWER(i->>'name') as item_name, - (i->>'quantity')::integer as quantity - FROM orders o, - jsonb_array_elements(o.items->'appetizers') as i - WHERE DATE(o.datetime) = CURRENT_DATE - ) - SELECT - CASE - WHEN es.type = true THEN 'Entrees' - WHEN es.type = false THEN 'Sides' - WHEN dt.id IS NOT NULL THEN 'Drinks' - WHEN ap.id IS NOT NULL THEN 'Appetizers' - ELSE 'Other' - END as category, - SUM(quantity) as items_sold, - SUM(order_total * quantity) as category_sales - FROM extracted_items ei - LEFT JOIN entree_side es ON LOWER(es.name) = ei.item_name - LEFT JOIN drink_table dt ON LOWER(dt.name) = ei.item_name - LEFT JOIN appetizers ap ON LOWER(ap.name) = ei.item_name - WHERE item_name NOT LIKE '%plate%' AND item_name NOT LIKE '%bowl%' - GROUP BY - CASE - WHEN es.type = true THEN 'Entrees' - WHEN es.type = false THEN 'Sides' - WHEN dt.id IS NOT NULL THEN 'Drinks' - WHEN ap.id IS NOT NULL THEN 'Appetizers' - ELSE 'Other' - END - ORDER BY items_sold DESC - `) - ]); - - const responseData = { - report_date: new Date().toISOString(), - summary: { - total_sales: parseFloat(totals[0]?.total_sales ?? '0'), - total_orders: parseInt(totals[0]?.total_orders ?? '0'), - average_order_value: parseFloat(totals[0]?.average_order_value ?? '0') - }, - items_sold: itemSales.map(row => ({ - name: row.name, - quantity: parseInt(row.quantity), - sales: parseFloat(row.estimated_sales) - })), - category_summary: categorySales.map(row => ({ - category: row.category, - items_sold: parseInt(row.items_sold), - sales: parseFloat(row.category_sales) - })) - }; - - return NextResponse.json(responseData); - - } catch (error) { - console.error('Error generating Z report:', error); - return NextResponse.json({ - error: 'Failed to generate Z report', - report_date: new Date().toISOString(), - summary: { - total_sales: 0, - total_orders: 0, - average_order_value: 0 - }, - items_sold: [], - category_summary: [] - }, { status: 500 }); - } -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\api\reports\utils.ts - -// src/app/api/reports/utils.ts -import db from '@/server/config/db'; - -export async function executeQuery(query: string, params?: any[]) { - try { - const result = await db.query(query, params); - return result.rows; - } catch (error) { - console.error('Database query error:', error); - throw error; - } -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\cart\page.tsx - -'use client'; - -import { MenuItem } from '@/types'; -import React, { useState, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import { fetchMenuItems as initialMenuItems } from '@/utils/menuItems'; - -const MenuPage: React.FC = () => { - const router = useRouter(); - // Keep your existing state variables - const [selectedCategory, setSelectedCategory] = useState('combo'); - const [menuItems, setMenuItems] = useState([]); - const [cartItems, setCartItems] = useState([]); - const [showModal, setShowModal] = useState(false); - const [modalItem, setModalItem] = useState(null); - const [selectedSide, setSelectedSide] = useState(null); - const [selectedEntrees, setSelectedEntrees] = useState([]); - const [showSideModal, setShowSideModal] = useState(false); - const [showEntreeModal, setShowEntreeModal] = useState(false); - - - // Add new state for order processing - const [selectedTipPercent, setSelectedTipPercent] = useState(null); - const [customTipAmount, setCustomTipAmount] = useState(''); - const [isProcessingOrder, setIsProcessingOrder] = useState(false); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - // Call the fetch function and await its result - const fetchData = async () => { - const items = await initialMenuItems(); // Assuming this fetches the menu items correctly - setMenuItems(items); // Set the state with the fetched items - setIsLoading(false); // Turn off loading spinner - }; - - fetchData(); // Call fetchData to execute the async operation - }, []); - - const TAX_RATE = 0.0825; // 8.25% tax rate - - // Calculate order totals - const subtotal = cartItems.reduce((acc, item) => acc + item.price, 0); - const tax = subtotal * TAX_RATE; - const tipAmount = selectedTipPercent ? (subtotal * selectedTipPercent) / 100 : - customTipAmount ? parseFloat(customTipAmount) : 0; - const total = subtotal + tax + tipAmount; - - - - // Keep your existing filter functions - const sideItems = menuItems.filter(item => item.category === 'side'); - const entreeItems = menuItems.filter(item => item.category === 'entree' || item.category === 'appetizer'); - const filteredItems = selectedCategory === 'all' - ? menuItems - : menuItems.filter(item => item.category === selectedCategory); - - const renderCartItemDetails = (item: MenuItem) => { - if (item.category === 'combo') { - return ( -
-

Side: {item.selectedSide ? item.selectedSide.name : 'None'}

-

Entrees: {item.selectedEntrees && item.selectedEntrees.length > 0 - ? item.selectedEntrees.map(entree => entree.name).join(', ') - : 'None'} -

-
- ); - } - return null; - }; - - // Handle bowl, plate, and bigger plate orders - const orderBowl = () => { - openModal({ - id: 'bowl', - name: 'Bowl', - description: '1 Side & 1 Entree', - price: 8.30, - category: 'combo', - imageUrl: '/images/combos/bowl.png', - available: true, - }); - }; - - const orderPlate = () => { - openModal({ - id: 'plate', - name: 'Plate', - description: '1 Side & 2 Entrees', - price: 9.80, - category: 'combo', - imageUrl: '/images/combos/plate.png', - available: true, - }); - }; - - const orderBiggerPlate = () => { - openModal({ - id: 'biggerPlate', - name: 'Bigger Plate', - description: '1 Side & 3 Entrees', - price: 11.30, - category: 'combo', - imageUrl: '/images/combos/biggerPlate.png', - available: true, - }); - }; - - // Remove item from cart - const removeFromCart = (index: number) => { - const updatedItems = [...cartItems]; - updatedItems.splice(index, 1); - setCartItems(updatedItems); - }; - - const handleSideSelect = (side: MenuItem) => { - setSelectedSide(side); - setShowSideModal(false); - }; - - const handleEntreeSelect = (entree: MenuItem) => { - if (modalItem?.name === 'Bowl' && selectedEntrees.length < 1) { - setSelectedEntrees([entree]); - } else if (modalItem?.name === 'Plate' && selectedEntrees.length < 2) { - setSelectedEntrees([...selectedEntrees, entree]); - } else if (modalItem?.name === 'Bigger Plate' && selectedEntrees.length < 3) { - setSelectedEntrees([...selectedEntrees, entree]); - } - - if ( - (modalItem?.name === 'Bowl' && selectedEntrees.length === 0) || - (modalItem?.name === 'Plate' && selectedEntrees.length === 1) || - (modalItem?.name === 'Bigger Plate' && selectedEntrees.length === 2) - ) { - setShowEntreeModal(false); - } - }; - - - // Handle modal opening and closing - const openModal = (item: MenuItem) => { - setModalItem(item); - setSelectedSide(null); - setSelectedEntrees([]); - setShowModal(true); - }; - - const closeModal = () => { - setModalItem(null); - setSelectedSide(null); - setSelectedEntrees([]); - setShowModal(false); - }; - - // Modify the handleCheckout function - const handleCheckout = async () => { - if (cartItems.length === 0) { - alert('Your cart is empty'); - return; - } - - setIsProcessingOrder(true); - - try { - // Format cart items for the API - const formattedItems = cartItems.flatMap(item => { - if (item.category === 'combo') { - // Handle combo items - const entrées = item.selectedEntrees?.map(entree => ({ - menuItemId: entree.id, - name: entree.name.toLowerCase(), - quantity: 1, - price: item.price / (item.selectedEntrees?.length || 1), // Split combo price - category: 'entree' - })); - - const side = item.selectedSide ? [{ - menuItemId: item.selectedSide.id, - name: item.selectedSide.name.toLowerCase(), - quantity: 1, - price: 0, // Side is included in combo price - category: 'side' - }] : []; - - return [...(entrées || []), ...side]; - } - - // Handle regular items - return { - menuItemId: item.id, - name: item.name.toLowerCase(), - quantity: 1, - price: item.price, - category: item.category - }; - }); - - const orderData = { - items: formattedItems, - subtotal, - tax, - tip: tipAmount, - total - }; - - const response = await fetch('/api/customer-orders', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(orderData) - }); - - if (!response.ok) { - throw new Error('Failed to process order'); - } - - const result = await response.json(); - - if (result.success) { - // Store order details for confirmation page - localStorage.setItem('lastOrder', JSON.stringify({ - orderId: result.orderId, - items: cartItems, - subtotal, - tax, - tip: tipAmount, - total, - timestamp: new Date().toISOString() - })); - - // Clear cart and redirect - setCartItems([]); - router.push(`/order-confirmation/${result.orderId}`); - } else { - throw new Error(result.message || 'Failed to process order'); - } - } catch (error) { - console.error('Checkout error:', error); - alert('Failed to process order. Please try again.'); - } finally { - setIsProcessingOrder(false); - } - }; - - // Keep all your existing handlers - const addToCart = (item: MenuItem) => { - if (item.category === 'combo') { - const comboItem = { - ...item, - selectedSide, - selectedEntrees, - }; - - setCartItems([...cartItems, comboItem]); - - - } else { - setCartItems([...cartItems, item]); - } - }; - - return ( -
- -

Our Menu

- -
- {/* Menu items grid */} -
-
- {/* Category buttons */} - - - - - - - - - - - - - - - - - -
- -
- {filteredItems.map(item => ( -
- {item.name} -
-
{/* Adjust min height as needed */} -

{item.name}

-

{item.description}

{/* Full text shown */} -
-
-

${Number(item.price).toFixed(2)}

- {/*

${item.price}

*/} - -
-
-
- ))} -
- - -
- - {/* Enhanced Checkout column */} -
-
-

Your Cart

- {cartItems.length === 0 ? ( -

Your cart is empty

- ) : ( - <> -
    - {cartItems.map((item, index) => ( -
  • -
    -

    {item.name}

    -

    ${Number(item.price).toFixed(2)}

    - {/*

    ${item.price}

    */} - {renderCartItemDetails(item)} -
    - -
  • - ))} -
- -
-
- Subtotal - ${subtotal.toFixed(2)} -
-
- Tax (8.25%) - ${tax.toFixed(2)} -
- - {/* Tip Selection */} -
-

Add Tip

-
- {[15, 18, 20].map((percent) => ( - - ))} -
-
- $ - { - setCustomTipAmount(e.target.value); - setSelectedTipPercent(null); - }} - placeholder="Custom amount" - className="w-full p-2 border rounded text-sm" - min="0" - step="0.01" - /> -
-
- -
- Total - ${total.toFixed(2)} -
-
- - - - )} -
-
-
- - - {/* Modal for bowl, plate, and bigger plate orders */} - {showModal && modalItem && ( -
-
-

{modalItem.name}

-

{modalItem.description}

-

${modalItem.price.toFixed(2)}

- - - - {/* Selected Side Section */} -
-
-

Side

- {!selectedSide && ( - - )} -
- {selectedSide ? ( -
- {selectedSide.name} -
-
{selectedSide.name}
-

{selectedSide.description}

-
- -
- ) : ( -
- No side selected -
- )} -
- -
-
-

- Entrees ({selectedEntrees.length}/ - {modalItem.name === 'Bowl' ? '1' : - modalItem.name === 'Plate' ? '2' : '3'}) -

- {selectedEntrees.length < ( - modalItem.name === 'Bowl' ? 1 : - modalItem.name === 'Plate' ? 2 : 3 - ) && ( - - )} -
-
- {selectedEntrees.length > 0 ? ( - selectedEntrees.map((entree, index) => ( -
- {entree.name} -
-
{entree.name}
-

{entree.description}

-
- -
- )) - ) : ( -
- No entrees selected -
- )} -
-
- - - {/* Confirm/Cancel */} -
- - -
-
-
- )} - -{showSideModal && ( -
-
-
-

Choose a Side

- -
-
- {sideItems.map(item => ( -
handleSideSelect(item)} - > - {item.name} -
-

{item.name}

-

{item.description}

-
-
- ))} -
-
-
- )} - - {/* Updated Entree Selection Modal */} - {showEntreeModal && ( -
-
-
-
-

Choose your Entrees

-

- Selected: {selectedEntrees.length} / - {modalItem?.name === 'Bowl' ? '1' : - modalItem?.name === 'Plate' ? '2' : '3'} -

-
- -
-
- {entreeItems.map(item => ( -
e.id === item.id) ? 'border-[var(--panda-red)]' : 'border-transparent'}`} - onClick={() => handleEntreeSelect(item)} - > - {item.name} -
-

{item.name}

-

{item.description}

-
-
- ))} -
-
-
- )} - -
- ); -}; - -export default MenuPage; - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\menu\page.tsx - -// page.tsx -'use client'; - -import { useEffect, useState } from 'react'; -import MenuGrid from '@/components/menu/MenuGrid'; -import { MenuItem } from '@/types'; -import { fetchMenuItems as initialMenuItems } from '@/utils/menuItems'; - -export default function MenuPage() { - const [menuItems, setMenuItems] = useState([]); - const [selectedCategory, setSelectedCategory] = useState('all'); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const fetchData = async () => { - const items = await initialMenuItems(); - setMenuItems(items); - setIsLoading(false); - }; - - fetchData(); - }, []); - - const filteredItems = selectedCategory === 'all' - ? menuItems - : menuItems.filter(item => item.category === selectedCategory); - - return ( -
-

Our Menu

- -
- - - - - - - - - - - -
- - -
- {isLoading ? ( -
-
-
- ) : ( - - )} -
-
- ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\order-confirmation\[orderId]\page.tsx - - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\globals.css - -@tailwind base; -@tailwind components; -@tailwind utilities; - -/* Panda Express Brand Colors */ -:root { - --panda-red: #ED1C24; - --panda-dark-red: #C41017; - --panda-black: #000000; - --panda-gold: #C4A484; - --panda-light-gold: #D4B494; - --panda-cream: #FFF8DC; - --panda-white: #FFFFFF; - --panda-gray: #F5F5F5; - --video-fade-opacity: 0.7; -} - -/* Base Styles */ -@layer base { - body { - background-color: var(--panda-white); - color: var(--panda-black); - min-height: 100vh; - font-family: sans-serif; - } - - h1 { - @apply text-3xl font-bold mb-4; - } - - h2 { - @apply text-2xl font-bold mb-3; - } - - h3 { - @apply text-xl font-bold mb-2; - } - -} - -/* High Contrast Mode */ -@layer base { - .high-contrast { - background-color: #000000 !important; - color: #FFFFFF !important; - } - - /* Enhanced Text Sizes */ - .high-contrast h1 { - font-size: 8rem !important; /* 56px */ - line-height: 6rem !important; - margin-bottom: 1.5rem !important; - } - - .high-contrast h2 { - font-size: 5rem !important; /* 44px */ - line-height: 4.25rem !important; - margin-bottom: 1.25rem !important; - } - - .high-contrast h3 { - font-size: 3rem !important; /* 36px */ - line-height: 3.5rem !important; - margin-bottom: 1rem !important; - } - - .high-contrast p { - font-size: 2.5rem !important; /* 24px */ - line-height: 2.75rem !important; - margin-bottom: 1.5rem !important; - } - - /* Button and Link Text */ - .high-contrast .btn-primary, - .high-contrast .btn-secondary, - .high-contrast .nav-link { - font-size: 2rem !important; /* 24px */ - line-height: 2rem !important; - padding: 1rem 1.5rem !important; - } - - .high-contrast nav { - height: 125px !important; /* Increase navbar height */ - padding: 0 2rem !important; /* Increase padding for more space */ - padding-top: 1rem !important; - - } - - .high-contrast button { - background-color: #FF0000 !important; /* Bright red for high contrast */ - color: #FFFFFF !important; /* White text */ - padding: 1rem 2rem !important; /* Larger padding for better clickability */ - border-radius: 0.375rem !important; /* Rounded corners */ - font-weight: bold !important; - font-size: 1.5rem !important; /* Increased font size */ - - } - - /* Menu Items and Prices */ - .high-contrast .text-xl { - font-size: 1.75rem !important; /* 28px */ - line-height: 2.25rem !important; - } - - .high-contrast .text-lg { - font-size: 1.5rem !important; /* 24px */ - line-height: 2rem !important; - } - - .high-contrast .text-base { - font-size: 1.25rem !important; /* 20px */ - line-height: 1.75rem !important; - } - - /* Input Fields */ - .high-contrast input, - .high-contrast select, - .high-contrast textarea { - font-size: 1.5rem !important; /* 24px */ - line-height: 2rem !important; - padding: 1rem !important; - } - - /* Override background colors for sections */ - .high-contrast .bg-white, - .high-contrast .bg-gray-50, - .high-contrast .bg-gray-100 { - background-color: #000000 !important; - color: #FFFFFF !important; - - } - - - /* Text colors */ - .high-contrast .text-gray-600, - .high-contrast .text-gray-700, - .high-contrast .text-gray-800, - .high-contrast .text-gray-900 { - /* color: #FFFFFF !important; */ - color: #FF1A1A - } - - /* Override card backgrounds */ - .high-contrast .bg-white.rounded-lg, - .high-contrast .card { - background-color: #000000 !important; - color: #FFFFFF !important; - border: 10px solid #ff0000 !important; /* Increased border width */ - padding: 1.5rem !important; /* Increased padding */ - - - } - - .high-contrast .grid { - grid-template-columns: repeat(1, minmax(0, 1fr)) !important; - } - - @media (min-width: 640px) { - .high-contrast .grid { - grid-template-columns: repeat(1, minmax(0, 1fr)) !important; - } - } - - @media (min-width: 768px) { - .high-contrast .grid { - grid-template-columns: repeat(2, minmax(0, 1fr)) !important; - } - } - - @media (min-width: 1024px) { - .high-contrast .grid { - grid-template-columns: repeat(2, minmax(0, 1fr)) !important; - } - } - - /* Increased Grid Gap */ - .high-contrast .gap-4 { - gap: 2rem !important; - } - - /* Buttons */ - .high-contrast .btn-primary { - background-color: #000000 !important; - color: #ffffff !important; - border: 3px solid #FF1A1A !important; - font-size: 2rem !important; - padding: 1rem 1.5rem !important; - line-height: 2rem !important; - font-weight: bold; - min-height: 3.5rem !important; - margin: 0.5rem !important; - } - - .high-contrast .btn-primary:hover { - background-color: #FF1A1A !important; - color: #FFFFFF !important; - } - - /* Ensure buttons maintain proper spacing */ - .high-contrast .flex.gap-4 > * { - margin: 0.5rem !important; - } - - .high-contrast .btn-secondary { - background-color: #000000 !important; - color: #FFFFFF !important; - border: 3px solid #FFFFFF !important; - font-weight: bold; - min-height: 3.5rem !important; - border-color: #FF1A1A !important; - } - - .high-contrast .btn-secondary:hover { - background-color: #FFFFFF !important; - color: #000000 !important; - } - - /* Input fields */ - .high-contrast .input-field { - background-color: #000000 !important; - color: #FFFFFF !important; - border: 3px solid #FFFFFF !important; - min-height: 3.5rem !important; - } - - .high-contrast .input-field:focus { - border-color: #FF1A1A !important; - box-shadow: 0 0 0 3px #FF1A1A !important; - } - - .high-contrast .max-w-md { - max-width: 600px !important; /* Make modal wider */ - max-height: 90vh !important; /* Set a max height for the modal (90% of viewport height) */ - overflow-y: auto !important; - } - - /* Navigation */ - .high-contrast .nav-link { - color: #FFFFFF !important; - border: 3px solid #FFFFFF !important; - border-color: #FF1A1A !important; - } - - .high-contrast modal { - height: 400px; - } - - .high-contrast .nav-link:hover { - - text-decoration: underline; - text-decoration-thickness: 3px !important; - } - - /* Tables */ - .high-contrast .table-header { - background-color: #000000 !important; - color: #FFFFFF !important; - border: 3px solid #FFFFFF !important; - font-size: 1.5rem !important; - } - - .high-contrast .table-cell { - color: #FFFFFF !important; - border: 2px solid #FFFFFF !important; - font-size: 1.25rem !important; - padding: 1rem !important; - } - - /* Focus states */ - .high-contrast *:focus { - outline: 10px solid #018737 !important; - outline-offset: 4px !important; - } - - /* Images */ - .high-contrast img { - opacity: 0.9 !important; - filter: contrast(1.2) brightness(1.1); - } - - /* Price and accent colors */ - .high-contrast .text-\[var\(--panda-red\)\] { - color: #FF1A1A !important; - font-size: 1.5rem !important; - } - - /* Spacing Adjustments for Larger Text */ - .high-contrast .space-y-4 > * + * { - margin-top: 1rem !important; - } - - .high-contrast .space-x-4 > * + * { - margin-left: 1rem !important; - } -} - -/* Maintain existing accessibility classes */ -.sr-only { - position: absolute; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - border: 0; - clip: rect(0, 0, 0, 0); - clip-path: inset(50%); - white-space: nowrap; - overflow: hidden; -} - -/* Keep existing reduced motion preference */ -@media (prefers-reduced-motion: reduce) { - .hero-video-fade { - animation: none; - } -} - - -.dropdown-menu { - z-index: 9999; - position: absolute; -} - -/* Enhanced Text Sizes without color */ -@layer base { - - - .text-lg h1 { - font-size: 8rem !important; /* 56px */ - line-height: 6rem !important; - margin-bottom: 1.5rem !important; - } - - .text-lg h2 { - font-size: 5rem !important; /* 44px */ - line-height: 4.25rem !important; - margin-bottom: 1.25rem !important; - } - - .text-lg h3 { - font-size: 3rem !important; /* 36px */ - line-height: 3.5rem !important; - margin-bottom: 1rem !important; - } - - .text-lg p { - font-size: 2.5rem !important; /* 24px */ - line-height: 2.75rem !important; - margin-bottom: 1.5rem !important; - } - - /* Button and Link Text */ - .text-lg .btn-primary, - .text-lg .btn-secondary, - .text-lg .nav-link { - font-size: 2rem !important; /* 24px */ - line-height: 2rem !important; - padding: 1rem 1.5rem !important; - } - - .text-lg nav { - height: 125px !important; /* Increase navbar height */ - padding: 0 2rem !important; /* Increase padding for more space */ - padding-top: 1rem !important; - } - - /* Input Fields */ - .text-lg input, - .text-lg select, - .text-lg textarea { - font-size: 1.5rem !important; /* 24px */ - line-height: 2rem !important; - padding: 1rem !important; - } - - /* Text Sizes */ - .text-lg .text-xl { - font-size: 1.75rem !important; /* 28px */ - line-height: 2.25rem !important; - } - - .text-lg .text-lg { - font-size: 1.5rem !important; /* 24px */ - line-height: 2rem !important; - } - - .text-lg .text-base { - font-size: 1.25rem !important; /* 20px */ - line-height: 1.75rem !important; - } - - /* Grid Adjustments */ - .text-lg .grid { - grid-template-columns: repeat(1, minmax(0, 1fr)) !important; - } - - @media (min-width: 640px) { - .text-lg .grid { - grid-template-columns: repeat(1, minmax(0, 1fr)) !important; - } - } - - @media (min-width: 768px) { - .text-lg .grid { - grid-template-columns: repeat(2, minmax(0, 1fr)) !important; - } - } - - @media (min-width: 1024px) { - .text-lg .grid { - grid-template-columns: repeat(2, minmax(0, 1fr)) !important; - } - } - - - /* Increased Grid Gap */ - .text-lg .gap-4 { - gap: 2rem !important; - } - - /* Button Sizing */ - .text-lg .btn-primary { - font-size: 2rem !important; - padding: 1rem 1.5rem !important; - line-height: 2rem !important; - font-weight: bold; - min-height: 3.5rem !important; - margin: 0.5rem !important; - } - - /* Input fields */ - .text-lg .input-field { - min-height: 3.5rem !important; - } - - .text-lg .max-w-md { - max-width: 600px !important; /* Make modal wider */ - max-height: 90vh !important; /* Set a max height for the modal (90% of viewport height) */ - overflow-y: auto !important; - } - - /* Tables */ - .text-lg .table-header { - font-size: 1.5rem !important; - } - - .text-lg .table-cell { - font-size: 1.25rem !important; - padding: 1rem !important; - } - - /* Spacing Adjustments for Larger Text */ - .text-lg .space-y-4 > * + * { - margin-top: 1rem !important; - } - - .text-lg .space-x-4 > * + * { - margin-left: 1rem !important; - } -} - - - - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\layout.tsx - -// client/src/app/layout.tsx -import type { Metadata } from 'next'; -import { Inter } from 'next/font/google'; -import { LanguageProvider } from '@/context/LanguageContext'; -import Navbar from '@/components/layout/Navbar'; -import AIChat from '@/components/chat/AIChat'; -import './globals.css'; - -const inter = Inter({ subsets: ['latin'] }); - -export const metadata: Metadata = { - title: 'Panda Express POS', - description: 'Point of Sale System for Panda Express', -}; - -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - - - -
- {children} -
- -
- - - ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\app\page.tsx - -// client/src/app/page.tsx -'use client'; - -import { useState, useEffect } from 'react'; -import Link from 'next/link'; -import Image from 'next/image'; -import { useLanguage } from '../context/LanguageContext'; -import { ArrowRight, Clock, ShoppingBag, Utensils, Search } from 'lucide-react'; -import ScreenMagnifier from '../components/ScreenMagnifier'; - -export default function HomePage() { - const { translate } = useLanguage(); - const [isClient, setIsClient] = useState(false); - const [magnifierEnabled, setMagnifierEnabled] = useState(false); - const [magnification, setMagnification] = useState(2); - - useEffect(() => { - setIsClient(true); - }, []); - - const features = [ - { - icon: Clock, - title: translate('Quick & Easy'), - description: translate('Order ahead and skip the line'), - }, - { - icon: Utensils, - title: translate('Fresh & Delicious'), - description: translate('Made fresh daily with quality ingredients'), - }, - { - icon: ShoppingBag, - title: translate('Convenient Pickup'), - description: translate('Ready when you are'), - }, - ]; - - const popularItems = [ - { - name: translate('Orange Chicken'), - image: '/images/entrees/the_original_orange_chicken.png', - price: 5.00, - }, - { - name: translate('Beijing Beef'), - image: '/images/entrees/beijing_beef.png', - price: 5.00, - }, - { - name: translate('Chow Mein'), - image: '/images/sides/chow_mein.png', - price: 5.00, - }, - ]; - - return ( -
- {/* Hero Section */} -
- {/* Video Background - Only rendered on client side */} - {isClient && ( -
- -
-
- )} - - {/* Static Background for Server - Hidden when video loads */} - {!isClient && ( -
- Background -
-
- )} - - {/* Content */} -
-

- {translate('Welcome to Panda Express')} -

-

- {translate('Experience the bold flavors of American Chinese cuisine')} -

- - {translate('Start Your Order')} - - -
-
- - - {/* New Seasonal Item: Moon Cakes Section */} -
-
-

- {translate('New Seasonal Item: Moon Cakes')} -

-
-
- {translate('Moon -
-
-

{translate('Celebrate the Mid-Autumn Festival')}

-

- {translate('Indulge in the rich and delicate flavors of traditional moon cakes, available for a limited time.')} -

- - {translate('Order Now')} - - -
-
-
-
- - {/* Features Section */} -
-
-
- {features.map((feature, index) => ( -
- -

{feature.title}

-

{feature.description}

-
- ))} -
-
-
- - {/* Popular Items Section */} -
-
-

- {translate('Popular Items')} -

-
- {popularItems.map((item, index) => ( -
-
- {item.name} -
-
-

{item.name}

-

- ${item.price.toFixed(2)} -

-
-
- ))} -
-
- - {translate('View Full Menu')} - - -
-
-
- - - {/* Accessibility Features */} -
-
- - - - {magnifierEnabled && ( -
- - setMagnification(Number(e.target.value))} - className="w-32 align-middle" - /> - {magnification}x -
- )} -
-
- - -
- ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\admin\Dashboard.tsx - -// client/src/components/admin/Dashboard.tsx -interface DashboardProps { - stats: { - totalOrders: number; - totalRevenue: number; - averageOrderValue: number; - popularItems: Array<{ - name: string; - quantity: number; - revenue: number; - }>; - }; - } - - export default function Dashboard({ stats }: DashboardProps) { - return ( -
-
-

Total Orders

-

{stats.totalOrders}

-
- -
-

Total Revenue

-

- ${stats.totalRevenue.toFixed(2)} -

-
- -
-

Average Order Value

-

- ${stats.averageOrderValue.toFixed(2)} -

-
- -
-

Most Popular Item

-

- {stats.popularItems[0]?.name || 'N/A'} -

-
- -
-
-

Popular Items

-
- - - - - - - - - - {stats.popularItems.map((item, index) => ( - - - - - - ))} - -
Item NameQuantity SoldRevenue
{item.name}{item.quantity}${item.revenue.toFixed(2)}
-
-
-
-
- ); - } - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\admin\SalesChart.tsx - -// client/src/components/admin/SalesChart.tsx -'use client'; - -import { useState, useEffect } from 'react'; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'; -import api from '@/lib/api'; - -interface SalesChartProps { - dateRange: string; -} - -interface ChartData { - date: string; - sales: number; - orders: number; -} - -export default function SalesChart({ dateRange }: SalesChartProps) { - const [data, setData] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const fetchChartData = async () => { - try { - const response = await api.get(`/admin/sales-chart?range=${dateRange}`); - setData(response.data); - } catch (error) { - console.error('Failed to fetch chart data:', error); - } finally { - setIsLoading(false); - } - }; - - fetchChartData(); - }, [dateRange]); - - if (isLoading) { - return ( -
-
-
- ); - } - - return ( -
-

Sales Overview

- - - - - - - - - - - - -
- ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\cashier\Checkout.tsx - -import React, { useState } from 'react'; -import { MenuItem, Order, OrderItem } from '@/types'; -import { PlusCircle, Receipt, CreditCard } from 'lucide-react'; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/cashier/dialog'; - - -interface CheckoutProps { - menuItems: MenuItem[]; - onCreateOrder: (order: Partial) => void; - activeOrder: Order | null; -} - -interface ComboSelection { - entrees: MenuItem[]; - side?: MenuItem; - maxEntrees: number; -} - -export default function EnhancedCheckout({ menuItems, onCreateOrder }: CheckoutProps) { - const [draftOrders, setDraftOrders] = useState>([]); - - const [activeDraftId, setActiveDraftId] = useState(null); - const [selectedTipPercent, setSelectedTipPercent] = useState(null); - const [selectedSplit, setSelectedSplit] = useState(null); // State for split bill - const [customTipAmount, setCustomTipAmount] = useState(''); - const [completedSplits, setCompletedSplits] = useState(0); // State to track completed parts of split - const [isComboModalOpen, setIsComboModalOpen] = useState(false); - const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false); - const [currentPaymentAmount, setCurrentPaymentAmount] = useState(0); - const [processingPayment, setProcessingPayment] = useState(false); - - const [currentComboSelection, setCurrentComboSelection] = useState({ - entrees: [], - maxEntrees: 1 - }); - const [selectedComboBase, setSelectedComboBase] = useState(null); - - - const currentItems = draftOrders.find(d => d.id === activeDraftId)?.items || []; - - const TAX_RATE = 0.0825; - const TIP_PERCENTAGES = [15, 18, 20]; - - const entrees = menuItems.filter(item => item.category === 'entree'); - const sides = menuItems.filter(item => item.category === 'side'); - - const getComboRequirements = (comboType: string): { maxEntrees: number, name: string } => { - switch (comboType.toLowerCase()) { - case 'bowl': - return { maxEntrees: 1, name: 'Bowl' }; - case 'plate': - return { maxEntrees: 2, name: 'Plate' }; - case 'bigger plate': - return { maxEntrees: 3, name: 'Bigger Plate' }; - default: - return { maxEntrees: 1, name: 'Combo' }; - } - }; - - - const createNewDraft = () => { - const newDraft = { - id: `draft-${Date.now()}`, - items: [], - createdAt: new Date(), - }; - - setDraftOrders(prev => [...prev, newDraft]); - setActiveDraftId(newDraft.id); - - // Reset both tip and split bill selections when a new draft is created - setSelectedTipPercent(null); - setCustomTipAmount(''); - setSelectedSplit(null); // Reset split bill on new draft - setCompletedSplits(0); // Reset completed split count - }; - - const handleComboClick = (comboItem: MenuItem) => { - const { maxEntrees } = getComboRequirements(comboItem.name); - setSelectedComboBase(comboItem); - setCurrentComboSelection({ - entrees: [], - maxEntrees - }); - setIsComboModalOpen(true); - }; - - const handleEntreeSelection = (entree: MenuItem) => { - setCurrentComboSelection(prev => { - const existingIndex = prev.entrees.findIndex(e => e.id === entree.id); - - if (existingIndex >= 0) { - // Remove the entree if it's already selected - return { - ...prev, - entrees: prev.entrees.filter((_, index) => index !== existingIndex) - }; - } - - if (prev.entrees.length >= prev.maxEntrees) { - // Remove the first entree if we're at max capacity - return { - ...prev, - entrees: [...prev.entrees.slice(1), entree] - }; - } - - // Add the new entree - return { - ...prev, - entrees: [...prev.entrees, entree] - }; - }); - }; - - - - const addComboToOrder = () => { - if (!activeDraftId || !selectedComboBase || - currentComboSelection.entrees.length !== currentComboSelection.maxEntrees || - !currentComboSelection.side) return; - - const entreeNames = currentComboSelection.entrees.map(e => e.name).join(', '); - const comboName = `${selectedComboBase.name} (${entreeNames}, ${currentComboSelection.side.name})`; - - setDraftOrders(prev => prev.map(draft => { - if (draft.id !== activeDraftId) return draft; - - return { - ...draft, - items: [...draft.items, { - menuItemId: selectedComboBase.id, - name: comboName, - quantity: 1, - price: selectedComboBase.price - }] - }; - })); - - setIsComboModalOpen(false); - setCurrentComboSelection({ entrees: [], maxEntrees: 1 }); - setSelectedComboBase(null); - }; - - - - - const addItem = (menuItem: MenuItem) => { - if (menuItem.category === 'combo') { - handleComboClick(menuItem); - return; - } - - if (!activeDraftId) { - const newDraft = { - id: `draft-${Date.now()}`, - items: [{ - menuItemId: menuItem.id, - name: menuItem.name, - quantity: 1, - price: menuItem.price - }], - createdAt: new Date(), - }; - setDraftOrders([newDraft]); - setActiveDraftId(newDraft.id); - return; - } - - - setDraftOrders(prev => prev.map(draft => { - if (draft.id !== activeDraftId) return draft; - - const existingItem = draft.items.find(item => item.menuItemId === menuItem.id); - if (existingItem) { - return { - ...draft, - items: draft.items.map(item => - item.menuItemId === menuItem.id - ? { ...item, quantity: item.quantity + 1 } - : item - ) - }; - } else { - return { - ...draft, - items: [...draft.items, { - menuItemId: menuItem.id, - name: menuItem.name, - quantity: 1, - price: menuItem.price - }] - }; - } - })); - }; - - const updateQuantity = (menuItemId: string, quantity: number) => { - setDraftOrders(prev => prev.map(draft => { - if (draft.id !== activeDraftId) return draft; - - if (quantity < 1) { - return { - ...draft, - items: draft.items.filter(item => item.menuItemId !== menuItemId) - }; - } - - return { - ...draft, - items: draft.items.map(item => - item.menuItemId === menuItemId - ? { ...item, quantity } - : item - ) - }; - })); - }; - - const handleTipPercentSelect = (percent: number) => { - setSelectedTipPercent(percent); - setCustomTipAmount(''); // Reset custom tip amount when a percentage is selected - }; - - const handleCustomTipChange = (value: string) => { - setCustomTipAmount(value); - setSelectedTipPercent(null); // Clear percentage tip when custom tip is entered - }; - - const handleSplitSelect = (num: number) => { - setSelectedSplit(num); // Set selected split bill option - setCompletedSplits(0); // Reset completed split count when split selection changes - }; - - const calculateTotals = (items: OrderItem[]) => { - const subtotal = items.reduce( - (sum, item) => sum + item.price * item.quantity, - 0 - ); - const tax = subtotal * TAX_RATE; - - let tipAmount = 0; - if (selectedTipPercent) { - tipAmount = (subtotal * selectedTipPercent) / 100; - } else if (customTipAmount) { - tipAmount = parseFloat(customTipAmount) || 0; - } - - const total = subtotal + tax + tipAmount; - return { subtotal, tax, tipAmount, total }; - }; - - const handleCheckout = (index: number) => { - const { total } = calculateTotals(currentItems); - const splitAmount = selectedSplit ? total / selectedSplit : total; - setCurrentPaymentAmount(splitAmount); - setIsPaymentModalOpen(true); - }; - - const processPayment = async () => { - setProcessingPayment(true); - - try { - // Calculate all totals - const { subtotal, tax, tipAmount, total } = calculateTotals(currentItems); - - // Format items with required category information - const formattedItems = currentItems.map(item => { - let category; - // Determine category based on item type or ID prefixes - if (item.menuItemId.startsWith('drink')) { - category = 'drink'; - } else if (item.menuItemId.startsWith('app')) { - category = 'appetizer'; - } else if (item.menuItemId.startsWith('side')) { - category = 'side'; - } else { - category = 'entree'; - } - - return { - ...item, - category - }; - }); - - // Prepare order data - const orderData = { - items: formattedItems, - subtotal, - tax, - tip: tipAmount, - total, - status: 'pending' - }; - - // Send order to backend - const response = await fetch('/api/orders', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(orderData), - }); - - if (!response.ok) { - throw new Error('Failed to create order'); - } - - // Handle successful order - const result = await response.json(); - - if (result.success) { - // Clear current order - setDraftOrders(prev => prev.filter(d => d.id !== activeDraftId)); - setActiveDraftId(null); - - // Reset payment-related states - setSelectedTipPercent(null); - setCustomTipAmount(''); - setSelectedSplit(null); - setCompletedSplits(0); - - // Show success message or trigger receipt print - // You can add success notification here - } - - } catch (error) { - console.error('Payment processing error:', error); - // Show error message to user - } finally { - setProcessingPayment(false); - setIsPaymentModalOpen(false); - } -}; - - - - return ( -
-
- - {draftOrders.map((draft) => ( - - ))} -
- -
-
-

Menu Items

-
- {menuItems.map((item) => ( - - ))} -
-
- -
-

- {activeDraftId ? 'Current Order' : 'No Active Order'} -

- {currentItems.length > 0 ? ( - <> -
- {currentItems.map((item) => ( -
-
-

{item.name}

-

- ${item.price} each -

-
-
- - {item.quantity} - -
-
- ))} -
- -
-

Add Tip

-
- {TIP_PERCENTAGES.map((percent) => ( - - ))} -
-
- $ - handleCustomTipChange(e.target.value)} - placeholder="Custom amount" - className="w-full p-2 border rounded-lg text-sm" - min="0" - step="0.01" - /> -
-
- -
-

Split Bill

-
- {[1, 2, 3, 4].map((num) => ( - - ))} -
-
- -
-
- Subtotal - ${calculateTotals(currentItems).subtotal.toFixed(2)} -
-
- Tax - ${calculateTotals(currentItems).tax.toFixed(2)} -
-
- Tip - ${calculateTotals(currentItems).tipAmount.toFixed(2)} -
-
- Total - ${calculateTotals(currentItems).total.toFixed(2)} -
- -
- {Array(selectedSplit || 1).fill(null).map((_, index) => ( - - ))} -
-
- - ) : ( -

- {activeDraftId ? 'No items selected' : 'Select "New Order" to begin'} -

- )} -
-
- - - - - Customize Your {selectedComboBase?.name} - - -
-
-

- Select {currentComboSelection.maxEntrees > 1 ? `Entrees (${currentComboSelection.entrees.length}/${currentComboSelection.maxEntrees})` : 'Entree'} -

-
- {entrees.map((entree) => ( - - ))} -
-
- -
-

Select Side

-
- {sides.map((side) => ( - - ))} -
-
- - -
-
-
- - - - - Process Payment - - -
-
- -

- ${currentPaymentAmount.toFixed(2)} -

- {selectedSplit && selectedSplit > 1 && ( -

- Split {completedSplits + 1} of {selectedSplit} -

- )} -
- -
- - - -
-
-
-
- -
- ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\cashier\dialog.tsx - -"use client" - -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" - -import { cn } from "@/components/cashier/utils" - -const Dialog = DialogPrimitive.Root - -const DialogTrigger = DialogPrimitive.Trigger - -const DialogPortal = DialogPrimitive.Portal - -const DialogClose = DialogPrimitive.Close - -const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName - -const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - {children} - - - Close - - - -)) -DialogContent.displayName = DialogPrimitive.Content.displayName - -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -DialogHeader.displayName = "DialogHeader" - -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -DialogFooter.displayName = "DialogFooter" - -const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName - -const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName - -export { - Dialog, - DialogPortal, - DialogOverlay, - DialogClose, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\cashier\input.tsx - -import * as React from "react" - -import { cn } from "@/components/cashier/utils" - -export interface InputProps - extends React.InputHTMLAttributes {} - -const Input = React.forwardRef( - ({ className, type, ...props }, ref) => { - return ( - - ) - } -) -Input.displayName = "Input" - -export { Input } - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\cashier\OrderList.tsx - -// client/src/components/cashier/OrderList.tsx -import { Order } from '@/types'; -import { format } from 'date-fns'; - -interface OrderListProps { - orders: Order[]; - onUpdateStatus: (orderId: string, status: Order['status']) => void; - onSelectOrder: (order: Order) => void; -} - -export default function OrderList({ orders, onUpdateStatus, onSelectOrder }: OrderListProps) { - const getStatusColor = (status: Order['status']) => { - const colors = { - pending: 'bg-yellow-100 text-yellow-800', - preparing: 'bg-blue-100 text-blue-800', - ready: 'bg-green-100 text-green-800', - completed: 'bg-gray-100 text-gray-800', - cancelled: 'bg-red-100 text-red-800' - }; - return colors[status]; - }; - - const getNextStatus = (currentStatus: Order['status']) => { - const statusFlow = { - pending: 'preparing', - preparing: 'ready', - ready: 'completed' - } as const; - - return statusFlow[currentStatus as keyof typeof statusFlow] || null; - }; - - return ( -
-

Active Orders

-
- {orders - .filter(order => order.status !== 'completed' && order.status !== 'cancelled') - .map((order) => ( -
-
-

Order #{order.orderNumber}

- - {order.status} - -
- -
- {order.items.map((item, index) => ( -
- {item.quantity}x {item.name} - ${(item.price * item.quantity).toFixed(2)} -
- ))} -
-
- Subtotal - ${order.subtotal.toFixed(2)} -
-
- Tax - ${order.tax.toFixed(2)} -
-
- Total - ${order.total.toFixed(2)} -
-
-
- -
- Created: {format(new Date(order.createdAt), 'MMM d, h:mm a')} -
- -
- - {getNextStatus(order.status) && ( - - )} -
- - {order.status === 'pending' && ( - - )} -
- ))} -
- - {orders.filter(order => order.status !== 'completed' && order.status !== 'cancelled').length === 0 && ( -
- No active orders -
- )} -
- ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\cashier\utils.tsx - -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\chat\AIChat.tsx - -'use client'; - -import { useState, useRef, useEffect } from 'react'; -import { MessageSquare, X, Send, Loader } from 'lucide-react'; -import { useLanguage } from '@/context/LanguageContext'; - -interface Message { - role: 'user' | 'assistant'; - content: string; - timestamp: Date; -} - -export default function AIChat() { - const [isOpen, setIsOpen] = useState(false); - const [messages, setMessages] = useState([ - { - role: 'assistant', - content: 'Hello! I can help you with anything about our menu, ingredients, or recommendations!', - timestamp: new Date(), - }, - ]); - const [inputMessage, setInputMessage] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const messagesEndRef = useRef(null); - const { translate } = useLanguage(); - - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }; - - useEffect(() => { - scrollToBottom(); - }, [messages]); - - const handleSend = async () => { - if (!inputMessage.trim()) return; - - const userMessage: Message = { - role: 'user', - content: inputMessage, - timestamp: new Date(), - }; - - setMessages(prev => [...prev, userMessage]); - setInputMessage(''); - setIsLoading(true); - - try { - const response = await fetch('http://127.0.0.1:5000/generate', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ prompt: inputMessage }), - }); - - const data = await response.json(); - - const assistantMessage: Message = { - role: 'assistant', - content: data.response || 'I don’t know', - timestamp: new Date(), - }; - - setMessages(prev => [...prev, assistantMessage]); - } catch (error) { - const errorMessage: Message = { - role: 'assistant', - content: 'Sorry, I encountered an error. Please try again.', - timestamp: new Date(), - }; - setMessages(prev => [...prev, errorMessage]); - } finally { - setIsLoading(false); - } - }; - - return ( -
- - - {isOpen && ( -
-
-

Panda Express Assistant

- -
- -
- {messages.map((message, index) => ( -
-
-

{message.content}

- - {new Date(message.timestamp).toLocaleTimeString()} - -
-
- ))} - {isLoading && ( -
-
- -
-
- )} -
-
- -
-
- setInputMessage(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && handleSend()} - placeholder={translate('Ask me anything about our menu...')} - className="flex-1 border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-[var(--panda-red)]" - /> - -
-
-
- )} -
- ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\chat\route.js - -const express = require('express'); -const { ChatOpenAI } = require('@langchain/openai'); -const { OpenAIEmbeddings } = require('@langchain/embeddings'); -const { FAISS } = require('@langchain/vectorstores'); -const pdfParse = require('pdf-parse'); -const fs = require('fs'); -const path = require('path'); -const dotenv = require('dotenv'); -const cors = require('cors'); - -dotenv.config(); - -const apiKey = process.env.OPENAI_API_KEY; -if (!apiKey) { - throw new Error('Missing OpenAI API Key'); -} - -const app = express(); -app.use(cors()); -app.use(express.json()); - -const llm = new ChatOpenAI({ - openAIApiKey: apiKey, - modelName: 'gpt-3.5-turbo', - temperature: 0.7, -}); - -let vectorStore; - -// Function to load and embed documents -async function loadDocumentsAndCreateStore() { - const docs = []; - const directory = path.join(__dirname, 'documents'); - - if (!fs.existsSync(directory)) { - console.error('Documents directory does not exist:', directory); - return; - } - - const files = fs.readdirSync(directory); - for (const file of files) { - if (file.endsWith('.pdf')) { - const filePath = path.join(directory, file); - try { - const dataBuffer = fs.readFileSync(filePath); - const pdfData = await pdfParse(dataBuffer); - docs.push(pdfData.text); - console.log(`Loaded document: ${file}`); - } catch (error) { - console.error(`Error loading document ${file}:`, error); - } - } - } - - if (docs.length === 0) { - console.error('No documents found to embed.'); - return; - } - - const embeddings = new OpenAIEmbeddings({ openAIApiKey: apiKey }); - vectorStore = await FAISS.fromTexts(docs, embeddings); - console.log('Vector store created successfully'); -} - -// Function to retrieve the most relevant document chunk -async function retrieveRelevantDocument(query) { - if (!vectorStore) { - console.error('Vector store is not initialized'); - return null; - } - - const results = await vectorStore.similaritySearch(query, 3); // Get top 3 relevant chunks - return results.map(result => result.text).join('\n'); -} - -// Function to get a response from OpenAI using the retrieved documents -async function getResponse(prompt) { - const context = await retrieveRelevantDocument(prompt); - - if (!context) { - return 'I don’t know'; - } - - const augmentedPrompt = `Based on the following information, answer the query:\n\n${context}\n\nQuery: ${prompt}`; - try { - const response = await llm.call(augmentedPrompt); - return response.trim(); - } catch (error) { - console.error('Error getting response from OpenAI:', error); - return 'I don’t know'; - } -} - -// Endpoint to handle user queries -app.post('/generate', async (req, res) => { - const { prompt } = req.body; - if (!prompt) { - return res.status(400).json({ error: 'Prompt is required' }); - } - - try { - const response = await getResponse(prompt); - return res.json({ response }); - } catch (error) { - console.error('Error:', error.message); - return res.status(500).json({ error: error.message }); - } -}); - -// Load documents and create vector store on startup -(async () => { - await loadDocumentsAndCreateStore(); -})(); - -const PORT = process.env.PORT || 5001; -app.listen(PORT, () => { - console.log(`Server is running on http://127.0.0.1:${PORT}`); -}); - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\layout\LanguageSelector.tsx - -import React from 'react'; -import { Globe } from 'lucide-react'; -import GoogleTranslate from '../GoogleTranslate'; - -export default function LanguageSelector() { - return ( -
- - -
- ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\layout\LoginButton.tsx - -// client/src/components/layout/LoginButton.tsx -'use client'; - -import { useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { UserCircle, LogOut } from 'lucide-react'; - -export default function LoginButton() { - const router = useRouter(); - const [isOpen, setIsOpen] = useState(false); - - // In a real app, this would check a global auth state - const isAuthenticated = typeof window !== 'undefined' && localStorage.getItem('token'); - - const handleLogout = () => { - localStorage.removeItem('token'); - router.push('/'); - setIsOpen(false); - }; - - return ( -
- {isAuthenticated ? ( - <> - - - {isOpen && ( -
-
- -
-
- )} - - ) : ( - - )} -
- ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\layout\Navbar.tsx - -'use client'; - -import Link from 'next/link'; -import Image from 'next/image'; -import { usePathname } from 'next/navigation'; -import { useState, useEffect } from 'react'; -import LoginButton from './LoginButton'; -import Weather from './Weather'; -import LanguageSelector from './LanguageSelector'; -import { useLanguage } from '@/context/LanguageContext'; -import { Users, Calculator, ChevronDown } from 'lucide-react'; // Import icons -import { Search } from 'lucide-react'; // Import the magnifier icon -import ScreenMagnifier from '../../components/ScreenMagnifier'; - - -export default function Navbar() { - const pathname = usePathname(); - const { translate } = useLanguage(); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [magnifierEnabled, setMagnifierEnabled] = useState(false); - const [magnification, setMagnification] = useState(1.5); - const [isAccessibilityDropdownOpen, setIsAccessibilityDropdownOpen] = useState(false); - const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 }); - const [selectedOption, setSelectedOption] = useState(null); - - - const handleMouseMove = (event: React.MouseEvent) => { - setMousePosition({ - x: event.clientX, - y: event.clientY, - }); - }; - - useEffect(() => { - if (magnifierEnabled) { - // Apply the scale transformation based on the magnification value - document.body.style.transform = `scale(${magnification})` - document.body.style.transformOrigin = 'top left'; // Keep zoom anchored at the top left - document.body.style.transition = 'transform 0.3s ease'; // Smooth zoom transition - } else { - // Reset the transform when magnifier is disabled - document.body.style.transform = 'none'; - } - }, [magnifierEnabled, magnification]); - - - // Function to toggle high contrast mode - const toggleHighContrastMode = () => { - document.documentElement.classList.toggle('high-contrast'); - }; - - // Function to toggle enlarged text - const toggleEnlargedText = () => { - document.documentElement.classList.toggle('text-lg'); - }; - - // Function to enable all accessibility features - const enableAllAccessibilityFeatures = () => { - toggleHighContrastMode(); - toggleEnlargedText(); - - }; - - const toggleAccessibilityDropdown = () => { - setIsAccessibilityDropdownOpen(!isAccessibilityDropdownOpen); - }; - - - return ( - - ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\layout\Weather.tsx - -// client/src/components/layout/Weather.tsx -'use client'; - -import { useEffect, useState } from 'react'; -import { Cloud, Sun, CloudRain, Loader, MapPin, Clock } from 'lucide-react'; - -interface WeatherData { - main: { - temp: number; - }; - weather: Array<{ - main: string; - description: string; - }>; -} - -export default function Weather() { - const [weather, setWeather] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [currentTime, setCurrentTime] = useState(new Date()); - - // College Station coordinates - const COLLEGE_STATION = { - lat: 30.6280, - lon: -96.3344, - name: 'College Station' - }; - - useEffect(() => { - // Update time every second - const timeInterval = setInterval(() => { - setCurrentTime(new Date()); - }, 1000); - - const fetchWeather = async () => { - console.log('API Key:', process.env.NEXT_PUBLIC_OPENWEATHER_API_KEY); - - if (!process.env.NEXT_PUBLIC_OPENWEATHER_API_KEY) { - console.error('OpenWeather API key is missing'); - setError('API key not configured'); - setIsLoading(false); - return; - } - - try { - const url = `https://api.openweathermap.org/data/2.5/weather?lat=${COLLEGE_STATION.lat}&lon=${COLLEGE_STATION.lon}&appid=${process.env.NEXT_PUBLIC_OPENWEATHER_API_KEY}&units=imperial`; - console.log('Fetching weather from:', url); - - const response = await fetch(url); - console.log('Response status:', response.status); - - if (!response.ok) { - throw new Error(`Failed to fetch weather: ${response.status}`); - } - - const data = await response.json(); - console.log('Weather data:', data); - setWeather(data); - } catch (err) { - console.error('Weather fetch error:', err); - setError(err instanceof Error ? err.message : 'Unable to load weather'); - } finally { - setIsLoading(false); - } - }; - - fetchWeather(); - // Refresh weather every 5 minutes - const weatherInterval = setInterval(fetchWeather, 5 * 60 * 1000); - - return () => { - clearInterval(weatherInterval); - clearInterval(timeInterval); - }; - }, []); - - const getWeatherIcon = (weatherMain: string) => { - const mainWeather = weatherMain.toLowerCase(); - console.log('Weather condition:', mainWeather); - - switch (mainWeather) { - case 'clear': - return ; - case 'clouds': - return ; - case 'rain': - case 'drizzle': - return ; - default: - return ; - } - }; - - const formatTime = (date: Date) => { - return date.toLocaleTimeString('en-US', { - hour: 'numeric', - minute: '2-digit', - hour12: true - }); - }; - - if (isLoading) { - return ( -
- - Loading weather... -
- ); - } - - if (error || !weather) { - return ( -
- Weather unavailable: {error} -
- ); - } - - return ( -
- {/* Location Row */} -
- - {COLLEGE_STATION.name} -
- - {/* Weather and Time Row */} -
- {/* Weather Information */} -
- {getWeatherIcon(weather.weather[0].main)} -
-
- {Math.round(weather.main.temp)}°F -
-
- {weather.weather[0].description} -
-
-
- - {/* Time Information */} -
- - - {formatTime(currentTime)} - -
-
-
- ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\menu\MenuGrid.tsx - -// MenuGrid.tsx -import React from 'react'; -import { Info } from 'lucide-react'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/cashier/dialog" - -interface MenuItem { - id: string; - name: string; - imageUrl: string; - price: number; - description: string; - available: boolean; - category?: string; // Added to match the data structure -} - -interface NutritionInfo { - servingSize: string; - calories: number; - caloriesFromFat: number; - totalFat: number; - saturatedFat: number; - transFat: number; - cholesterol: number; - sodium: number; - totalCarbs: number; - dietaryFiber: number; - sugars: number; - protein: number; - allergens?: string[]; // Optional property for allergens -} - -// Mapping between numerical IDs and nutrition data keys -const nutritionIdMap: { [key: string]: string } = { - '1': 'the-original-orange-chicken', - '2': 'beijing-beef', - '3': 'black-pepper-chicken', - '4': 'black-pepper-sirloin-steak', - '5': 'broccoli-beef', - '7': 'grilled-teriyaki-chicken', - '8': 'honey-sesame-chicken', - '9': 'honey-walnut-shrimp', - '10': 'hot-ones-blazing-bourbon-chicken', // Fixed the key to match the nutritionData - '11': 'kung-pao-chicken', - '12': 'mushroom-chicken', - '13': 'string-bean-chicken', - '14': 'sweetfire-chicken', - '15': 'chow-mein', - '16': 'fried-rice', - '17': 'super-greens', - '18': 'white-steamed-rice', - '19': 'apple-pie-roll', - '20': 'chicken-egg-roll', - '21': 'cream-cheese-rangoon', - '22': 'veggie-egg-roll', - '23': 'barqs-root-beer', - '24': 'coca-cola', - '25': 'coke-mexico-12oz-bottle', - '26': 'coke-zero-20oz-bottle', - '27': 'dasani-16oz-bottle', - '28': 'diet-coke', - '29': 'dr-pepper', - '30': 'fanta-orange', - '31': 'fuze-raspberry-iced-tea', - '32': 'minute-maid-apple-juice-12oz-bottle', - '33': 'minute-maid-lemonade', - '34': 'mango-guava-flavored-tea', - '35': 'peach-lychee-flavored-refresher', - '36': 'pomegranate-pineapple-flavored-lemonade', - '37': 'smartwater-700ml-bottle', - '38': 'sprite', - '39': 'watermelon-mango-flavored-refresher', - '41': 'powerade-mountain-berry-blast', - '42': 'coca-cola-cherry' -}; - - -const nutritionData: { [key: string]: NutritionInfo } = { - 'the-original-orange-chicken': { - servingSize: '5.7 oz', - calories: 420, - caloriesFromFat: 180, - totalFat: 21, - saturatedFat: 4, - transFat: 0, - cholesterol: 95, - sodium: 620, - totalCarbs: 43, - dietaryFiber: 0, - sugars: 18, - protein: 15, - allergens: ['Wheat', 'Soy', 'Eggs', 'Milkyhg'] - }, - 'beijing-beef': { - servingSize: '5.6 oz', - calories: 690, - caloriesFromFat: 360, - totalFat: 40, - saturatedFat: 8, - transFat: 0.5, - cholesterol: 65, - sodium: 890, - totalCarbs: 57, - dietaryFiber: 4, - sugars: 25, - protein: 26, - allergens: ['Wheat', 'Soy', 'Milk'] - }, - 'broccoli-beef': { - servingSize: '5.4 oz', - calories: 130, - caloriesFromFat: 40, - totalFat: 4, - saturatedFat: 1, - transFat: 0, - cholesterol: 15, - sodium: 710, - totalCarbs: 13, - dietaryFiber: 3, - sugars: 3, - protein: 10, - allergens: ['Wheat', 'Soy'] - }, - 'black-pepper-chicken': { - servingSize: '6.1 oz', - calories: 250, - caloriesFromFat: 130, - totalFat: 14, - saturatedFat: 3, - transFat: 0, - cholesterol: 120, - sodium: 930, - totalCarbs: 12, - dietaryFiber: 2, - sugars: 5, - protein: 19, - allergens: ['Wheat', 'Soy'] - }, - 'black-pepper-sirloin-steak': { - servingSize: '6.1 oz', - calories: 210, - caloriesFromFat: 90, - totalFat: 10, - saturatedFat: 2, - transFat: 0, - cholesterol: 65, - sodium: 650, - totalCarbs: 13, - dietaryFiber: 1, - sugars: 2, - protein: 19, - allergens: ['Wheat', 'Soy'] -}, - 'grilled-teriyaki-chicken': { - servingSize: '6.3 oz', - calories: 275, - caloriesFromFat: 90, - totalFat: 10, - saturatedFat: 2, - transFat: 0, - cholesterol: 85, - sodium: 680, - totalCarbs: 14, - dietaryFiber: 0, - sugars: 8, - protein: 33, - allergens: ['Wheat', 'Soy'] - }, - 'honey-sesame-chicken': { - servingSize: '5.7 oz', // Adjust as needed - calories: 340, - caloriesFromFat: 135, - totalFat: 15, - saturatedFat: 3, - transFat: 0, - cholesterol: 75, - sodium: 620, - totalCarbs: 35, - dietaryFiber: 2, - sugars: 16, - protein: 16, - allergens: ['Wheat', 'Soy'] - }, - 'honey-walnut-shrimp': { - servingSize: '5.4 oz', - calories: 360, - caloriesFromFat: 216, - totalFat: 24, - saturatedFat: 3, - transFat: 0, - cholesterol: 90, - sodium: 500, - totalCarbs: 27, - dietaryFiber: 1, - sugars: 15, - protein: 11, - allergens: ['Wheat', 'Soy', 'Treenuts'] - }, - 'hot-ones-blazing-bourbon-chicken': { - servingSize: '5.9 oz', // Adjust as needed - calories: 300, - caloriesFromFat: 90, - totalFat: 10, // Approximate value based on saturated fat - saturatedFat: 2, - transFat: 0, - cholesterol: 75, - sodium: 720, - totalCarbs: 37, - dietaryFiber: 2, - sugars: 12, - protein: 15, - allergens: ['Wheat', 'Soy'] - }, - 'kung-pao-chicken': { - servingSize: '5.6 oz', // Adjust as needed - calories: 290, - caloriesFromFat: 171, - totalFat: 19, - saturatedFat: 3.5, - transFat: 0, - cholesterol: 70, - sodium: 970, - totalCarbs: 14, - dietaryFiber: 2, - sugars: 6, - protein: 16, - allergens: ['Wheat', 'Soy', 'Peanuts'] - }, - 'mushroom-chicken': { - servingSize: '5.9 oz', - calories: 220, - caloriesFromFat: 120, - totalFat: 13, - saturatedFat: 3, - transFat: 0, - cholesterol: 100, - sodium: 760, - totalCarbs: 9, - dietaryFiber: 1, - sugars: 4, - protein: 17, - allergens: ['Wheat', 'Soy'] - }, - 'string-bean-chicken': { - servingSize: '5.6 oz', - calories: 170, - caloriesFromFat: 60, - totalFat: 7, - saturatedFat: 1.5, - transFat: 0, - cholesterol: 35, - sodium: 740, - totalCarbs: 13, - dietaryFiber: 2, - sugars: 5, - protein: 15, - allergens: ['Wheat', 'Soy'] - }, - 'sweetfire-chicken': { - servingSize: '5.8 oz', - calories: 440, - caloriesFromFat: 160, - totalFat: 18, - saturatedFat: 3.5, - transFat: 0, - cholesterol: 45, - sodium: 370, - totalCarbs: 53, - dietaryFiber: 1, - sugars: 27, - protein: 17, - allergens: ['Wheat', 'Soy'] - }, - 'chow-mein': { - servingSize: '9.4 oz', - calories: 500, - caloriesFromFat: 210, - totalFat: 23, - saturatedFat: 4, - transFat: 0, - cholesterol: 0, - sodium: 980, - totalCarbs: 61, - dietaryFiber: 4, - sugars: 5, - protein: 18, - allergens: ['Wheat', 'Soy'] - }, - 'fried-rice': { - servingSize: '9.3 oz', - calories: 530, - caloriesFromFat: 140, - totalFat: 16, - saturatedFat: 3, - transFat: 0, - cholesterol: 150, - sodium: 820, - totalCarbs: 82, - dietaryFiber: 1, - sugars: 3, - protein: 12, - allergens: ['Wheat', 'Soy', 'Eggs'] - }, - 'super-greens': { - servingSize: '7 oz', - calories: 90, - caloriesFromFat: 27, - totalFat: 3, - saturatedFat: 0.5, - transFat: 0, - cholesterol: 0, - sodium: 300, - totalCarbs: 10, - dietaryFiber: 4, - sugars: 3, - protein: 6, - allergens: ['Wheat', 'Soy'] - }, - 'white-steamed-rice': { - servingSize: '8.1 oz', - calories: 380, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 0, - totalCarbs: 86, - dietaryFiber: 0, - sugars: 0, - protein: 7 - }, - 'apple-pie-roll': { - servingSize: '1.94 oz', - calories: 150, - caloriesFromFat: 27, - totalFat: 3, - saturatedFat: 1, - transFat: 0, - cholesterol: 0, - sodium: 90, - totalCarbs: 30, - dietaryFiber: 1, - sugars: 13, - protein: 2, - allergens: ['Wheat', 'Soy', 'Milk'] - }, - 'chicken-egg-roll': { - servingSize: '3.0 oz / 1 roll', - calories: 200, - caloriesFromFat: 100, - totalFat: 12, - saturatedFat: 4, - transFat: 0, - cholesterol: 20, - sodium: 390, - totalCarbs: 16, - dietaryFiber: 2, - sugars: 2, - protein: 8, - allergens: ['Wheat', 'Soy'] - }, - 'cream-cheese-rangoon': { - servingSize: '2.4 oz / 3 pcs', - calories: 190, - caloriesFromFat: 70, - totalFat: 8, - saturatedFat: 5, - transFat: 0, - cholesterol: 35, - sodium: 180, - totalCarbs: 24, - dietaryFiber: 2, - sugars: 1, - protein: 5, - allergens: ['Wheat', 'Eggs', 'Milk'] - }, - 'veggie-egg-roll': { - servingSize: '3.4 oz / 2 rolls', - calories: 160, - caloriesFromFat: 60, - totalFat: 7, - saturatedFat: 1, - transFat: 0, - cholesterol: 0, - sodium: 540, - totalCarbs: 22, - dietaryFiber: 4, - sugars: 2, - protein: 2, - allergens: ['Wheat', 'Soy', 'Milk'] - }, - 'barqs-root-beer': { - servingSize: '12 fl oz', - calories: 160, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 65, - totalCarbs: 44, - dietaryFiber: 0, - sugars: 44, - protein: 0 - }, - 'coca-cola': { - servingSize: '12 fl oz', - calories: 140, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 45, - totalCarbs: 39, - dietaryFiber: 0, - sugars: 39, - protein: 0 - }, - 'coca-cola-cherry': { - servingSize: '12 fl oz', - calories: 150, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 40, - totalCarbs: 42, - dietaryFiber: 0, - sugars: 42, - protein: 0 - }, - 'coke-zero-20oz-bottle': { - servingSize: '12 fl oz', - calories: 0, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 40, - totalCarbs: 0, - dietaryFiber: 0, - sugars: 0, - protein: 0 - }, - 'dasani-16oz-bottle': { - servingSize: '16.9 fl oz', - calories: 0, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 0, - totalCarbs: 0, - dietaryFiber: 0, - sugars: 0, - protein: 0 - }, - 'diet-coke': { - servingSize: '12 fl oz', - calories: 0, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 40, - totalCarbs: 0, - dietaryFiber: 0, - sugars: 0, - protein: 0 - }, - 'dr-pepper': { - servingSize: '12 fl oz', - calories: 150, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 55, - totalCarbs: 40, - dietaryFiber: 0, - sugars: 40, - protein: 0 - }, - 'fanta-orange': { - servingSize: '12 fl oz', - calories: 160, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 55, - totalCarbs: 44, - dietaryFiber: 0, - sugars: 44, - protein: 0 - }, - 'fuze-raspberry-iced-tea': { - servingSize: '16.9 fl oz', - calories: 100, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 10, - totalCarbs: 27, - dietaryFiber: 0, - sugars: 27, - protein: 0 - }, - 'minute-maid-apple-juice-12oz-bottle': { - servingSize: '10 fl oz', - calories: 140, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 15, - totalCarbs: 35, - dietaryFiber: 0, - sugars: 34, - protein: 0 - }, - 'minute-maid-lemonade': { - servingSize: '12 fl oz', - calories: 150, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 15, - totalCarbs: 39, - dietaryFiber: 0, - sugars: 38, - protein: 0 - }, - 'mango-guava-flavored-tea': { - servingSize: '16 fl oz', - calories: 120, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 10, - totalCarbs: 30, - dietaryFiber: 0, - sugars: 28, - protein: 0 - }, - 'peach-lychee-flavored-refresher': { - servingSize: '16 fl oz', - calories: 140, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 20, - totalCarbs: 35, - dietaryFiber: 0, - sugars: 33, - protein: 0 - }, - 'pomegranate-pineapple-flavored-lemonade': { - servingSize: '16 fl oz', - calories: 150, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 15, - totalCarbs: 39, - dietaryFiber: 0, - sugars: 38, - protein: 0 - }, - 'smartwater-700ml-bottle': { - servingSize: '16.9 fl oz', - calories: 0, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 0, - totalCarbs: 0, - dietaryFiber: 0, - sugars: 0, - protein: 0 - }, - 'sprite': { - servingSize: '12 fl oz', - calories: 140, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 65, - totalCarbs: 38, - dietaryFiber: 0, - sugars: 38, - protein: 0 - }, - 'watermelon-mango-flavored-refresher': { - servingSize: '16 fl oz', - calories: 140, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 20, - totalCarbs: 36, - dietaryFiber: 0, - sugars: 35, - protein: 0 - }, - 'powerade-mountain-berry-blast': { - servingSize: '12 fl oz', - calories: 80, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 150, // Typical for electrolyte drinks - totalCarbs: 21, - dietaryFiber: 0, - sugars: 21, - protein: 0 -}, -'coke-mexico-12oz-bottle': { - servingSize: '12 fl oz', - calories: 150, - caloriesFromFat: 0, - totalFat: 0, - saturatedFat: 0, - transFat: 0, - cholesterol: 0, - sodium: 85, - totalCarbs: 39, - dietaryFiber: 0, - sugars: 39, - protein: 0 -} -}; - -const normalizeName = (name: string): string => { - return name.toLowerCase().replace(/ /g, '-').replace(/[^a-z0-9-]/g, ''); -}; - -const NutritionPanel = ({ itemName }: { itemName: string }) => { - console.log("NutritionPanel itemName:", itemName); - - const nutritionKey = normalizeName(itemName); - console.log("Mapped nutritionKey:", nutritionKey); - - const nutrition = nutritionData[nutritionKey]; - console.log("Nutrition data accessed:", nutrition); - - if (!nutrition) { - console.error("Nutrition information not found for key:", nutritionKey); - return ( -
-

Nutrition information not available

-

Please contact support if this is an error.

-
- ); - } - - - const nutritionItems = [ - { label: 'Serving Size', value: nutrition.servingSize, unit: '' }, - { label: 'Calories', value: nutrition.calories, unit: 'kcal' }, - { label: 'Total Fat', value: nutrition.totalFat, unit: 'g' }, - { label: 'Saturated Fat', value: nutrition.saturatedFat, unit: 'g' }, - { label: 'Trans Fat', value: nutrition.transFat, unit: 'g' }, - { label: 'Cholesterol', value: nutrition.cholesterol, unit: 'mg' }, - { label: 'Sodium', value: nutrition.sodium, unit: 'mg' }, - { label: 'Total Carbs', value: nutrition.totalCarbs, unit: 'g' }, - { label: 'Dietary Fiber', value: nutrition.dietaryFiber, unit: 'g' }, - { label: 'Sugars', value: nutrition.sugars, unit: 'g' }, - { label: 'Protein', value: nutrition.protein, unit: 'g' } - ]; - - return ( -
- {nutritionItems.map((item) => ( -
-
{item.label}
-
- {item.value} - {item.unit} -
-
- ))} - - {nutrition.allergens && ( -
-

Allergen Warning: {nutrition.allergens.join(', ')}

-
- )} - -
- * Percent Daily Values are based on a 2,000 calorie diet. -
-
- ); -}; - - - -const MenuGrid = ({ items }: { items: MenuItem[] }) => { - const itemsWithoutInfoButton = ['41', '42', '43']; - return ( -
- {items.map((item) => ( -
- {!itemsWithoutInfoButton.includes(item.id) && ( -
- - - - - - - Nutrition Information - {item.name} - - - - -
- )} - - -
- {item.name} -
- -
-
-

{item.name}

- - ${Number(item.price).toFixed(2)} - -
- -

- {item.description} -

- - {!item.available && ( - - Currently Unavailable - - )} -
-
- ))} -
- ); -}; - -export default MenuGrid; - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\GoogleTranslate.tsx - -import React, { useEffect, useRef } from 'react'; -import { useLanguage } from '@/context/LanguageContext'; - -declare global { - interface Window { - google: any; - googleTranslateElementInit: () => void; - g_gTranslateIsAdded: boolean; - } -} - -// Initialize the global flag -if (typeof window !== 'undefined') { - window.g_gTranslateIsAdded = window.g_gTranslateIsAdded || false; -} - -export default function GoogleTranslate() { - const { currentLanguage } = useLanguage(); - const googleTranslateRef = useRef(null); - - useEffect(() => { - // Skip if already initialized - if (window.g_gTranslateIsAdded) { - return; - } - - window.googleTranslateElementInit = () => { - if (!window.g_gTranslateIsAdded && googleTranslateRef.current) { - window.g_gTranslateIsAdded = true; - new window.google.translate.TranslateElement( - { - pageLanguage: 'en', - includedLanguages: 'en,es,vi,zh-CN,zh-TW,ja,ko,th,fr,de,it,ru,ar,hi,tl', - layout: window.google.translate.TranslateElement.InlineLayout.SIMPLE, - autoDisplay: false, - }, - googleTranslateRef.current - ); - - // Add custom styling - const style = document.createElement('style'); - style.textContent = ` - .goog-te-gadget { - font-family: inherit !important; - font-size: 13px !important; - margin: -4px 0 !important; - } - .goog-te-gadget-simple { - background-color: transparent !important; - border: none !important; - padding: 0 !important; - line-height: 1.2 !important; - } - .goog-te-gadget-simple .goog-te-menu-value { - padding: 0 !important; - margin: 0 !important; - vertical-align: middle !important; - } - .goog-te-gadget-simple .goog-te-menu-value span:not(:first-child) { - display: none !important; - } - .goog-te-gadget-simple .goog-te-menu-value span:first-child { - color: transparent !important; - visibility: visible !important; - } - .goog-te-gadget img { - display: none !important; - } - .goog-te-gadget-simple span { - margin-right: 0 !important; - border: none !important; - } - .goog-te-combo { - padding: 4px !important; - border: 1px solid #ccc !important; - border-radius: 4px !important; - font-size: 13px !important; - color: #374151 !important; - background-color: white !important; - min-width: 120px !important; - appearance: auto !important; - -webkit-appearance: auto !important; - } - .goog-logo-link { - display: none !important; - } - .goog-te-banner-frame { - display: none !important; - } - #goog-gt-tt, .goog-te-balloon-frame { - display: none !important; - } - .goog-text-highlight { - background: none !important; - box-shadow: none !important; - } - `; - document.head.appendChild(style); - } - }; - - // Add the script only if not already added - if (!document.querySelector('script[src*="translate_a/element.js"]')) { - const script = document.createElement('script'); - script.src = '//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit'; - script.async = true; - document.body.appendChild(script); - } - }, []); - - return ( -
- ); -} - -# C:\Users\rahif\Desktop\project-3-team-6b\project-3\client\src\components\ScreenMagnifier.tsx - -import React, { useState, useEffect, useRef } from 'react'; - -const ScreenMagnifier = ({ enabled = false, magnification = 2 }) => { - const [position, setPosition] = useState({ x: 0, y: 0 }); - const [isVisible, setIsVisible] = useState(false); - const contentRef = useRef(null); - const magnifierRef = useRef(null); - const debounceTimeoutRef = useRef(null); - - useEffect(() => { - if (!enabled) return; - - const updateMagnifierContent = (iframe: HTMLIFrameElement, x: number, y: number) => { - if (iframe.contentDocument) { - const content = iframe.contentDocument.documentElement.cloneNode(true) as HTMLElement; - - // Remove scripts for safety - content.querySelectorAll('script').forEach(script => script.remove()); - - // Pull styles from the current document - const styles = Array.from(document.styleSheets).map(sheet => { - try { - if (sheet.href) { - return ``; // External stylesheets - } - return ``; // Internal styles - } catch { - if (sheet.href) { - return ``; // External stylesheets - } - return ''; // Return empty if styles can't be fetched - } - }).join(''); - - // Write the content to the iframe - iframe.contentDocument.open(); - iframe.contentDocument.write(` - - - - ${styles} - - - ${content.innerHTML} - - - `); - iframe.contentDocument.close(); - - // Now, adjust the content inside the iframe to zoom in - const imageArea = iframe.contentDocument.querySelector('body'); - if (imageArea) { - // Zoom in around the mouse position - const scaleX = x / window.innerWidth; - const scaleY = y / window.innerHeight; - iframe.contentDocument.body.style.transform = `scale(${magnification})`; - iframe.contentDocument.body.style.transformOrigin = `${scaleX * 100}% ${scaleY * 100}%`; - } - } - }; - - - - const handleMouseMove = (e: MouseEvent) => { - const x = e.clientX; // Use clientX to exclude scroll offset - const y = e.clientY; // Use clientY to exclude scroll offset - - - const xNum = e.pageX; // Use pageX to include scroll offset - const yNum = e.pageY; - // Update magnifier position - setPosition({ x, y }); - setIsVisible(true); - - if (debounceTimeoutRef.current) { - clearTimeout(debounceTimeoutRef.current); - } - debounceTimeoutRef.current = setTimeout(() => { - const iframe = magnifierRef.current?.querySelector('iframe'); - if (iframe) { - updateMagnifierContent(iframe, x, y); - } - }, 25); // Update every 50ms, can adjust based on performance needs - - // Adjust the content translation - if (contentRef.current) { - const lensWidth = 1000; // Width of your magnifier lens - const lensHeight = 500; // Height of your magnifier lens - - // Calculate the offset for the content translation - const offsetX = xNum - lensWidth / 2; - const offsetY = yNum - lensHeight / 2; - - contentRef.current.style.transform = `translate(${-offsetX}px, ${-offsetY}px)`; - } - }; - - - - const handleMouseLeave = () => { - setIsVisible(false); - - }; - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseleave', handleMouseLeave); - - return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseleave', handleMouseLeave); - }; - }, [enabled]); - - if (!enabled || !isVisible) return null; - - return ( - <> - {/* Semi-transparent overlay to capture the current view */} -
- {/* Magnifier lens */} -
- {/* Content container */} -
- {/* Scaled content */} -
- -
-