diff --git a/backend/app/kiosk/routes.py b/backend/app/kiosk/routes.py index 5210cc38..c3879920 100644 --- a/backend/app/kiosk/routes.py +++ b/backend/app/kiosk/routes.py @@ -21,7 +21,7 @@ def get_menu_items(): ).fetchall() single_drink = db.session.execute( - text("SELECT * FROM menu_item WHERE item_name = 'drinks' LIMIT 1") + text("SELECT * FROM menu_item WHERE item_name = 'drink' LIMIT 1") ).fetchall() all_other_items = db.session.execute( @@ -30,7 +30,7 @@ def get_menu_items(): WHERE item_name NOT LIKE 'appetizer%' AND item_name NOT LIKE 'dessert%' AND item_name NOT LIKE 'aLaCarte%' - AND item_name NOT LIKE 'drinks%' + AND item_name NOT LIKE 'drink%' ORDER BY menu_item_id ASC """) ).fetchall() @@ -152,8 +152,8 @@ def get_drinks(): ] drinks = db.session.execute( - text("SELECT * FROM product_item WHERE type = :type ORDER BY product_id ASC"), - {'type': 'drink'} + text("SELECT * FROM product_item WHERE type = :type1 OR type = :type2 ORDER BY product_id ASC"), + {'type1': 'drink', 'type2': 'fountainDrink'} ).fetchall() drinks_list = [ diff --git a/backend/app/menu/routes.py b/backend/app/menu/routes.py index e340d7c8..75d422a2 100644 --- a/backend/app/menu/routes.py +++ b/backend/app/menu/routes.py @@ -20,7 +20,7 @@ def get_menu_items(): WHERE item_name NOT LIKE 'appetizer%' AND item_name NOT LIKE 'dessert%' AND item_name NOT LIKE 'aLaCarte%' - AND item_name NOT LIKE 'drinks%' + AND item_name NOT LIKE 'drink%' ORDER BY menu_item_id ASC """) ).fetchall() diff --git a/backend/app/pos/routes.py b/backend/app/pos/routes.py index ea141cde..a485e9ee 100644 --- a/backend/app/pos/routes.py +++ b/backend/app/pos/routes.py @@ -1,5 +1,401 @@ from . import pos_bp +from flask import request, jsonify +from sqlalchemy import text +from app.extensions import db +from datetime import datetime @pos_bp.route('/', methods=['GET']) def cashier_home(): return {"message": "Welcome to the Cashier POS"} + +@pos_bp.route('/menu', methods=['GET']) +def get_menu_items(): + single_appetizer = db.session.execute( + text("SELECT * FROM menu_item WHERE item_name = 'appetizerSmall' LIMIT 1") + ).fetchall() + + single_a_la_carte = db.session.execute( + text("SELECT * FROM menu_item WHERE item_name = 'aLaCarteSideMedium' LIMIT 1") + ).fetchall() + + single_drink = db.session.execute( + text("SELECT * FROM menu_item WHERE item_name = 'drink' LIMIT 1") + ).fetchall() + + all_other_items = db.session.execute( + text(""" + SELECT * FROM menu_item + WHERE item_name NOT LIKE 'appetizer%' + AND item_name NOT LIKE 'dessert%' + AND item_name NOT LIKE 'aLaCarte%' + AND item_name NOT LIKE 'drink%' + ORDER BY menu_item_id ASC + """) + ).fetchall() + + menu_items = all_other_items + single_a_la_carte + single_appetizer + single_drink + + menu_items_list = [ + { + "menu_item_id": menu_item.menu_item_id, + "item_name": menu_item.item_name, + "max_entrees": menu_item.max_entrees, + "max_sides": menu_item.max_sides, + "menu_item_base_price": menu_item.menu_item_base_price, + "premium_multiplier": menu_item.premium_multiplier, + "menu_item_description": menu_item.menu_item_description, + "calories": menu_item.calories, + "image": menu_item.image, + } + for menu_item in menu_items + ] + + return jsonify(menu_items_list), 200 + + +@pos_bp.route('/sides', methods=['GET']) +def get_sides(): + sides = db.session.execute( + text("SELECT * FROM product_item WHERE type = :type ORDER BY product_id ASC"), + {'type': 'side'} + ).fetchall() + + sides_list = [ + { + "product_id": side.product_id, + "product_name": side.product_name, + "type": side.type, + "is_seasonal": side.is_seasonal, + "is_available": side.is_available, + "servings_remaining": side.servings_remaining, + "allergens": side.allergens, + "display_icons": side.display_icons, + "product_description": side.product_description, + "premium_addition": side.premium_addition, + "serving_size": side.serving_size, + "calories": side.calories, + "saturated_fat": side.saturated_fat, + "carbohydrate": side.carbohydrate, + "protein": side.protein, + "image": side.image, + "is_premium": side.is_premium, + } + for side in sides + ] + + return jsonify(sides_list), 200 + + +@pos_bp.route('/entrees', methods=['GET']) +def get_entrees(): + entrees = db.session.execute( + text("SELECT * FROM product_item WHERE type = :type ORDER BY product_id ASC"), + {'type': 'entree'} + ).fetchall() + + entrees_list = [ + { + "product_id": entree.product_id, + "product_name": entree.product_name, + "type": entree.type, + "is_seasonal": entree.is_seasonal, + "is_available": entree.is_available, + "servings_remaining": entree.servings_remaining, + "allergens": entree.allergens, + "display_icons": entree.display_icons, + "product_description": entree.product_description, + "premium_addition": entree.premium_addition, + "serving_size": entree.serving_size, + "calories": entree.calories, + "saturated_fat": entree.saturated_fat, + "carbohydrate": entree.carbohydrate, + "protein": entree.protein, + "image": entree.image, + "is_premium": entree.is_premium, + } + for entree in entrees + ] + + return jsonify(entrees_list), 200 + + +@pos_bp.route('/alacarte', methods=['GET']) +def get_a_la_carte(): + products = db.session.execute( + text("SELECT * FROM product_item WHERE type = :type1 OR type = :type2 ORDER BY product_id ASC"), + {'type1': 'entree', 'type2': 'side'} + ).fetchall() + + products_list = [ + { + "product_id": product.product_id, + "product_name": product.product_name, + "type": product.type, + "is_seasonal": product.is_seasonal, + "is_available": product.is_available, + "servings_remaining": product.servings_remaining, + "allergens": product.allergens, + "display_icons": product.display_icons, + "product_description": product.product_description, + "premium_addition": product.premium_addition, + "serving_size": product.serving_size, + "calories": product.calories, + "saturated_fat": product.saturated_fat, + "carbohydrate": product.carbohydrate, + "protein": product.protein, + "image": product.image, + "is_premium": product.is_premium, + } + for product in products + ] + + return jsonify(products_list), 200 + + +@pos_bp.route('/appetizer', methods=['GET']) +def get_appetizers(): + products = db.session.execute( + text("SELECT * FROM product_item WHERE type = :type1 OR type = :type2 ORDER BY product_id ASC"), + {'type1': 'appetizer', 'type2': 'dessert'} + ).fetchall() + + products_list = [ + { + "product_id": product.product_id, + "product_name": product.product_name, + "type": product.type, + "is_seasonal": product.is_seasonal, + "is_available": product.is_available, + "servings_remaining": product.servings_remaining, + "allergens": product.allergens, + "display_icons": product.display_icons, + "product_description": product.product_description, + "premium_addition": product.premium_addition, + "serving_size": product.serving_size, + "calories": product.calories, + "saturated_fat": product.saturated_fat, + "carbohydrate": product.carbohydrate, + "protein": product.protein, + "image": product.image, + "is_premium": product.is_premium, + } + for product in products + ] + + return jsonify(products_list), 200 + + +@pos_bp.route('/drink', methods=['GET']) +def get_drinks(): + drinks = db.session.execute( + text("SELECT * FROM product_item WHERE type = :type1 OR type = :type2 ORDER BY product_id ASC"), + {'type1': 'drink', 'type2': 'fountainDrink'} + ).fetchall() + + drinks_list = [ + { + "product_id": drink.product_id, + "product_name": drink.product_name, + "type": drink.type, + "is_seasonal": drink.is_seasonal, + "is_available": drink.is_available, + "servings_remaining": drink.servings_remaining, + "allergens": drink.allergens, + "display_icons": drink.display_icons, + "product_description": drink.product_description, + "premium_addition": drink.premium_addition, + "serving_size": drink.serving_size, + "calories": drink.calories, + "saturated_fat": drink.saturated_fat, + "carbohydrate": drink.carbohydrate, + "protein": drink.protein, + "image": drink.image, + "is_premium": drink.is_premium, + } + for drink in drinks + ] + + return jsonify(drinks_list), 200 + + +@pos_bp.route('/size//', methods=['GET']) +def get_size_price(item_name, size): + size_mapping = { + "small": "Small", + "medium": "Medium", + "large": "Large" + } + + size_name = size_mapping.get(size.lower(), None) + if not size_name: + return jsonify({"error": "Invalid size"}), 400 + + result = db.session.execute( + text(""" + SELECT + item_name, + menu_item_base_price, + premium_multiplier + FROM menu_item + WHERE item_name = :item_name || :size_name + """), + {"item_name": item_name, "size_name": size_name} + ).fetchone() + + if not result: + return jsonify({"error": "Size not found"}), 404 + + return jsonify({ + "name": result.item_name, + "price": float(result.menu_item_base_price), + "multiplier": float(result.premium_multiplier) + }), 200 + + +@pos_bp.route('/checkout', methods=['POST']) +def checkout(): + try: + data = request.get_json() + print("Received Order Data:", data) + + items = data.get("items", []) + total_price = data.get("total", 0) + print(f"\nTotal Price: ${total_price}") + + for idx, item in enumerate(items): + print(f"\nItem {idx + 1}:") + print(f" - Name: {item['name']}") + print(f" - Quantity: {item['quantity']}") + print(f" - Price: ${item['price']}") + + subitems = item.get('subitems', []) + for sub_idx, subitem in enumerate(subitems): + print(f" Subitem {sub_idx + 1}:") + print(f" - Name: {subitem['product_name']}") + print(f" - Type: {subitem['type']}") + print(f" - Quantity: {subitem['quantity']}") + + return jsonify({"message": "Order received and printed successfully"}), 200 + + except Exception as e: + print(f"Error while processing checkout: {e}") + return jsonify({"error": "Failed to process the order"}), 500 + + +@pos_bp.route('/checkout/confirm', methods=['POST']) +def confirm_checkout(): + try: + order_data = request.get_json() + employee_id = 121202 # CHANGE LATER + + # Order Table + order_items = order_data.get("items", []) + total_price = order_data.get("total", 0.0) + order_date_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + insert_order_query = text(""" + INSERT INTO public."order" (order_date_time, employee_id, total_price, is_ready) + VALUES (:order_date_time, :employee_id, :total_price, :is_ready) + RETURNING order_id + """) + order_result = db.session.execute( + insert_order_query, + { + "order_date_time": order_date_time, + "employee_id": employee_id, + "total_price": total_price, + "is_ready": False + } + ) + db.session.commit() + order_id = order_result.fetchone().order_id + + # order + for item_idx, item in enumerate(order_items): + item_name = item.get("name") + item_price = item.get("price") + quantity = item.get("quantity", 1) + + # Get the menu item ID + menu_item_query = text("SELECT menu_item_id FROM menu_item WHERE item_name = :item_name") + menu_item_result = db.session.execute(menu_item_query, {"item_name": item_name}).fetchone() + + if not menu_item_result: + return jsonify({"error": f"Menu item '{item_name}' not found"}), 404 + + menu_item_id = menu_item_result.menu_item_id + + # order_menu_item + for _ in range(quantity): + insert_order_menu_item_query = text(""" + INSERT INTO public.order_menu_item (order_id, menu_item_id, subtotal_price) + VALUES (:order_id, :menu_item_id, :subtotal_price) + RETURNING order_menu_item_id + """) + order_menu_item_result = db.session.execute( + insert_order_menu_item_query, + { + "order_id": order_id, + "menu_item_id": menu_item_id, + "subtotal_price": item_price + } + ) + db.session.commit() + order_menu_item_id = order_menu_item_result.fetchone().order_menu_item_id + + # order_menu_item_product + subitems = item.get("subitems", []) + for subitem in subitems: + product_id = subitem.get("product_id") + quantity = subitem.get("quantity", 1) + + insert_order_menu_item_product_query = text(""" + INSERT INTO public.order_menu_item_product (order_menu_item_id, product_id) + VALUES (:order_menu_item_id, :product_id) + """) + db.session.execute( + insert_order_menu_item_product_query, + { + "order_menu_item_id": order_menu_item_id, + "product_id": product_id + } + ) + + # Update servings remaining in the product_item table + update_servings_query = text(""" + UPDATE public.product_item + SET servings_remaining = servings_remaining - :quantity + WHERE product_id = :product_id + """) + db.session.execute( + update_servings_query, + { + "quantity": quantity, + "product_id": product_id + } + ) + + db.session.commit() + return jsonify({"message": "Successfully confirmed", "order_id": order_id}), 201 + + except Exception as e: + db.session.rollback() + print(f"Confirm order failed: {e}") + return jsonify({"error": "Failed to confirm the order"}), 500 + + +@pos_bp.route('/next-order-id', methods=['GET']) +def get_next_order_id(): + try: + next_order_id_query = text(""" + SELECT MAX(order_id) + 1 AS next_order_id + FROM public."order" + """) + result = db.session.execute(next_order_id_query).fetchone() + + next_order_id = result.next_order_id if result.next_order_id is not None else 1 + return jsonify({"next_order_id": next_order_id}), 200 + + except Exception as e: + print(f"Failed to get next order ID: {e}") + return jsonify({"error": "Failed to get next order ID"}), 500 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1e296637..fff3dba1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,6 +18,7 @@ "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "react": "^18.3.1", + "react-bootstrap": "^2.10.6", "react-chartjs-2": "^5.2.0", "react-datepicker": "^7.5.0", "react-dom": "^18.3.1", @@ -3012,6 +3013,21 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@react-aria/ssr": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.7.tgz", + "integrity": "sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@remix-run/router": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", @@ -3020,6 +3036,60 @@ "node": ">=14.0.0" } }, + "node_modules/@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.1.tgz", + "integrity": "sha512-qghR21ynHiUrpcIkKCoKYB+3rJtezY5Y7ikrwradCL+7hZHdQ2Ozc5ffxtpmpahoAGgc31gyXaSx2sXXaThmqA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@popperjs/core": "^2.11.8", + "@react-aria/ssr": "^3.5.0", + "@restart/hooks": "^0.5.0", + "@types/warning": "^3.0.3", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^8.0.4", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/@restart/ui/node_modules/@restart/hooks": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.0.tgz", + "integrity": "sha512-wS+h6IusJCPjTkmOOrRZxIPICD/mtFA3PRZviutoM23/b7akyDGfZF/WS+nIFk27u7JDhPE2+0GBdZxjSqHZkg==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui/node_modules/uncontrollable": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", + "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.14.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3343,6 +3413,15 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -3963,6 +4042,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", + "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -4034,6 +4122,12 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, + "node_modules/@types/warning": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", + "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.5.12", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", @@ -9221,6 +9315,15 @@ "node": ">=12" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -13207,6 +13310,25 @@ "react-is": "^16.13.1" } }, + "node_modules/prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "license": "MIT", + "dependencies": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, + "node_modules/prop-types-extra/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -13388,6 +13510,36 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, + "node_modules/react-bootstrap": { + "version": "2.10.6", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.6.tgz", + "integrity": "sha512-fNvKytSp0nHts1WRnRBJeBEt+I9/ZdrnhIjWOucEduRNvFRU1IXjZueDdWnBiqsTSJ7MckQJi9i/hxGolaRq+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@restart/hooks": "^0.4.9", + "@restart/ui": "^1.9.0", + "@types/react-transition-group": "^4.4.6", + "classnames": "^2.3.2", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.8.1", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.5", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "@types/react": ">=16.14.8", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-chartjs-2": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", @@ -13560,6 +13712,12 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, "node_modules/react-popper": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", @@ -15943,6 +16101,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, "node_modules/underscore": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index af6ccf3f..6a785b89 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,8 @@ "react": "^18.3.1", "chart.js": "^4.4.6", "date-fns": "^4.1.0", + "react": "^18.3.1", + "react-bootstrap": "^2.10.6", "react-chartjs-2": "^5.2.0", "react-datepicker": "^7.5.0", "react-dom": "^18.3.1", diff --git a/frontend/src/pages/kiosk/KioskMain.js b/frontend/src/pages/kiosk/KioskMain.js index ebaa6a5e..b28912bd 100644 --- a/frontend/src/pages/kiosk/KioskMain.js +++ b/frontend/src/pages/kiosk/KioskMain.js @@ -49,9 +49,9 @@ function KioskMain() { .toLowerCase() .replace(/\s+/g, '-'); console.log(formattedName); - - if (formattedName === "drinks") { - navigate(`/kiosk/order/drink`); + + if (formattedName === "drink") { + navigate(`/kiosk/order/drinks`); } else if (formattedName === "appetizers-&-more") { navigate(`/kiosk/order/appetizers-&-more`); diff --git a/frontend/src/pages/kiosk/components/NavBar.js b/frontend/src/pages/kiosk/components/NavBar.js index 090f0558..1ecfe28f 100644 --- a/frontend/src/pages/kiosk/components/NavBar.js +++ b/frontend/src/pages/kiosk/components/NavBar.js @@ -1,5 +1,5 @@ import axios from "axios"; -import beastLogo from "./beastLogo.png"; +import beastLogo from "../../../assets/beast-logo.png"; import "../../../styles/navbar.css"; import 'bootstrap-icons/font/bootstrap-icons.css'; import { useEffect, useState, useContext } from "react"; diff --git a/frontend/src/pages/kiosk/components/beastLogo.png b/frontend/src/pages/kiosk/components/beastLogo.png deleted file mode 100644 index 79a39843..00000000 Binary files a/frontend/src/pages/kiosk/components/beastLogo.png and /dev/null differ diff --git a/frontend/src/pages/pos/PosMain.js b/frontend/src/pages/pos/PosMain.js index 98f2063c..3ecfe079 100644 --- a/frontend/src/pages/pos/PosMain.js +++ b/frontend/src/pages/pos/PosMain.js @@ -1,34 +1,404 @@ -import { useState, useEffect } from "react"; -import api from '../../services/api'; // Axios instance with base URL +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import api from "../../services/api"; +import "../../styles/pos.css"; +import MenuSection from "./components/MenuSection"; +import OrderSection from "./components/OrderSection"; +import Footer from "./components/Footer"; +import SizeSelection from "./components/SizeSelection"; +import { Modal, Button } from "react-bootstrap"; function PosMain() { - const [posData, setPosData] = useState(null); // State to hold data + const [currentOrder, setCurrentOrder] = useState([]); + const [orderNumber, setOrderNumber] = useState(null); + const [total, setTotal] = useState(0); + const [menuEndpoint, setMenuEndpoint] = useState("/pos/menu"); + const [currentWorkflow, setCurrentWorkflow] = useState(null); + const [workflowStep, setWorkflowStep] = useState(0); + const [currentSubitemType, setCurrentSubitemType] = useState(null); + const navigate = useNavigate(); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [itemToDelete, setItemToDelete] = useState(null); + const [isHalfAndHalf, setIsHalfAndHalf] = useState(false); + const [halfSideActivated, setHalfSideActivated] = useState(false); + const [showCheckoutModal, setShowCheckoutModal] = useState(false); + const disableActions = currentWorkflow !== null || currentOrder.length === 0; - // Fetch data from /pos - const fetchAPI = async () => { + useEffect(() => { + const fetchNextOrderNumber = async () => { + try { + const response = await api.get("/pos/next-order-id"); + if (response.status === 200) { + setOrderNumber(response.data.next_order_id); + } + } catch (error) { + console.error("Failed to fetch the next order number:", error); + } + }; + + fetchNextOrderNumber(); + }, []); + + const resetCurrentWorkflow = () => { + setCurrentWorkflow(null); + setWorkflowStep(0); + setMenuEndpoint("/pos/menu"); + setCurrentSubitemType(null); + setIsHalfAndHalf(false); + setHalfSideActivated(false); + }; + + const formatNames = (item) => { + let formattedName = item.replace(/Small|Medium|Side|Entree/g, ""); + formattedName = formattedName.replace(/([A-Z])/g, " $1").trim(); + return formattedName + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join("") + .toLowerCase(); + }; + + const generateWorkflowSteps = (item) => { + const steps = []; + for (let i = 0; i < item.max_sides; i++) steps.push("sides"); + for (let i = 0; i < item.max_entrees; i++) steps.push("entrees"); + return steps; + }; + + const handleAddToOrder = (item) => { + const itemType = formatNames(item.item_name); + let steps; + + if (["drink", "appetizer", "alacarte"].includes(itemType)) { + steps = [`${itemType}`]; + } else { + steps = generateWorkflowSteps(item); + } + + setCurrentWorkflow({ + quantity: 1, + name: item.item_name, + price: parseFloat(item.menu_item_base_price) || 0, + multiplier: parseFloat(item.premium_multiplier), + steps: steps, + subitems: [], + }); + setWorkflowStep(0); + setMenuEndpoint(`/pos/${steps[0]}`); + }; + + const handleSubitemSelect = (subitem) => { + if (isHalfAndHalf && subitem.type === "side") { + const subitems = [...currentWorkflow.subitems, { ...subitem, quantity: 0.5 }]; + setCurrentWorkflow({ ...currentWorkflow, subitems }); + + if (workflowStep > 0 && currentWorkflow.steps[workflowStep] === "sides") { + setIsHalfAndHalf(false); + if (workflowStep < currentWorkflow.steps.length - 1) { + setWorkflowStep(workflowStep + 1); + setMenuEndpoint(`/pos/${currentWorkflow.steps[workflowStep + 1]}`); + } + else { + finalizeItem(subitems); + setHalfSideActivated(false); + } + } + else { + setWorkflowStep(workflowStep + 1); + setMenuEndpoint(`/pos/${currentWorkflow.steps[workflowStep + 1]}`); + } + } + else { + const subitems = [...currentWorkflow.subitems, { ...subitem, quantity: 1 }]; + + if ( + ["fountaindrink", "dessert", "appetizer"].includes(formatNames(subitem.type)) || + (currentWorkflow.name === "aLaCarteSideMedium" && ["entree", "side"].includes(subitem.type)) + ) { + setCurrentWorkflow({ ...currentWorkflow, subitems }); + currentWorkflow.steps.push("size-selection"); + setWorkflowStep(1); + setCurrentSubitemType(subitem.type); + setMenuEndpoint("/pos/size-selection"); + } else { + const currentSteps = currentWorkflow.steps || []; + + if (workflowStep < currentSteps.length - 1) { + setCurrentWorkflow({ ...currentWorkflow, subitems }); + setWorkflowStep(workflowStep + 1); + setMenuEndpoint(`/pos/${currentSteps[workflowStep + 1]}`); + } else { + finalizeItem(subitems); + setHalfSideActivated(false); + } + } + } + } + + const finalizeItem = (subitems) => { + const finalizedItem = { + quantity: 1, + name: currentWorkflow.name, + subitems, + price: + parseFloat(currentWorkflow.price) + + subitems.reduce((sum, si) => sum + (parseFloat(si.premium_addition) || 0) * (si.quantity || 1), 0), + }; + + setCurrentOrder((prevOrder) => [...prevOrder, finalizedItem]); + setTotal((prevTotal) => prevTotal + finalizedItem.price); + resetCurrentWorkflow(); + }; + + const handleHalfAndHalf = () => { + if (currentWorkflow && currentWorkflow.steps[workflowStep] === "sides") { + const updatedSteps = [...currentWorkflow.steps]; + updatedSteps.splice(workflowStep + 1, 0, "sides"); + + setCurrentWorkflow({ + ...currentWorkflow, + steps: updatedSteps, + }); + + setIsHalfAndHalf(true); + } + }; + + const handleCancelHalfSide = () => { + const updatedSteps = [...currentWorkflow.steps]; + const firstSidesIndex = updatedSteps.indexOf("sides"); + const filteredSteps = updatedSteps.filter((step, index) => step !== "sides" || index === firstSidesIndex); + + setCurrentWorkflow({ + ...currentWorkflow, + steps: filteredSteps, + subitems: [] + }); + + if (filteredSteps[firstSidesIndex] === "sides") { + setWorkflowStep(firstSidesIndex); + setMenuEndpoint(`/pos/${filteredSteps[firstSidesIndex]}`); + setIsHalfAndHalf(false); + setHalfSideActivated(false); + } + else { + resetCurrentWorkflow(); + } + }; + + const handleSizeSelect = async (size) => { try { - const response = await api.get("/pos"); - setPosData(response.data); - } catch (error) { - console.error("Error fetching POS data:", error); + let endpointBase = currentWorkflow.name.replace(/Small|Medium|Side/g, ""); + if (endpointBase === "aLaCarte") { + endpointBase += size.type.charAt(0).toUpperCase() + size.type.slice(1); + } + else if (size.type === "dessert") endpointBase = size.type; + const response = await api.get(`/pos/size/${endpointBase}/${size.name}`); + + const finalizedItem = { + quantity: 1, + name: response.data.name, + multiplier: response.data.multiplier, + price: response.data.price + (parseFloat(currentWorkflow.subitems[0].premium_addition) * parseFloat(response.data.multiplier)), + subitems: currentWorkflow.subitems, + }; + + setCurrentOrder((prevOrder) => [...prevOrder, finalizedItem]); + setTotal((prevTotal) => prevTotal + finalizedItem.price); + resetCurrentWorkflow(); + } + catch (error) { + console.error(`Failed to fetch size pricing:`, error); + alert(`Error fetching size pricing. Please try again.`); } }; - // Fetch data on component load - useEffect(() => { - fetchAPI(); - }, []); + const handleIncreaseQuantity = (index) => { + const updatedOrder = [...currentOrder]; + updatedOrder[index].quantity += 1; + setCurrentOrder(updatedOrder); + setTotal((prevTotal) => prevTotal + updatedOrder[index].price); + }; + + const handleDecreaseQuantity = (index) => { + if (currentOrder[index].quantity === 1) { + setItemToDelete(index); + setShowDeleteModal(true); + } else { + const updatedOrder = [...currentOrder]; + updatedOrder[index].quantity -= 1; + setCurrentOrder(updatedOrder); + setTotal((prevTotal) => prevTotal - updatedOrder[index].price); + } + }; + + const handleChangeQuantity = (index, newQuantity) => { + const updatedOrder = [...currentOrder]; + if (newQuantity > 0) { + const oldQuantity = updatedOrder[index].quantity; + updatedOrder[index].quantity = newQuantity; + const priceDifference = updatedOrder[index].price * (newQuantity - oldQuantity); + setCurrentOrder(updatedOrder); + setTotal((prevTotal) => prevTotal + priceDifference); + } + }; + + const confirmDeleteItem = () => { + if (itemToDelete !== null) { + const updatedOrder = [...currentOrder]; + const itemPrice = updatedOrder[itemToDelete].price * updatedOrder[itemToDelete].quantity; + updatedOrder.splice(itemToDelete, 1); + setCurrentOrder(updatedOrder); + setTotal((prevTotal) => prevTotal - itemPrice); + setShowDeleteModal(false); + setItemToDelete(null); + } + }; + + const cancelDeleteItem = () => { + setShowDeleteModal(false); + setItemToDelete(null); + }; + + const handleCheckout = () => { + setShowCheckoutModal(true); + }; + + const confirmCheckout = async () => { + try { + const response = await api.post("/pos/checkout/confirm", { + items: currentOrder, + total: (total*1.0625).toFixed(2), + }); + + if (response.status === 201) { + setCurrentOrder([]); + setTotal(0); + setOrderNumber(orderNumber + 1); + resetCurrentWorkflow(); + setShowCheckoutModal(false); + } + else { + throw new Error("Failed to finalize the order"); + } + } catch (error) { + console.error("Failed to finalize the order:", error); + alert("Error during checkout. Please try again."); + setShowCheckoutModal(false); + } + }; return ( -
-

Cashier POS

- {posData ? ( -

{posData.message}

// Display the "message" from the API response - ) : ( -

Loading...

// Show loading text while data is being fetched - )} +
+
+ {menuEndpoint === "/pos/size-selection" ? ( + + ) : ( + + )} + { + setCurrentOrder([]); + setTotal(0); + setCurrentWorkflow(null); + setWorkflowStep(0); + setMenuEndpoint("/pos/menu"); + setCurrentSubitemType(null); + }} + onIncreaseQuantity={handleIncreaseQuantity} + onDecreaseQuantity={handleDecreaseQuantity} + onChangeQuantity={handleChangeQuantity} + disableActions={disableActions} + /> +
+
{ + if (workflowStep > 0) { + const updatedSubitems = [...currentWorkflow.subitems]; + updatedSubitems.pop(); + const updatedSteps = [...currentWorkflow.steps]; + + if ( + isHalfAndHalf && + updatedSteps[workflowStep] === "sides" && + updatedSteps[workflowStep - 1] === "sides" + ) { + updatedSteps.splice(workflowStep, 1); + setIsHalfAndHalf(false); + setHalfSideActivated(false); + setWorkflowStep(workflowStep - 1); + setMenuEndpoint(`/pos/${updatedSteps[workflowStep - 1]}`); + } else { + setCurrentWorkflow({ + ...currentWorkflow, + subitems: updatedSubitems, + steps: updatedSteps, + }); + + setWorkflowStep(workflowStep - 1); + setMenuEndpoint(`/pos/${updatedSteps[workflowStep - 1]}`); + } + } + else { + resetCurrentWorkflow(); + } + }} + /> + + + + Confirm Removal + + Are you sure you want to remove this item from the order? + + + + + + + setShowCheckoutModal(false)}> + + Confirm Checkout + + + Are you sure you want to finalize Order #{orderNumber}? +
+ Total Amount: ${(total*1.0625).toFixed(2)} +
+ + + + +
); } -export default PosMain; +export default PosMain; \ No newline at end of file diff --git a/frontend/src/pages/pos/components/Footer.js b/frontend/src/pages/pos/components/Footer.js new file mode 100644 index 00000000..31078377 --- /dev/null +++ b/frontend/src/pages/pos/components/Footer.js @@ -0,0 +1,35 @@ +import React from "react"; + +function Footer({ navigate, onBack, menuEndpoint }) { + return ( +
+ Ethan (temp) + {menuEndpoint !== "/pos/menu" && ( + + )} +
+ ); +} + +const footerStyles = { + position: 'relative', + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-start', +}; + +const backButtonStyles = { + alignItems: 'right', + position: 'absolute', + top: '1%', + right: '0px', + color: "#fff", +}; + +export default Footer; diff --git a/frontend/src/pages/pos/components/MenuSection.js b/frontend/src/pages/pos/components/MenuSection.js new file mode 100644 index 00000000..951e9ed3 --- /dev/null +++ b/frontend/src/pages/pos/components/MenuSection.js @@ -0,0 +1,94 @@ +import React, { useState, useEffect } from "react"; +import api from "../../../services/api"; + +function MenuSection({ currentWorkflow, apiEndpoint, onAddToOrder, onSubitemSelect, onHalfSide, onCancelHalfSide, halfSideActivated, setHalfSideActivated }) { + const [menuItems, setMenuItems] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchMenuItems = async () => { + try { + const response = await api.get(apiEndpoint); + setMenuItems(response.data || []); + } catch (err) { + console.error("Failed to fetch menu items:", err); + setError("Failed to load menu items. Please try again later."); + } + }; + + fetchMenuItems(); + }, [apiEndpoint]); + + if (error) { + return
{error}
; + } + + const getItemClass = (item) => { + if (item.is_premium) return "premium-item"; + if (item.type === "side") return "side-item"; + if (item.type === "entree") return "entree-item"; + if (item.type === "dessert") return "dessert-item"; + if (item.type === "appetizer") return "appetizer-item"; + if (item.type === "drink") return "drink-item"; + return ""; + }; + + const formatMenuNames = (item) => { + const name = item.item_name || item.product_name || item.name || "Unknown Item"; + + let formattedName = name.replace(/Small|Medium|Side|Entree/g, ""); + if (formattedName.toLowerCase() === "appetizer") return "Apps & More"; + + formattedName = formattedName.replace(/([A-Z])/g, " $1").trim(); + return formattedName + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + }; + + const handleHalfSideButtonClick = () => { + if (halfSideActivated) { + setHalfSideActivated(false); + onCancelHalfSide(); + } + else { + setHalfSideActivated(true); + onHalfSide(); + } + }; + + return ( +
+
+ {menuItems.length > 0 ? ( + menuItems.map((item, index) => ( + + )) + ) : ( +
Loading menu items...
+ )} +
+ {apiEndpoint.includes("sides") && currentWorkflow?.name !== "familyMeal" && ( + + )} +
+ ); +} + +export default MenuSection; \ No newline at end of file diff --git a/frontend/src/pages/pos/components/OrderSection.js b/frontend/src/pages/pos/components/OrderSection.js new file mode 100644 index 00000000..aa167395 --- /dev/null +++ b/frontend/src/pages/pos/components/OrderSection.js @@ -0,0 +1,167 @@ +import React from "react"; + +function OrderSection({ + orderNumber, + currentOrder, + total, + onCheckout, + onCancel, + onIncreaseQuantity, + onDecreaseQuantity, + onChangeQuantity, + disableActions +}) { + const taxAmount = parseFloat(total) * 0.0625; + const totalWithTax = parseFloat(total) + taxAmount; + + const formatOrderNames = (item) => { + const name = item.item_name || item.product_name || item.name || "Unknown Item"; + let formattedName = name.replace(/Side|Entree/g, ""); + formattedName = formattedName.replace(/([A-Z])/g, " $1").trim(); + formattedName = formattedName.replace(/\b(Small|Medium|Large)\b/gi, "($1)"); + return formattedName + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + }; + + return ( +
+

+ Order #{orderNumber} +

+
+
+ {currentOrder.length > 0 ? ( + currentOrder.map((item, index) => ( +
+

+ {formatOrderNames(item)} + + ${parseFloat(item.price * item.quantity).toFixed(2)} + +

+ {item.subitems && ( +
    + {item.subitems.map((subitem, subIndex) => ( +
  • + {formatOrderNames(subitem)} + {subitem.quantity === 0.5 && " (1/2)"} + {subitem.is_premium && " *"} +
  • + ))} +
+ )} +
+ + onChangeQuantity(index, parseInt(e.target.value))} + className="quantity-input" + /> + +
+
+ )) + ) : ( +

+ No items added yet. +

+ )} +
+
+
+
+ Subtotal: ${parseFloat(total).toFixed(2)} +
+
+ Tax: ${taxAmount.toFixed(2)} +
+
+ Total: ${totalWithTax.toFixed(2)} +
+
+
+
+ + +
+
+ ); +} + +export default OrderSection; \ No newline at end of file diff --git a/frontend/src/pages/pos/components/SizeSelection.js b/frontend/src/pages/pos/components/SizeSelection.js new file mode 100644 index 00000000..8fbb7732 --- /dev/null +++ b/frontend/src/pages/pos/components/SizeSelection.js @@ -0,0 +1,38 @@ +function SizeSelection({ onSizeSelect, itemType }) { + const sizes = [ + { name: "Small" }, + { name: "Medium" }, + { name: "Large" }, + ]; + + const disabledSizes = { + appetizer: ["Medium"], + side: ["Small"], + }; + + return ( +
+
+ {sizes.map((size, index) => ( + + ))} +
+
+ ); +} + +export default SizeSelection; \ No newline at end of file diff --git a/frontend/src/styles/pos.css b/frontend/src/styles/pos.css index e69de29b..03e24aa0 100644 --- a/frontend/src/styles/pos.css +++ b/frontend/src/styles/pos.css @@ -0,0 +1,271 @@ +/* General Layout */ +html, body { + margin: 0; + padding: 0; +} + +.pos-container { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; +} + +/* Main Content (Menu + Order Section) */ +.main-content { + display: flex; + flex: 0 0 90%; + overflow: hidden; +} + +/* Menu Section */ +.menu-section { + flex: 7; /* Menu takes 70% of the width */ + background-color: grey; + display: flex; + justify-content: flex-start; + align-items: flex-start; + padding: 20px; + overflow: hidden; + position: relative; +} + +.menu-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 10px; + width: 100%; +} + +.menu-item-btn { + background-color: #d5dbd7; + color: black; + border: none; + padding: 40px; + text-align: center; + border-radius: 5px; + font-size: 1rem; + cursor: pointer; + font-weight: bold; +} + +.menu-item-btn:hover { + background-color: rgba(rgb(0, 0, 0), rgb(0, 0, 0), rgb(0, 0, 0), 50); +} + +.premium-item { + background-color: gold; + font-weight: bold; + border: 1px solid #d4af37; +} + +.side-item { + background-color: lightgreen; +} + +.entree-item { + background-color: #d9b38c; +} + +.dessert-item { + background-color: #dda0dd; +} + +.drink-item { + background-color: #67a9bd; +} + + +.appetizer-item { + background-color: #f88379; +} + +.category-btn { + background-color: #ffcc00; +} + +.category-btn:hover { + background-color: #ffaa00; +} + +.half-side-btn { + position: absolute; + bottom: 20px; + left: 20px; + padding: 12px 20px; + background-color: #e28787; + color: #ffffff; + border: none; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + cursor: pointer; + font-size: 1rem; + transition: background-color 0.3s ease, box-shadow 0.3s ease; +} + +.half-side-btn:hover { + background-color: #af5b5b; + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); +} + +.half-side-btn:active { + transform: translateY(2px); +} + +.half-side-btn.active { + background-color: #28a745; + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4); +} + +.half-side-btn.active:hover { + background-color: #1c8635; + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); +} + +/* Order Section */ +.order-section { + flex: 3; /* Order takes 30% of the width */ + background-color: #222; + color: white; + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 20px; + overflow-y: auto; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.order-section::-webkit-scrollbar { + width: 0px; + height: 0px; + display: none; + padding: 20px; + background-color: #2b2b2b; + color: #fff; +} + +.order-items { + flex: 1; + overflow-y: auto; +} + +.order-buttons { + display: flex; + justify-content: space-between; + margin-top: 20px; + gap: 10px; +} + +.order-item { + margin-bottom: 1rem; + font-size: 1rem; +} + +.order-item > p { + display: flex; + justify-content: space-between; + margin: 0; + font-weight: bold; +} + +.order-subitem { + margin-left: 1rem; + font-size: 0.8rem; +} + +.checkout-btn { + flex: 1; + margin-right: 4px; + background-color: #ff4d4d; + color: white; + border: none; + padding: 0.5rem 1rem; + cursor: pointer; + font-size: 1rem; + font-weight: bold; + border-radius: 5px; +} + +.checkout-btn:hover { + background-color: #af4444; +} + +.cancel-btn { + background-color: #dc3545; + color: white; + border: none; + border-radius: 5px; + padding: 0.5rem 1rem; + cursor: pointer; + font-size: 1rem; + font-weight: bold; + margin-left: 4px; +} + +.cancel-btn:hover { + background-color: #666; +} + +/* Footer */ +.footer { + flex: 0 0 10%; + background-color: #800000; + color: white; + padding: 10px 20px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.back-btn { + background-color: #800000; + overflow: hidden; + text-align: right; + padding-right: 5%; + border: none; + width: 300px; + height: 95%; + font-size: 3rem; + cursor: pointer; +} + +/* Size Section */ +.invisible-btn { + visibility: hidden; + pointer-events: none; + } + +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="number"] { + -moz-appearance: textfield; +} + +.quantity-btn { + background-color: #007bff; + border: none; + padding: 5px 10px; + border-radius: 5px; + color: white; + cursor: pointer; + font-size: 1rem; + width: 30px; + justify-content: center; +} + +.quantity-input { + font-size: 1rem; + width: 40px; + text-align: center; + padding: 5px; + margin: 0px 10px; + border-radius: 5px; + border: 1px solid #444; + background-color: #555; + color: white; +} \ No newline at end of file