diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..13566b81b018ad684f3a35fee301741b2734c8f4 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000000000000000000000000000000000000..5d8becd669a486480fc8a59decf9b0656b039317 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,7 @@ +<component name="InspectionProjectProfileManager"> + <profile version="1.0"> + <option name="myName" value="Project Default" /> + <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> + <inspection_tool class="PyInterpreterInspection" enabled="true" level="INFORMATION" enabled_by_default="true" /> + </profile> +</component> \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000000000000000000000000000000000000..105ce2da2d6447d11dfe32bfb846c3d5b199fc99 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ +<component name="InspectionProjectProfileManager"> + <settings> + <option name="USE_PROJECT_PROFILE" value="false" /> + <version value="1.0" /> + </settings> +</component> \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000000000000000000000000000000000..e7161a1e7fa75fd7cd38b9c832cafd2f00378162 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/.idea/systemdev_hrproject.iml" filepath="$PROJECT_DIR$/.idea/systemdev_hrproject.iml" /> + </modules> + </component> +</project> \ No newline at end of file diff --git a/.idea/systemdev_hrproject.iml b/.idea/systemdev_hrproject.iml new file mode 100644 index 0000000000000000000000000000000000000000..37512de08fe8c5d5cca096f2c3678d35d0b8f41c --- /dev/null +++ b/.idea/systemdev_hrproject.iml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="PYTHON_MODULE" version="4"> + <component name="NewModuleRootManager"> + <content url="file://$MODULE_DIR$" /> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> + <component name="PyDocumentationSettings"> + <option name="format" value="PLAIN" /> + <option name="myDocStringFormat" value="Plain" /> + </component> + <component name="TemplatesService"> + <option name="TEMPLATE_CONFIGURATION" value="Jinja2" /> + <option name="TEMPLATE_FOLDERS"> + <list> + <option value="$MODULE_DIR$/app/templates" /> + </list> + </option> + </component> +</module> \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000000000000000000000000000000000..35eb1ddfbbc029bcab630581847471d7f238ec53 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="" vcs="Git" /> + </component> +</project> \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index 4ddcea7ab5450f5d0fe26ac8b86971f60218ef30..9b5eaa58bd16deaec2be2ed71055125aeecf263f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -6,34 +6,56 @@ from sqlalchemy import inspect def create_app(test_config=None): - # create and configure the app + """ + Application Factory Function + Creates and configures the Flask application instance + + Args: + test_config: Configuration to use for testing (default: None) + + Returns: + Configured Flask application instance + """ + # Create and configure the Flask application app = Flask(__name__, instance_relative_config=True) app.config.from_pyfile("config.py") app.config["SECRET_KEY"] = "alsd;kfjpqaweuja" app.config["SQLALCHEMY_DATABASE_URI"] = DATABASE_URI app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + # Import blueprints for different application modules from app.routes.auth import auth_bp from app.routes.home import home_bp from app.routes.users import users_bp - from app.routes.reports import reports_bp from app.routes.reservations import reservations_bp from app.routes.inventory import inventory_bp from app.routes.discounts import discounts_bp from app.routes.payments import payments_bp from app.routes.errors import errors_bp + from app.routes.order import order_bp + from app.routes.menu import menu_bp + from app.routes.staff import staff_bp + from app.routes.restaurant import restaurant_bp + # Register all blueprints with the application app.register_blueprint(auth_bp) app.register_blueprint(home_bp) app.register_blueprint(users_bp) - app.register_blueprint(reports_bp) app.register_blueprint(reservations_bp) app.register_blueprint(inventory_bp) app.register_blueprint(discounts_bp) app.register_blueprint(payments_bp) app.register_blueprint(errors_bp) + app.register_blueprint(order_bp) + app.register_blueprint(menu_bp) + app.register_blueprint(staff_bp) + app.register_blueprint(restaurant_bp) +<<<<<<< HEAD create_database_if_not_exists() +======= + # Initialize the database with the application +>>>>>>> dev init_app(app) # Check if the database is initialized diff --git a/app/db.py b/app/db.py index 573d75181fd805bc886362a898241bf50f4516c6..cc7dd10484ac3bef6429452b418499252f79ab17 100644 --- a/app/db.py +++ b/app/db.py @@ -20,6 +20,11 @@ def create_database_if_not_exists(): def init_db(): + """ + Initialize the database by creating all tables + Uses the SQLAlchemy models to create the database schema + Should be run when setting up the application for the first time + """ with current_app.app_context(): db.create_all() @@ -27,11 +32,22 @@ def init_db(): @click.command("init-db") @with_appcontext def init_db_command(): - """Clear the existing data and create new tables.""" + """ + CLI command to initialize the database + Clears existing data and creates new tables + Can be run from the command line using 'flask init-db' + """ init_db() click.echo("Initialized the database.") def init_app(app): + """ + Initialize the Flask application with the database + Registers the init-db command with the Flask CLI + + Args: + app: Flask application instance + """ db.init_app(app) app.cli.add_command(init_db_command) diff --git a/app/dummydata.py b/app/dummydata.py new file mode 100644 index 0000000000000000000000000000000000000000..61cf73fb055d6c78c5b62b0888b55dc426f28391 --- /dev/null +++ b/app/dummydata.py @@ -0,0 +1,699 @@ +from app.models import ( + Restaurant, + Menu, + User, + Inventory, + db, + Order, + OrderItem, + Payment, + Reservation, + Attendance, + Performance, + Discount, +) +from datetime import timedelta +from datetime import datetime +from werkzeug.security import generate_password_hash +from app import create_app +import traceback +from decimal import Decimal + + +def create_dummy_data(): + app = create_app() + with app.app_context(): + try: + # Check if data already exists + existing_restaurants = Restaurant.query.count() + existing_users = User.query.count() + existing_inventory = Inventory.query.count() + + if existing_restaurants > 0 or existing_users > 0 or existing_inventory > 0: + print( + f"Found existing data: {existing_restaurants} restaurants, {existing_users} users, {existing_inventory} inventory items" + ) + print("Clearing existing data...") + Menu.query.delete() + Restaurant.query.delete() + User.query.delete() + Inventory.query.delete() + db.session.commit() + print("Existing data cleared.") + + # Create inventory items + inventory_data = [ + # Fresh Produce + { + "name": "Tomatoes", + "quantity": 50, + "category": "Fresh Produce", + }, + { + "name": "Lettuce", + "quantity": 30, + "category": "Fresh Produce", + }, + { + "name": "Onions", + "quantity": 100, + "category": "Fresh Produce", + }, + { + "name": "Bell Peppers", + "quantity": 40, + "category": "Fresh Produce", + }, + { + "name": "Mushrooms", + "quantity": 25, + "category": "Fresh Produce", + }, + # Dairy + { + "name": "Cheddar Cheese", + "quantity": 20, + "category": "Dairy", + }, + { + "name": "Heavy Cream", + "quantity": 15, + "category": "Dairy", + }, + { + "name": "Butter", + "quantity": 20, + "category": "Dairy", + }, + { + "name": "Parmesan Cheese", + "quantity": 10, + "category": "Dairy", + }, + # Meat & Seafood + { + "name": "Chicken Breast", + "quantity": 60, + "category": "Meat", + }, + { + "name": "Salmon Fillet", + "quantity": 25, + "category": "Seafood", + }, + { + "name": "Ground Beef", + "quantity": 40, + "category": "Meat", + }, + { + "name": "Shrimp", + "quantity": 0, + "category": "Seafood", + }, + # Pantry + { + "name": "Olive Oil", + "quantity": 10, + "category": "Pantry", + }, + { + "name": "Rice", + "quantity": 75, + "category": "Pantry", + }, + { + "name": "Pasta", + "quantity": 55, + "category": "Pantry", + }, + { + "name": "Flour", + "quantity": 30, + "category": "Pantry", + }, + # Spices & Seasonings + { + "name": "Black Pepper", + "quantity": 20, + "category": "Spices", + }, + { + "name": "Sea Salt", + "quantity": 35, + "category": "Spices", + }, + { + "name": "Garlic Powder", + "quantity": 5, + "category": "Spices", + }, + ] + + print("Creating inventory items...") + for item_data in inventory_data: + # Determine status based on quantity + status = "out" if item_data["quantity"] <= 0 else "low" if item_data["quantity"] <= 20 else "sufficient" + item = Inventory( + name=item_data["name"], + quantity=item_data["quantity"], + category=item_data["category"], + status=status, + ) + db.session.add(item) + + # Create dummy customers + customers_data = [ + { + "full_name": "John Smith", + "email": "john.smith@example.com", + "phone": "0700-000-001", + "password": "customer123", + }, + { + "full_name": "Emma Wilson", + "email": "emma.wilson@example.com", + "phone": "0700-000-002", + "password": "customer123", + }, + { + "full_name": "Michael Brown", + "email": "michael.brown@example.com", + "phone": "0700-000-003", + "password": "customer123", + }, + { + "full_name": "Sarah Davis", + "email": "sarah.davis@example.com", + "phone": "0700-000-004", + "password": "customer123", + }, + { + "full_name": "James Johnson", + "email": "james.johnson@example.com", + "phone": "0700-000-005", + "password": "customer123", + }, + ] + + # Create restaurants + restaurants_data = [ + # Birmingham + { + "name": "Brummie Bites", + "city": "Birmingham", + "address": "123 Corporation St", + "phone": "0121-000-0001", + }, + { + "name": "The Spiced Horizon", + "city": "Birmingham", + "address": "45 New Street", + "phone": "0121-000-0002", + }, + # Bristol + { + "name": "Harbour & Hearth", + "city": "Bristol", + "address": "78 Harbourside", + "phone": "0117-000-0001", + }, + { + "name": "West End Whisk", + "city": "Bristol", + "address": "90 Park Street", + "phone": "0117-000-0002", + }, + # Cardiff + { + "name": "Cymru Cravings", + "city": "Cardiff", + "address": "12 Castle Street", + "phone": "029-0000-0001", + }, + { + "name": "Dragon's Dish", + "city": "Cardiff", + "address": "34 Queen Street", + "phone": "029-0000-0002", + }, + # Glasgow + { + "name": "Clyde & Cuisine", + "city": "Glasgow", + "address": "56 Buchanan Street", + "phone": "0141-000-0001", + }, + { + "name": "The Tartan Table", + "city": "Glasgow", + "address": "78 Sauchiehall Street", + "phone": "0141-000-0002", + }, + # Manchester + { + "name": "Northern Nosh", + "city": "Manchester", + "address": "91 Deansgate", + "phone": "0161-000-0001", + }, + { + "name": "The Manc Fork", + "city": "Manchester", + "address": "23 Market Street", + "phone": "0161-000-0002", + }, + # Nottingham + { + "name": "Robin's Feast", + "city": "Nottingham", + "address": "45 Derby Road", + "phone": "0115-000-0001", + }, + { + "name": "Sherwood Spoon", + "city": "Nottingham", + "address": "67 Forest Road", + "phone": "0115-000-0002", + }, + # London + { + "name": "The London Larder", + "city": "London", + "address": "89 Oxford Street", + "phone": "020-0000-0001", + }, + { + "name": "Fog & Fork", + "city": "London", + "address": "12 Baker Street", + "phone": "020-0000-0002", + }, + ] + + # Create dishes data + dishes = [ + { + "name": "Slow-cooked Beef Brisket", + "description": "Tender beef brisket served with smoked paprika mash", + "price": 24.99, + }, + { + "name": "Tandoori Lamb Cutlets", + "description": "Spiced lamb cutlets with saffron yogurt drizzle", + "price": 26.99, + }, + { + "name": "Pan-seared Sea Bass", + "description": "Fresh sea bass with lemon-thyme butter", + "price": 23.99, + }, + { + "name": "Whisky-glazed Duck Breast", + "description": "Duck breast with wild mushroom risotto", + "price": 28.99, + }, + { + "name": "Leek and Lamb Pie", + "description": "Traditional pie with minted pea purée", + "price": 19.99, + }, + { + "name": "Chili Honey Chicken", + "description": "Spicy-sweet chicken with roasted root vegetables", + "price": 18.99, + }, + { + "name": "Smoked Haddock Chowder", + "description": "Creamy fish soup served with oatcakes", + "price": 16.99, + }, + { + "name": "Venison Medallions", + "description": "Tender venison with whisky peppercorn sauce", + "price": 29.99, + }, + { + "name": "Steak & Ale Pudding", + "description": "Classic pudding with mushy peas", + "price": 21.99, + }, + { + "name": "Spicy Chicken Tikka Mac", + "description": "Fusion dish combining tikka and mac & cheese", + "price": 17.99, + }, + { + "name": "Game Stew", + "description": "Rich stew with herb dumplings", + "price": 22.99, + }, + { + "name": "Wild Mushroom Tagliatelle", + "description": "Pasta with truffle oil", + "price": 19.99, + }, + { + "name": "Duck Confit", + "description": "Classic duck confit with parsnip purée and red wine jus", + "price": 27.99, + }, + { + "name": "Chargrilled Aubergine Stack", + "description": "Vegetarian delight with tahini drizzle", + "price": 16.99, + }, + ] + + try: + # Create customers + print("Creating customers...") + for customer_data in customers_data: + customer = User( + full_name=customer_data["full_name"], + email=customer_data["email"], + phone=customer_data["phone"], + password_hash=generate_password_hash(customer_data["password"]), + role="customer", + ) + db.session.add(customer) + db.session.flush() + print(f"Created {len(customers_data)} customers") + + # Create staff members + staff_data = [ + { + "full_name": "David Thompson", + "email": "david.thompson@restaurant.com", + "phone": "0700-111-001", + "password": "staffpass123", + "role": "manager", + }, + { + "full_name": "Rachel Chen", + "email": "rachel.chen@restaurant.com", + "phone": "0700-111-002", + "password": "staffpass123", + "role": "staff", + }, + ] + + print("Creating staff members...") + created_staff = [] + for staff_member in staff_data: + staff = User( + full_name=staff_member["full_name"], + email=staff_member["email"], + phone=staff_member["phone"], + password_hash=generate_password_hash(staff_member["password"]), + role=staff_member["role"], + ) + db.session.add(staff) + db.session.flush() + created_staff.append(staff) + + # Create attendance records for the last 30 days + print("Creating attendance records...") + end_date = datetime.now() + start_date = end_date - timedelta(days=30) + current_date = start_date + + while current_date <= end_date: + for staff in created_staff: + # Randomly assign attendance status + import random + + status = random.choices( + ["present", "late", "absent"], weights=[0.8, 0.15, 0.05] + )[0] + + if status != "absent": + clock_in = datetime.strptime("09:00", "%H:%M").time() + if status == "late": + clock_in = datetime.strptime("09:30", "%H:%M").time() + + attendance = Attendance( + user_id=staff.id, + date=current_date.date(), + clock_in=clock_in, + clock_out=datetime.strptime("17:00", "%H:%M").time(), + status=status, + ) + db.session.add(attendance) + + current_date += timedelta(days=1) + + # Create performance records + print("Creating performance records...") + current_month = datetime.now().strftime("%Y-%m") + for staff in created_staff: + performance = Performance( + user_id=staff.id, + review_period=current_month, + orders_handled=random.randint(50, 200), + rating=round(random.uniform(3.5, 5.0), 2), + attendance_rate=round(random.uniform(85, 100), 2), + punctuality_rate=round(random.uniform(90, 100), 2), + notes="Regular performance review", + ) + db.session.add(performance) + + db.session.commit() + print("Staff data created successfully!") + + # Create restaurants + print("Creating restaurants...") + created_restaurants = [] + for restaurant_data in restaurants_data: + restaurant = Restaurant( + name=restaurant_data["name"], + city=restaurant_data["city"], + address=restaurant_data["address"], + phone=restaurant_data["phone"], + ) + db.session.add(restaurant) + db.session.flush() # Get the ID without committing + created_restaurants.append(restaurant) + print(f"Created {len(created_restaurants)} restaurants") + + # Add dishes to each restaurant with slight price variations + print("Creating menu items...") + menu_items_count = 0 + for restaurant in created_restaurants: + for dish in dishes: + # Add random price variation (-1 to +1) for each restaurant + import random + + price_variation = random.uniform(-1, 1) + menu_item = Menu( + name=dish["name"], + description=dish["description"], + price=round(dish["price"] + price_variation, 2), + restaurant_id=restaurant.id, + available=True, + ) + db.session.add(menu_item) + menu_items_count += 1 + + db.session.commit() + print(f"Created {menu_items_count} menu items") + + print("Creating orders and payments...") + users = User.query.filter_by(role="customer").all() + restaurants = Restaurant.query.all() + menu_items = Menu.query.all() + + # Generate orders for the last 6 months + end_date = datetime.utcnow() + start_date = end_date - timedelta(days=180) + + orders_count = 0 + payments_count = 0 + + while start_date <= end_date: + # Create 3-7 orders per day + daily_orders = random.randint(3, 7) + + for _ in range(daily_orders): + user = random.choice(users) + restaurant = random.choice(restaurants) + + # Select 1-4 menu items for the order + order_items = random.sample(menu_items, random.randint(1, 4)) + total_amount = sum(item.price for item in order_items) + + # Determine order status first + order_status = random.choice( + ["completed", "completed", "completed", "pending", "preparing", "ready"] + ) + + # Set payment status based on order status + if order_status == "completed": + payment_status = "paid" + else: + payment_status = "pending" + + # Create order + order = Order( + restaurant_id=restaurant.id, + user_id=user.id, + table_number=random.randint(1, 20), + total_amount=total_amount, + service_fee=total_amount * Decimal('0.05'), # 5% service fee + vat=(total_amount * Decimal('1.05')) * Decimal('0.10'), # 10% VAT on subtotal + service fee + grand_total=total_amount * Decimal('1.155'), # total + 5% service fee + 10% VAT + order_status=order_status, + payment_status=payment_status, + created_at=start_date, + ) + db.session.add(order) + db.session.flush() + + # Create order items + for menu_item in order_items: + quantity = random.randint(1, 3) + subtotal = menu_item.price * quantity + order_item = OrderItem( + order_id=order.id, + menu_id=menu_item.id, + quantity=quantity, + subtotal=subtotal, + ) + db.session.add(order_item) + + # Create payment for completed orders + if order_status == "completed": + payment = Payment( + order_id=order.id, + user_id=user.id, + amount=total_amount, + transaction_id=f"TXN-{random.randint(100000, 999999)}", + status="success", + created_at=start_date, + ) + db.session.add(payment) + payments_count += 1 + + orders_count += 1 + + start_date += timedelta(days=1) + + db.session.commit() + print(f"Created {orders_count} orders and {payments_count} payments") + + print("Creating reservations...") + today = datetime.now() + reservations = [ + # Pending reservations + Reservation( + restaurant_id=created_restaurants[0].id, + user_id=users[0].id, # John Smith + reservation_date=(today + timedelta(days=1)).date(), + reservation_time=datetime.strptime("19:00", "%H:%M").time(), + guest_count=4, + table_number="A1", + notes="Birthday celebration", + status="pending", + ), + # Preparing reservations + Reservation( + restaurant_id=created_restaurants[1].id, + user_id=users[1].id, # Emma Wilson + reservation_date=(today + timedelta(days=2)).date(), + reservation_time=datetime.strptime("20:00", "%H:%M").time(), + guest_count=2, + table_number="B3", + notes="Window seat preferred", + status="preparing", + ), + # Ready reservations + Reservation( + restaurant_id=created_restaurants[2].id, + user_id=users[2].id, # Michael Brown + reservation_date=today.date(), + reservation_time=datetime.strptime("18:30", "%H:%M").time(), + guest_count=6, + table_number="C2", + notes="Anniversary dinner", + status="ready", + ), + # Completed reservations + Reservation( + restaurant_id=created_restaurants[0].id, + user_id=users[0].id, + reservation_date=(today - timedelta(days=1)).date(), + reservation_time=datetime.strptime("19:30", "%H:%M").time(), + guest_count=3, + table_number="A4", + status="completed", + ), + # Cancelled reservations + Reservation( + restaurant_id=created_restaurants[1].id, + user_id=users[1].id, + reservation_date=(today - timedelta(days=2)).date(), + reservation_time=datetime.strptime("18:00", "%H:%M").time(), + guest_count=2, + table_number="B1", + status="cancelled", + ), + ] + + for reservation in reservations: + db.session.add(reservation) + db.session.commit() + print(f"Created {len(reservations)} reservations") + + print("Creating discount offers...") + discounts_data = [ + { + "code": "WELCOME25", + "description": "Welcome Discount 25% Off", + "discount_percent": 25.0, + "valid_from": datetime.now() - timedelta(days=30), + "valid_to": datetime.now() + timedelta(days=60), + "status": "active", + }, + { + "code": "SUMMER2025", + "description": "Summer Special 15% Off", + "discount_percent": 15.0, + "valid_from": datetime.now(), + "valid_to": datetime.now() + timedelta(days=90), + "status": "active", + }, + { + "code": "EXPIRED10", + "description": "Past Promotion 10% Off", + "discount_percent": 10.0, + "valid_from": datetime.now() - timedelta(days=60), + "valid_to": datetime.now() - timedelta(days=30), + "status": "expired", + }, + ] + + for discount_data in discounts_data: + discount = Discount( + restaurant_id=random.choice(created_restaurants).id, + **discount_data, + ) + db.session.add(discount) + db.session.commit() + print(f"Created {len(discounts_data)} discount offers") + + print("Dummy data created successfully!") + + except Exception as e: + db.session.rollback() + print(f"Error creating dummy data: {str(e)}") + print("Traceback:") + traceback.print_exc() + + except Exception as e: + print(f"Error setting up database connection: {str(e)}") + print("Traceback:") + traceback.print_exc() + + +if __name__ == "__main__": + create_dummy_data() diff --git a/app/models.py b/app/models.py index ae22946d94070945a531617189b899f6aa514ad3..d67a01c5472cedc1c160d96d2af245ef94ee1ca2 100644 --- a/app/models.py +++ b/app/models.py @@ -1,10 +1,16 @@ from flask_sqlalchemy import SQLAlchemy from datetime import datetime +# Initialize SQLAlchemy database instance db = SQLAlchemy() class Restaurant(db.Model): + """ + Restaurant Model - Represents restaurant entities in the system + Stores basic information about restaurants including name, location, and contact details + """ + __tablename__ = "restaurants" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(255), nullable=False) @@ -16,6 +22,12 @@ class Restaurant(db.Model): class User(db.Model): + """ + User Model - Represents all system users including staff and customers + Users can have different roles (admin, manager, staff, customer) with different permissions + Users can be associated with specific restaurants (especially for staff) + """ + __tablename__ = "users" id = db.Column(db.Integer, primary_key=True) restaurant_id = db.Column( @@ -30,6 +42,11 @@ class User(db.Model): class Menu(db.Model): + """ + Menu Model - Represents menu items available at restaurants + Each menu item belongs to a specific restaurant and has details like price and availability + """ + __tablename__ = "menu" id = db.Column(db.Integer, primary_key=True) restaurant_id = db.Column( @@ -44,6 +61,12 @@ class Menu(db.Model): class Order(db.Model): + """ + Order Model - Represents customer orders at restaurants + Tracks the order status, payment status, and total amount + Links to the restaurant and the user (customer) who placed the order + """ + __tablename__ = "orders" id = db.Column(db.Integer, primary_key=True) restaurant_id = db.Column( @@ -52,6 +75,13 @@ class Order(db.Model): user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="SET NULL")) table_number = db.Column(db.Integer) total_amount = db.Column(db.Numeric(10, 2)) + discount_id = db.Column( + db.Integer, db.ForeignKey("discounts.id", ondelete="SET NULL") + ) + discount_amount = db.Column(db.Numeric(10, 2)) # Amount of discount applied + service_fee = db.Column(db.Numeric(10, 2)) # 5% service fee + vat = db.Column(db.Numeric(10, 2)) # 10% VAT + grand_total = db.Column(db.Numeric(10, 2)) # total_amount + service_fee + vat order_status = db.Column( db.Enum("pending", "preparing", "ready", "completed", "cancelled"), default="pending", @@ -61,6 +91,12 @@ class Order(db.Model): class OrderItem(db.Model): + """ + OrderItem Model - Represents individual items within an order + Links each order item to its parent order and the menu item that was ordered + Tracks quantity and subtotal price for each item + """ + __tablename__ = "order_items" id = db.Column(db.Integer, primary_key=True) order_id = db.Column(db.Integer, db.ForeignKey("orders.id", ondelete="CASCADE")) @@ -70,48 +106,126 @@ class OrderItem(db.Model): class Payment(db.Model): + """ + Payment Model - Records payment transactions for orders + Supports different payment methods and tracks payment status + Links to the order being paid for and the user making the payment + """ + __tablename__ = "payments" id = db.Column(db.Integer, primary_key=True) order_id = db.Column(db.Integer, db.ForeignKey("orders.id", ondelete="CASCADE")) user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="SET NULL")) amount = db.Column(db.Numeric(10, 2), nullable=False) - payment_method = db.Column(db.Enum("cash", "credit_card", "paypal"), nullable=False) + payment_method = db.Column(db.Enum("credit_card", "paypal", "cash"), nullable=True) transaction_id = db.Column(db.String(100), unique=True) status = db.Column(db.Enum("success", "failed", "pending"), default="pending") created_at = db.Column(db.DateTime, default=datetime.utcnow) class Reservation(db.Model): + """ + Reservation Model - Manages table reservations in restaurants + Links customers, restaurants, and specific tables together for reservations + """ + __tablename__ = "reservations" + id = db.Column(db.Integer, primary_key=True) restaurant_id = db.Column( - db.Integer, db.ForeignKey("restaurants.id", ondelete="CASCADE") + db.Integer, db.ForeignKey("restaurants.id", ondelete="CASCADE"), nullable=False + ) + user_id = db.Column( + db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + reservation_date = db.Column(db.Date, nullable=False) + reservation_time = db.Column(db.Time, nullable=False) + guest_count = db.Column(db.Integer, nullable=False) + table_number = db.Column(db.String(10)) + notes = db.Column(db.Text) + status = db.Column( + db.Enum( + "pending", + "preparing", + "ready", + "completed", + "cancelled", + name="reservation_status", + ), + default="pending", ) - user_id = db.Column(db.Integer, db.ForeignKey("users.id", ondelete="SET NULL")) - table_number = db.Column(db.Integer) - reservation_time = db.Column(db.DateTime, nullable=False) - status = db.Column(db.Enum("pending", "confirmed", "cancelled"), default="pending") created_at = db.Column(db.DateTime, default=datetime.utcnow) - name = db.Column(db.String(255), nullable=False) - contact = db.Column(db.String(255), nullable=False) + updated_at = db.Column( + db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) + + # Relationships + restaurant = db.relationship( + "Restaurant", + backref=db.backref("reservations", lazy=True, cascade="all, delete-orphan"), + ) + user = db.relationship( + "User", + backref=db.backref("reservations", lazy=True, cascade="all, delete-orphan"), + ) + + def __repr__(self): + return f"<Reservation {self.id}: {self.user.name} at {self.restaurant.name}>" + + @property + def formatted_datetime(self): + """Returns the formatted date and time of the reservation""" + return f"{self.reservation_date.strftime('%Y-%m-%d')} {self.reservation_time.strftime('%H:%M')}" class Inventory(db.Model): + """ + Inventory Model - Tracks inventory items and quantities for restaurants + Used for stock management and monitoring inventory levels + """ + __tablename__ = "inventory" id = db.Column(db.Integer, primary_key=True) restaurant_id = db.Column( db.Integer, db.ForeignKey("restaurants.id", ondelete="CASCADE") ) - item_name = db.Column(db.String(255), nullable=False) + name = db.Column(db.String(255), nullable=False) quantity = db.Column(db.Integer, nullable=False) - category = db.Column(db.String(100), nullable=False) - status = db.Column(db.String(50), nullable=False) - last_updated = db.Column( + category = db.Column(db.String(50), nullable=False) + status = db.Column(db.Enum("low", "sufficient", "out"), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column( db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow ) + # Relationship + restaurant = db.relationship( + "Restaurant", backref=db.backref("inventory_items", lazy=True) + ) + + def __repr__(self): + return f"<Inventory {self.name}>" + + @property + def serialize(self): + """Return object data in easily serializable format""" + return { + "id": self.id, + "name": self.name, + "quantity": self.quantity, + "category": self.category, + "status": self.status, + "restaurant_id": self.restaurant_id, + } + class Discount(db.Model): + """ + Discount Model - Manages promotional discounts and offers + Tracks discount codes, validity periods, and discount percentages + Links to the restaurant offering the discount + """ + __tablename__ = "discounts" id = db.Column(db.Integer, primary_key=True) restaurant_id = db.Column( @@ -125,15 +239,53 @@ class Discount(db.Model): status = db.Column(db.Enum("active", "expired"), default="active") created_at = db.Column(db.DateTime, default=datetime.utcnow) + def is_valid(self): + """Check if the discount is currently valid""" + today = datetime.now().date() + return self.valid_from <= today <= self.valid_to and self.status == "active" + -class Report(db.Model): - __tablename__ = "reports" +class Attendance(db.Model): + """ + Attendance Model - Tracks staff attendance records + """ + + __tablename__ = "attendance" id = db.Column(db.Integer, primary_key=True) - restaurant_id = db.Column( - db.Integer, db.ForeignKey("restaurants.id", ondelete="CASCADE") + user_id = db.Column( + db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), nullable=False ) - report_type = db.Column( - db.Enum("sales", "inventory", "customer_feedback"), nullable=False + date = db.Column(db.Date, nullable=False) + clock_in = db.Column(db.Time, nullable=False) + clock_out = db.Column(db.Time) + status = db.Column(db.Enum("present", "late", "absent", "half-day"), nullable=False) + notes = db.Column(db.Text) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + # Relationship + user = db.relationship("User", backref=db.backref("attendance_records", lazy=True)) + + +class Performance(db.Model): + """ + Performance Model - Tracks staff performance metrics + """ + + __tablename__ = "performance" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column( + db.Integer, db.ForeignKey("users.id", ondelete="CASCADE"), nullable=False ) - generated_at = db.Column(db.DateTime, default=datetime.utcnow) - data = db.Column(db.JSON, nullable=False) + review_period = db.Column(db.String(50)) # e.g., "2025-Q1", "2025-03" + orders_handled = db.Column(db.Integer, default=0) + rating = db.Column(db.Numeric(3, 2)) # Rating out of 5 + attendance_rate = db.Column(db.Numeric(5, 2)) # Percentage + punctuality_rate = db.Column(db.Numeric(5, 2)) # Percentage + notes = db.Column(db.Text) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column( + db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow + ) + + # Relationship + user = db.relationship("User", backref=db.backref("performance_records", lazy=True)) diff --git a/app/routes/auth.py b/app/routes/auth.py index 08c8c6272217d779b4331d39521287dd7bd4721f..310f3c820a0fd6e9258b1f141d651e100ce1e114 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -11,10 +11,22 @@ from flask import ( from werkzeug.security import check_password_hash, generate_password_hash from app.models import db, User +# Create authentication blueprint auth_bp = Blueprint("auth", __name__) def login_required(view): + """ + Decorator to protect routes that require authentication + Redirects unauthenticated users to the login page + + Args: + view: The view function to protect + + Returns: + The wrapped view function that checks for authentication + """ + @functools.wraps(view) def wrapped_view(**kwargs): if "user_id" not in session: @@ -26,6 +38,13 @@ def login_required(view): @auth_bp.route("/login", methods=["GET", "POST"]) def login(): + """ + User login route + Handles both GET requests (displaying the login form) and + POST requests (processing login form submission) + + For POST requests, validates user credentials and creates a session + """ if request.method == "POST": email = request.form["email"] password = request.form["password"] @@ -43,27 +62,39 @@ def login(): @auth_bp.route("/register", methods=["GET", "POST"]) def register(): + """ + User registration route + Handles both GET requests (displaying the registration form) and + POST requests (processing registration form submission) + + For POST requests, validates form data, checks for existing users, + and creates a new user account + """ if request.method == "POST": full_name = request.form["full_name"] email = request.form["email"] phone = request.form["phone"] password = request.form["password"] repeat_password = request.form["repeat_password"] - role = "customer" + role = "admin" + # Validate required fields if not full_name or not email or not password or not role: flash("All fields are required") return redirect(url_for("auth.register")) + # Validate password confirmation if password != repeat_password: flash("Passwords do not match") return redirect(url_for("auth.register")) + # Check if user already exists user = User.query.filter_by(email=email).first() if user is not None: flash("Email is already registered") return redirect(url_for("auth.register")) + # Create new user account new_user = User( full_name=full_name, email=email, @@ -80,6 +111,10 @@ def register(): @auth_bp.route("/signout") def signout(): + """ + User sign out/logout route + Clears the user session and redirects to the login page + """ session.clear() flash("Logged out successfully", category="success") return redirect(url_for("auth.login")) @@ -87,4 +122,8 @@ def signout(): @auth_bp.route("/forget_password") def forget_password(): + """ + Password recovery route + Displays the password reset form + """ return render_template("users/forget_pass.html") diff --git a/app/routes/discounts.py b/app/routes/discounts.py index af9b6e4a664330e6cfddc5f29d6871e312f25f8f..9ea03475b14e270b58afd67ca68ad2af0d0282cf 100644 --- a/app/routes/discounts.py +++ b/app/routes/discounts.py @@ -1,8 +1,111 @@ -from flask import Blueprint, render_template +from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify +from app.models import db, Discount +from datetime import datetime + discounts_bp = Blueprint("discounts", __name__) -@discounts_bp.route("/discount/offers") +@discounts_bp.route("/offers") def offers(): - return render_template("discounts/offers.html") + # Get all discounts and sort them by status and valid_from date + discounts = Discount.query.all() + today = datetime.now().date() + + # Process discounts to add dynamic status + for discount in discounts: + if discount.valid_from > today: + discount.dynamic_status = "upcoming" + elif discount.valid_to < today: + discount.dynamic_status = "expired" + else: + discount.dynamic_status = "active" + + return render_template("discounts/offers.html", discounts=discounts) + + +@discounts_bp.route("/offers/add", methods=["POST"]) +def add_offer(): + try: + # Get form data + name = request.form.get("name") + discount_type = request.form.get("discount_type") + # Convert to Decimal for precise calculations + discount_value = str(request.form.get("discount_value")) + valid_from = datetime.strptime( + request.form.get("valid_from"), "%Y-%m-%d" + ).date() + valid_to = datetime.strptime(request.form.get("valid_until"), "%Y-%m-%d").date() + + # Create new discount + new_discount = Discount( + code=name.replace(" ", "_").lower(), + description=name, + discount_percent=discount_value if discount_type == "percentage" else None, + valid_from=valid_from, + valid_to=valid_to, + status="active", + ) + + db.session.add(new_discount) + db.session.commit() + + flash("Offer added successfully!", "success") + except Exception as e: + db.session.rollback() + flash(f"Error adding offer: {str(e)}", "error") + + return redirect(url_for("discounts.offers")) + + +@discounts_bp.route("/offers/edit/<int:id>", methods=["POST"]) +def edit_offer(id): + try: + discount = Discount.query.get_or_404(id) + + # Update discount details using Decimal + discount.description = request.form.get("name") + discount.discount_percent = str(request.form.get("discount_value")) + discount.valid_from = datetime.strptime( + request.form.get("valid_from"), "%Y-%m-%d" + ).date() + discount.valid_to = datetime.strptime( + request.form.get("valid_until"), "%Y-%m-%d" + ).date() + + db.session.commit() + flash("Offer updated successfully!", "success") + except Exception as e: + db.session.rollback() + flash(f"Error updating offer: {str(e)}", "error") + + return redirect(url_for("discounts.offers")) + + +@discounts_bp.route("/api/discounts/available") +def get_available_discounts(): + today = datetime.now().date() + + # Get all active discounts + discounts = Discount.query.filter( + Discount.valid_from <= today, + Discount.valid_to >= today, + Discount.status == "active", + ).all() + + return jsonify( + { + "success": True, + "discounts": [ + { + "id": d.id, + "code": d.code, + "description": d.description, + # Convert Decimal to string for JSON serialization + "discount_percent": str(d.discount_percent), + "valid_to": d.valid_to.isoformat(), + } + for d in discounts + ], + } + ) diff --git a/app/routes/home.py b/app/routes/home.py index abbb8ca0c248b25c0f5f86e9708e1fb7f2b590d0..a29452445a8866e79eb65caa606041b26047a7f6 100644 --- a/app/routes/home.py +++ b/app/routes/home.py @@ -1,5 +1,8 @@ -from flask import Blueprint, render_template +from flask import Blueprint, render_template, request from app.routes.auth import login_required +from app.models import Order, Menu, User, Payment, db +from sqlalchemy import func +from datetime import datetime, timedelta home_bp = Blueprint("home", __name__) @@ -7,4 +10,169 @@ home_bp = Blueprint("home", __name__) @home_bp.route("/") @login_required def homepage(): - return render_template("home.html") + # Get total revenue from successful payments + total_revenue = ( + db.session.query(func.sum(Payment.amount)) + .filter(Payment.status == "success") + .scalar() + or 0 + ) + + # Get total number of orders + total_orders = Order.query.count() + + # Get total number of menu items + total_menu = Menu.query.count() + + # Get total number of staff (excluding customers) + total_staff = User.query.filter(User.role != "customer").count() + + # Get time period from request args, default to last 6 months + period = request.args.get("period", "last_6_months") + + # Get orders data for the last 6 months + today = datetime.utcnow() + if period == "last_3_months": + start_date = today - timedelta(days=90) + elif period == "this_year": + start_date = datetime(today.year, 1, 1) + else: # default to last 6 months + start_date = today - timedelta(days=180) + + # Get time period for revenue from request args + revenue_period = request.args.get("revenue_period", "monthly") + + # Calculate revenue data based on period + if revenue_period == "daily": + revenue_data = ( + db.session.query( + func.DATE_FORMAT(Payment.created_at, "%Y-%m-%d").label("date"), + func.sum(Payment.amount).label("total"), + ) + .filter(Payment.status == "success", Payment.created_at >= start_date) + .group_by(func.DATE_FORMAT(Payment.created_at, "%Y-%m-%d")) + .order_by(func.DATE_FORMAT(Payment.created_at, "%Y-%m-%d")) + .all() + ) + # Format daily data + revenue_labels = [] + revenue_amounts = [] + for date_str, amount in revenue_data: + date = datetime.strptime(date_str, "%Y-%m-%d") + revenue_labels.append(date.strftime("%d %b")) + revenue_amounts.append(float(amount)) + + elif revenue_period == "weekly": + revenue_data = ( + db.session.query( + func.DATE_FORMAT(Payment.created_at, "%X-%V").label("week"), + func.sum(Payment.amount).label("total"), + ) + .filter(Payment.status == "success", Payment.created_at >= start_date) + .group_by(func.DATE_FORMAT(Payment.created_at, "%X-%V")) + .order_by(func.DATE_FORMAT(Payment.created_at, "%X-%V")) + .all() + ) + # Format weekly data + revenue_labels = [] + revenue_amounts = [] + for week_str, amount in revenue_data: + year, week = week_str.split("-") + revenue_labels.append(f"Week {week}") + revenue_amounts.append(float(amount)) + + elif revenue_period == "yearly": + revenue_data = ( + db.session.query( + func.DATE_FORMAT(Payment.created_at, "%Y").label("year"), + func.sum(Payment.amount).label("total"), + ) + .filter(Payment.status == "success") + .group_by(func.DATE_FORMAT(Payment.created_at, "%Y")) + .order_by(func.DATE_FORMAT(Payment.created_at, "%Y")) + .all() + ) + # Format yearly data + revenue_labels = [] + revenue_amounts = [] + for year_str, amount in revenue_data: + revenue_labels.append(year_str) + revenue_amounts.append(float(amount)) + + else: # monthly (default) + revenue_data = ( + db.session.query( + func.DATE_FORMAT(Payment.created_at, "%Y-%m").label("month"), + func.sum(Payment.amount).label("total"), + ) + .filter(Payment.status == "success", Payment.created_at >= start_date) + .group_by(func.DATE_FORMAT(Payment.created_at, "%Y-%m")) + .order_by(func.DATE_FORMAT(Payment.created_at, "%Y-%m")) + .all() + ) + # Format monthly data + revenue_labels = [] + revenue_amounts = [] + for month_str, amount in revenue_data: + month_date = datetime.strptime(month_str, "%Y-%m") + revenue_labels.append(month_date.strftime("%b")) + revenue_amounts.append(float(amount)) + + monthly_orders = ( + db.session.query( + func.DATE_FORMAT(Order.created_at, "%Y-%m").label("month"), + func.count(Order.id).label("count"), + ) + .filter(Order.created_at >= start_date) + .group_by(func.DATE_FORMAT(Order.created_at, "%Y-%m")) + .order_by(func.DATE_FORMAT(Order.created_at, "%Y-%m")) + .all() + ) + + # Format data for the orders chart + order_months = [] + order_counts = [] + + for month_str, count in monthly_orders: + # Parse the month string into a datetime object + month_date = datetime.strptime(month_str, "%Y-%m") + order_months.append(month_date.strftime("%b")) + order_counts.append(count) + + # Get recent orders (last 5) + new_orders = ( + db.session.query(Order, User, Payment) + .join(User, Order.user_id == User.id) + .outerjoin(Payment, Order.id == Payment.order_id) + .order_by(Order.created_at.desc()) + .limit(5) + .all() + ) + + # Format orders for display + recent_orders = [] + for order, user, payment in new_orders: + recent_orders.append( + { + "id": order.id, + "customer_name": user.full_name, + "date": order.created_at.strftime("%B %d, %Y"), + "time": order.created_at.strftime("%I:%M %p"), + "amount": float(order.total_amount), + "status": order.order_status, + } + ) + + return render_template( + "home.html", + total_revenue=total_revenue, + total_orders=total_orders, + total_menu=total_menu, + total_staff=total_staff, + new_orders=recent_orders, + order_months=order_months, + order_counts=order_counts, + revenue_labels=revenue_labels, + revenue_amounts=revenue_amounts, + revenue_period=revenue_period, + ) diff --git a/app/routes/inventory.py b/app/routes/inventory.py index c293dbc71358bda64626626720da70dd01029df8..4a72fe78bbaa2ac2c409165745d76c3a449f8ae3 100644 --- a/app/routes/inventory.py +++ b/app/routes/inventory.py @@ -1,47 +1,157 @@ -from flask import Blueprint, render_template, request, redirect, url_for -from app.models import db, Inventory +from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify +from app.models import db, Inventory, Restaurant +from sqlalchemy import desc from app.routes.auth import login_required inventory_bp = Blueprint("inventory", __name__) +def update_status_based_on_quantity(quantity): + """Automatically determine status based on quantity""" + if quantity <= 0: + return "out" + elif quantity <= 20: + return "low" + else: + return "sufficient" + + @inventory_bp.route("/inventory") @login_required def manage_inventory(): - inventory = Inventory.query.all() - return render_template("inventory/manage_inventory.html", inventory=inventory) + # Get filter parameters + category = request.args.get("category") + status = request.args.get("status") + min_quantity = request.args.get("min_quantity", type=int) + max_quantity = request.args.get("max_quantity", type=int) + search = request.args.get("search", "").strip() + + # Base query + query = Inventory.query + + # Apply filters + if category: + query = query.filter(Inventory.category == category) + if status: + query = query.filter(Inventory.status == status) + if min_quantity is not None: + query = query.filter(Inventory.quantity >= min_quantity) + if max_quantity is not None: + query = query.filter(Inventory.quantity <= max_quantity) + if search: + query = query.filter(Inventory.name.ilike(f"%{search}%")) + + # Get distinct categories for filter dropdown + categories = db.session.query(Inventory.category).distinct().all() + categories = [cat[0] for cat in categories] + + # Get results ordered by category and name + inventory_items = query.order_by(Inventory.category, Inventory.name).all() + + return render_template( + "inventory/manage_inventory.html", + inventory=inventory_items, + categories=categories, + ) @inventory_bp.route("/inventory/add", methods=["POST"]) +@login_required def add_item(): - new_item = Inventory( - item_name=request.form["name"], - quantity=request.form["quantity"], - category=request.form["category"], - status=request.form["status"], - ) - db.session.add(new_item) - db.session.commit() + try: + name = request.form["name"].strip() + quantity = int(request.form["quantity"]) + category = request.form["category"].strip() + + # Validate input + if not name or not category: + flash("Name and category are required fields.", "error") + return redirect(url_for("inventory.manage_inventory")) + + if quantity < 0: + flash("Quantity cannot be negative.", "error") + return redirect(url_for("inventory.manage_inventory")) + + # Determine status based on quantity + status = update_status_based_on_quantity(quantity) + + # Check for existing item with same name + existing_item = Inventory.query.filter_by(name=name).first() + if existing_item: + flash("An item with this name already exists.", "error") + return redirect(url_for("inventory.manage_inventory")) + + new_item = Inventory( + name=name, quantity=quantity, category=category, status=status + ) + db.session.add(new_item) + db.session.commit() + flash("Item added successfully!", "success") + except ValueError: + flash("Invalid quantity value.", "error") + except Exception as e: + db.session.rollback() + flash("Error adding item. Please try again.", "error") + print(f"Error adding inventory item: {str(e)}") + return redirect(url_for("inventory.manage_inventory")) @inventory_bp.route("/inventory/edit", methods=["POST"]) +@login_required def edit_item(): - item_id = int(request.form["item_id"]) - item = Inventory.query.get(item_id) - if item: - item.item_name = request.form["name"] - item.quantity = request.form["quantity"] - item.category = request.form["category"] - item.status = request.form["status"] + try: + item_id = int(request.form["item_id"]) + item = Inventory.query.get_or_404(item_id) + + # Update fields + item.name = request.form["name"].strip() + item.quantity = int(request.form["quantity"]) + item.category = request.form["category"].strip() + + # Validate input + if not item.name or not item.category: + flash("Name and category are required fields.", "error") + return redirect(url_for("inventory.manage_inventory")) + + if item.quantity < 0: + flash("Quantity cannot be negative.", "error") + return redirect(url_for("inventory.manage_inventory")) + + # Update status based on new quantity + item.status = update_status_based_on_quantity(item.quantity) + db.session.commit() + flash("Item updated successfully!", "success") + except ValueError: + flash("Invalid quantity value.", "error") + except Exception as e: + db.session.rollback() + flash("Error updating item. Please try again.", "error") + print(f"Error updating inventory item: {str(e)}") + return redirect(url_for("inventory.manage_inventory")) @inventory_bp.route("/inventory/delete/<int:item_id>") +@login_required def delete_item(item_id): - item = Inventory.query.get(item_id) - if item: + try: + item = Inventory.query.get_or_404(item_id) db.session.delete(item) db.session.commit() + flash("Item deleted successfully!", "success") + except Exception as e: + db.session.rollback() + flash("Error deleting item. Please try again.", "error") + print(f"Error deleting inventory item: {str(e)}") + return redirect(url_for("inventory.manage_inventory")) + + +@inventory_bp.route("/api/inventory/categories") +@login_required +def get_categories(): + """API endpoint to get all unique categories""" + categories = db.session.query(Inventory.category).distinct().all() + return jsonify([cat[0] for cat in categories]) diff --git a/app/routes/menu.py b/app/routes/menu.py new file mode 100644 index 0000000000000000000000000000000000000000..4d469666223aaecadf42c2a9265ed6c20f445691 --- /dev/null +++ b/app/routes/menu.py @@ -0,0 +1,112 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash +from app.models import Menu, db +from sqlalchemy.exc import IntegrityError + + +menu_bp = Blueprint("menu", __name__) + + +@menu_bp.route("/menu") +def menu(): + page = request.args.get("page", 1, type=int) + per_page = 10 + + # Get paginated results + pagination = Menu.query.paginate(page=page, per_page=per_page, error_out=False) + menu_items = pagination.items + + return render_template( + "menu/menu.html", menu_items=menu_items, pagination=pagination + ) + + +@menu_bp.route("/menu/add", methods=["GET", "POST"]) +def add_new_menu_item(): + if request.method == "POST": + name = request.form.get("name") + price = request.form.get("price") + description = request.form.get("description") + + # Validate required fields + if not name or not price: + flash("Name and price are required.", "error") + return redirect(url_for("menu.add_new_menu_item")) + + # Validate price + try: + price_float = float(price) + if price_float <= 0: + flash("Price must be a positive number.", "error") + return redirect(url_for("menu.add_new_menu_item")) + except ValueError: + flash("Invalid price format. Please enter a valid number.", "error") + return redirect(url_for("menu.add_new_menu_item")) + + # Validate name length (example constraint) + if len(name.strip()) < 2: + flash("Name must be at least 2 characters long.", "error") + return redirect(url_for("menu.add_new_menu_item")) + + try: + new_menu_item = Menu( + name=name.strip(), + price=price_float, + description=description.strip() if description else None, + available=True, + ) + db.session.add(new_menu_item) + db.session.commit() + flash("Menu item added successfully!", "success") + return redirect(url_for("menu.menu")) + except IntegrityError: + db.session.rollback() + flash("A menu item with this name already exists.", "error") + return redirect(url_for("menu.add_new_menu_item")) + except Exception as e: + db.session.rollback() + flash(f"Error adding menu item: {str(e)}", "error") + return redirect(url_for("menu.add_new_menu_item")) + + return render_template("menu/add_new_dish.html") + + +@menu_bp.route("/menu/update/<int:item_id>", methods=["GET", "POST"]) +def update_menu_item(item_id): + menu_item = Menu.query.get_or_404(item_id) + + if request.method == "POST": + name = request.form.get("name") + price = request.form.get("price") + description = request.form.get("description") + available = request.form.get("available") == "on" + + if name: + menu_item.name = name + if price: + menu_item.price = price + if description: + menu_item.description = description + menu_item.available = available + + try: + db.session.commit() + flash("Menu item updated successfully!", "success") + return redirect(url_for("menu.menu")) + except Exception as e: + db.session.rollback() + flash("Error updating menu item", "error") + + return render_template("menu/add_new_dish.html", menu_item=menu_item) + + +@menu_bp.route("/menu/delete/<int:item_id>", methods=["POST"]) +def delete_menu_item(item_id): + menu_item = Menu.query.get_or_404(item_id) + try: + db.session.delete(menu_item) + db.session.commit() + flash("Menu item deleted successfully!", "success") + except Exception as e: + db.session.rollback() + flash("Error deleting menu item", "error") + return redirect(url_for("menu.menu")) diff --git a/app/routes/order.py b/app/routes/order.py new file mode 100644 index 0000000000000000000000000000000000000000..467d74d6536c8b8fa6e7b4cf20c52c3f6f7997b6 --- /dev/null +++ b/app/routes/order.py @@ -0,0 +1,424 @@ +from flask import ( + Blueprint, + render_template, + request, + redirect, + url_for, + flash, + jsonify, + session, +) +from datetime import datetime +from app.models import Order, OrderItem, Menu, db, User, Payment, Restaurant, Discount +from sqlalchemy import desc +from app.routes.auth import login_required +from decimal import Decimal + +# Create orders blueprint +order_bp = Blueprint("order", __name__) + + +def validate_order_quantities(items): + """Validate order quantities are within acceptable limits""" + MAX_ITEMS_PER_DISH = 10 + MAX_TOTAL_ITEMS = 50 + + total_items = 0 + for item in items: + qty = int(item.get("quantity", 0)) + if qty > MAX_ITEMS_PER_DISH: + return False, f"Maximum {MAX_ITEMS_PER_DISH} items allowed per dish" + total_items += qty + + if total_items > MAX_TOTAL_ITEMS: + return False, f"Maximum {MAX_TOTAL_ITEMS} total items allowed per order" + + return True, None + + +@order_bp.route("/orders") +@login_required +def order_history(): + """Display order history with filtering and pagination""" + page = request.args.get("page", 1, type=int) + per_page = 10 + status = request.args.get("status", "all") + user_id = session.get("user_id") + user = User.query.get(user_id) + + # Create base query for orders, ordered by most recent first + query = Order.query.order_by(desc(Order.created_at)) + + # Apply status filter if provided and not 'all' + if status and status.lower() != "all": + query = query.filter(Order.order_status == status.lower()) + + # Apply date range filter if both start and end dates are provided + start_date = request.args.get("start_date") + end_date = request.args.get("end_date") + if start_date and end_date: + query = query.filter(Order.created_at.between(start_date, end_date)) + + # Apply price range filters if provided + min_price = request.args.get("min_price", type=float) + max_price = request.args.get("max_price", type=float) + if min_price is not None: + query = query.filter(Order.total_amount >= min_price) + if max_price is not None: + query = query.filter(Order.total_amount <= max_price) + + # Apply payment method filter if provided + payment_method = request.args.get("payment_method") + if payment_method: + query = query.join(Payment).filter(Payment.payment_method == payment_method) + + # Get paginated results + pagination = query.paginate(page=page, per_page=per_page, error_out=False) + orders = [] + + # Fetch related data for each order + for order in pagination.items: + order_user = User.query.get(order.user_id) + payment = Payment.query.filter_by(order_id=order.id).first() + order_items = OrderItem.query.filter_by(order_id=order.id).all() + restaurant = Restaurant.query.get(order.restaurant_id) + + # Get menu item details for each order item + menu_items = [] + for item in order_items: + menu_item = Menu.query.get(item.menu_id) + if menu_item: + menu_items.append( + { + "name": menu_item.name, + "quantity": item.quantity, + "price": menu_item.price, + } + ) + + orders.append( + { + "order": order, + "user": order_user, + "payment": payment, + "menu_items": menu_items, + "restaurant": restaurant, + } + ) + + return render_template( + "orders/order_history.html", + orders=orders, + pagination=pagination, + status=status, + is_admin=(user and user.role == "admin"), + ) + + +@order_bp.route("/orders/new", methods=["GET", "POST"]) +@login_required +def create_order(): + """Create a new order with validation and security checks""" + if request.method == "POST": + try: + # Get and validate customer + customer_id = request.form.get("user_id") + if not customer_id: + flash("Please select a customer", "error") + return redirect(url_for("order.create_order")) + + customer = User.query.get(customer_id) + if not customer or customer.role != "customer": + flash("Invalid customer selected", "error") + return redirect(url_for("order.create_order")) + + # Get and validate restaurant + restaurant_id = request.form.get("restaurant_id") + if not restaurant_id: + flash("Please select a restaurant", "error") + return redirect(url_for("order.create_order")) + + restaurant = Restaurant.query.get(restaurant_id) + if not restaurant: + flash("Invalid restaurant selected", "error") + return redirect(url_for("order.create_order")) + + # Get list of menu items and quantities + item_ids = request.form.getlist("menu_id[]") + quantities = request.form.getlist("quantity[]") + + if not item_ids or not quantities: + flash("Please add at least one item to the order", "error") + return redirect(url_for("order.create_order")) + + # Validate quantities + order_items = [] + total_amount = 0.0 + + for item_id, quantity in zip(item_ids, quantities): + if not item_id or not quantity: + continue + + menu_item = Menu.query.get(int(item_id)) + if not menu_item or menu_item.restaurant_id != int(restaurant_id): + flash("Invalid menu item selected", "error") + return redirect(url_for("order.create_order")) + + if not menu_item.available: + flash(f"{menu_item.name} is currently unavailable", "error") + return redirect(url_for("order.create_order")) + + qty = int(quantity) + if qty <= 0: + flash("Quantity must be greater than 0", "error") + return redirect(url_for("order.create_order")) + + order_items.append( + {"menu_id": menu_item.id, "quantity": qty, "price": menu_item.price} + ) + + # Validate total quantities + is_valid, error_message = validate_order_quantities(order_items) + if not is_valid: + flash(error_message, "error") + return redirect(url_for("order.create_order")) + + total_amount = sum( + (item["price"]) * (item["quantity"]) for item in order_items + ) + + # Apply discount if provided + discount_id = request.form.get("applied_discount_id") + discount_amount = Decimal('0') + if discount_id: + discount = Discount.query.get(discount_id) + if discount and discount.is_valid(): + discount_amount = (total_amount * discount.discount_percent) / Decimal('100.0') + total_amount = total_amount - discount_amount + + # Calculate fees + service_fee = total_amount * Decimal('0.05') # 5% service fee + vat_base = total_amount + service_fee + vat = vat_base * Decimal('0.10') # 10% VAT + grand_total = total_amount + service_fee + vat + + # Create order with all fee information + new_order = Order( + user_id=int(customer_id), + restaurant_id=int(restaurant_id), + order_status="pending", + total_amount=total_amount, + discount_id=int(discount_id) if discount_id else None, + discount_amount=discount_amount, + service_fee=service_fee, + vat=vat, + grand_total=grand_total, + created_at=datetime.now(), + ) + db.session.add(new_order) + db.session.flush() + + # Create order items + for item in order_items: + new_order_item = OrderItem( + order_id=new_order.id, + menu_id=item["menu_id"], + quantity=item["quantity"], + subtotal=item["price"] * item["quantity"], + ) + db.session.add(new_order_item) + + db.session.commit() + flash("Order created successfully", "success") + return redirect(url_for("order.order_detail", order_id=new_order.id)) + + except Exception as e: + db.session.rollback() + flash(f"An error occurred while creating the order: {str(e)}", "error") + return redirect(url_for("order.create_order")) + + # GET request - show order form + restaurants = Restaurant.query.all() + customers = User.query.filter_by(role="customer").all() + return render_template( + "orders/create_order.html", restaurants=restaurants, users=customers + ) + + +@order_bp.route("/order/<int:order_id>/status", methods=["POST"]) +@login_required +def update_order_status(order_id): + """Update order status and sync payment status""" + data = request.get_json() + if not data or "status" not in data: + return jsonify({"success": False, "message": "Invalid request data"}), 400 + + new_status = data["status"] + valid_statuses = ["pending", "preparing", "ready", "completed", "cancelled"] + if new_status not in valid_statuses: + return jsonify({"success": False, "message": "Invalid status"}), 400 + + try: + order = Order.query.get_or_404(order_id) + + # Define valid status transitions + valid_transitions = { + "pending": ["preparing", "cancelled"], + "preparing": ["ready"], + "ready": ["completed"], + "completed": [], # No transitions allowed from completed + "cancelled": [] # No transitions allowed from cancelled + } + + # Check if the status transition is valid + if new_status not in valid_transitions.get(order.order_status, []): + return jsonify({ + "success": False, + "message": f"Cannot change status from {order.order_status} to {new_status}" + }), 400 + + old_status = order.order_status + order.order_status = new_status + + # Handle payment status updates based on order status + if new_status == "completed": + # Check if payment already exists + payment = Payment.query.filter_by(order_id=order_id).first() + if not payment: + # Create new payment record + payment = Payment( + order_id=order_id, + user_id=order.user_id, + amount=order.total_amount, + payment_method="credit_card", + status="success", + created_at=datetime.now() + ) + db.session.add(payment) + else: + # Update existing payment to success + payment.status = "success" + payment.amount = order.total_amount + + # Update order payment status + order.payment_status = "paid" + + elif new_status == "cancelled": + # Update payment status if exists + payment = Payment.query.filter_by(order_id=order_id).first() + if payment: + payment.status = "failed" + + # Update order payment status + order.payment_status = "failed" + + db.session.commit() + return jsonify({"success": True, "message": "Order status updated successfully"}) + except Exception as e: + db.session.rollback() + return jsonify({"success": False, "message": str(e)}), 500 + + +@order_bp.route("/order/<int:order_id>/cancel", methods=["POST"]) +@login_required +def cancel_order(order_id): + """Cancel an order if it's in pending status""" + order = Order.query.get_or_404(order_id) + + if order.order_status != "pending": + response = {"success": False, "message": "Only pending orders can be cancelled"} + return jsonify(response), 400 + + try: + order.order_status = "cancelled" + order.payment_status = "failed" # Update payment status + + # Update payment status if exists + payment = Payment.query.filter_by(order_id=order_id).first() + if payment: + payment.status = "failed" + + db.session.commit() + response = {"success": True, "message": "Order cancelled successfully"} + return jsonify(response) + except Exception as e: + db.session.rollback() + response = {"success": False, "message": "Error cancelling order"} + return jsonify(response), 500 + + +@order_bp.route("/order/<int:order_id>") +@login_required +def order_detail(order_id): + """View order details with proper authorization""" + user_id = session.get("user_id") + user = User.query.get(user_id) + order = Order.query.get_or_404(order_id) + + order_user = User.query.get(order.user_id) + payment = Payment.query.filter_by(order_id=order_id).first() + order_items = OrderItem.query.filter_by(order_id=order_id).all() + restaurant = Restaurant.query.get(order.restaurant_id) + + items = [] + total_items = 0 + subtotal = Decimal('0') + for item in order_items: + menu_item = Menu.query.get(item.menu_id) + if menu_item: + item_subtotal = menu_item.price * item.quantity + subtotal += item_subtotal + items.append( + { + "name": menu_item.name, + "quantity": item.quantity, + "price": menu_item.price, + "subtotal": item_subtotal, + } + ) + total_items += item.quantity + + # Update order total amount if it's different from calculated subtotal + if order.total_amount != subtotal: + order.total_amount = subtotal + db.session.commit() + + return render_template( + "orders/order_detail.html", + order=order, + user=order_user, + payment=payment, + items=items, + total_items=total_items, + restaurant=restaurant, + is_admin=(user and user.role == "admin"), + ) + + +@order_bp.route("/api/restaurant/<int:restaurant_id>/menu-items") +@login_required +def get_restaurant_menu_items(restaurant_id): + """Get menu items for a specific restaurant""" + print(f"Fetching menu items for restaurant {restaurant_id}") # Debug log + restaurant = Restaurant.query.get_or_404(restaurant_id) + menu_items = Menu.query.filter_by(restaurant_id=restaurant_id, available=True).all() + print(f"Found {len(menu_items)} menu items") # Debug log + + items_data = [] + for item in menu_items: + items_data.append( + { + "id": item.id, + "name": item.name, + "price": float(item.price), + "description": item.description, + "category": item.category, + } + ) + print(f"Returning {len(items_data)} items") # Debug log + return jsonify({"success": True, "data": items_data}) + + +@order_bp.route("/") +def index(): + return redirect(url_for("order.order_history")) diff --git a/app/routes/payments.py b/app/routes/payments.py index 5eda9ce3fd698e9e96d50aa45bcfd2b7bea6cc77..ed958cb595de1e67f680e98f325580937f326b1f 100644 --- a/app/routes/payments.py +++ b/app/routes/payments.py @@ -1,8 +1,133 @@ -from flask import Blueprint, render_template +from flask import Blueprint, render_template, request +from app.models import Order, Payment, db, Restaurant +from sqlalchemy import func +from datetime import datetime, timedelta +from decimal import Decimal payments_bp = Blueprint("payments", __name__) @payments_bp.route("/payment") def payment(): - return render_template("payments/payment.html") + # Get all restaurants + restaurants = Restaurant.query.all() + restaurant_stats = [] + + # Get selected restaurant from query params + selected_restaurant_id = request.args.get('restaurant_id', type=int) + + # Base query for restaurants + query = Restaurant.query + if selected_restaurant_id: + query = query.filter(Restaurant.id == selected_restaurant_id) + + for restaurant in query.all(): + # Get total paid amount from successful payments + total_paid = ( + db.session.query(func.sum(Order.total_amount)) + .join(Payment, Order.id == Payment.order_id) + .filter( + Order.restaurant_id == restaurant.id, + Order.order_status == "completed", # Only completed orders + Order.payment_status == "paid", # Must be paid + Payment.status == "success" + ) + .scalar() or Decimal('0') + ) + + # Get total unpaid amount from pending orders + total_unpaid = ( + db.session.query(func.sum(Order.total_amount)) + .outerjoin(Payment, Order.id == Payment.order_id) + .filter( + Order.restaurant_id == restaurant.id, + Order.order_status.in_(["pending", "preparing", "ready"]), # Active orders + Order.payment_status == "pending" # Must be pending payment + ) + .scalar() or Decimal('0') + ) + + # Calculate service fee (5% of total_paid) + service_fee = total_paid * Decimal('0.05') + + # Calculate VAT (10% of total_paid + service_fee) + vat_base = total_paid + service_fee + vat = vat_base * Decimal('0.10') + + # Calculate net vendor revenue (total_paid - service_fee) + vendor_get = total_paid - service_fee + + # Calculate growth rate + today = datetime.utcnow().date() + current_month_start = today.replace(day=1) + previous_month_start = (current_month_start - timedelta(days=1)).replace(day=1) + + current_month_revenue = ( + db.session.query(func.sum(Order.total_amount + Order.discount_amount)) + .join(Payment, Order.id == Payment.order_id) + .filter( + Order.restaurant_id == restaurant.id, + Order.order_status != "cancelled", + Payment.status == "success", + func.date(Payment.created_at) >= current_month_start + ) + .scalar() or Decimal('0') + ) + + previous_month_revenue = ( + db.session.query(func.sum(Order.total_amount + Order.discount_amount)) + .join(Payment, Order.id == Payment.order_id) + .filter( + Order.restaurant_id == restaurant.id, + Order.order_status != "cancelled", + Payment.status == "success", + func.date(Payment.created_at) >= previous_month_start, + func.date(Payment.created_at) < current_month_start + ) + .scalar() or Decimal('0') + ) + + growth_rate = 0 + if previous_month_revenue > 0: + growth_rate = round( + ((current_month_revenue - previous_month_revenue) / previous_month_revenue) * 100, + 1 + ) + + restaurant_stats.append({ + "id": restaurant.id, + "name": restaurant.name, + "city": restaurant.city, + "total_amount": float(total_paid + total_unpaid), + "total_paid": float(total_paid), + "total_unpaid": float(total_unpaid), + "service_fee": float(service_fee), + "vat": float(vat), + "vendor_get": float(vendor_get), + "growth_rate": growth_rate + }) + + # Calculate overall statistics + total_amount = sum(stat["total_paid"] + stat["total_unpaid"] for stat in restaurant_stats) + total_paid = sum(stat["total_paid"] for stat in restaurant_stats) + total_unpaid = sum(stat["total_unpaid"] for stat in restaurant_stats) + total_service_fee = sum(stat["service_fee"] for stat in restaurant_stats) + total_vat = sum(stat["vat"] for stat in restaurant_stats) + total_vendor_get = sum(stat["vendor_get"] for stat in restaurant_stats) + + overall_stats = { + "total_amount": f"${total_amount:.2f}", + "total_paid": f"${total_paid:.2f}", + "total_unpaid": f"${total_unpaid:.2f}", + "service_fee": f"${total_service_fee:.2f}", + "vat": f"${total_vat:.2f}", + "vendor_get": f"${total_vendor_get:.2f}", + } + + return render_template( + "payments/payment.html", + stats=overall_stats, + restaurant_stats=restaurant_stats, + restaurants=restaurants, + selected_restaurant_id=selected_restaurant_id + ) diff --git a/app/routes/reservations.py b/app/routes/reservations.py index 60d7e4eaba7b96cac6b4a032cbee6704d7c205c8..b076c264071f5f72655667b07b937492ec795352 100644 --- a/app/routes/reservations.py +++ b/app/routes/reservations.py @@ -1,46 +1,246 @@ -from flask import Blueprint, render_template, request, redirect, url_for -from app.models import db, Reservation -from datetime import datetime - +from flask import ( + Blueprint, + render_template, + request, + redirect, + url_for, + flash, + jsonify, + session, +) +from datetime import datetime, timedelta +from app.models import db, Reservation, Restaurant, User +from sqlalchemy import desc +from app.routes.auth import login_required reservations_bp = Blueprint("reservations", __name__) -reservations = [] -@reservations_bp.route("/reserve", methods=["GET", "POST"]) -def reserve_table(): - if request.method == "POST": - new_reservation = Reservation( - restaurant_id=1, # Assuming a default restaurant_id for now - user_id=None, # Assuming no user authentication for now - table_number=1, # Assuming a default table number for now - reservation_time=datetime.strptime( - f"{request.form['date']} {request.form['time']}", "%Y-%m-%d %H:%M" - ), - status="pending", - created_at=datetime.utcnow(), - ) - db.session.add(new_reservation) - db.session.commit() - reservations.append(new_reservation) - return redirect( - url_for( - "reservations.reservation_confirmation", name=new_reservation["name"] - ) +def validate_reservation( + restaurant_id, reservation_date, reservation_time, guest_count +): + """Validate reservation parameters""" + MAX_GUESTS_PER_TABLE = 10 + MIN_GUESTS = 1 + + # Validate guest count + if guest_count < MIN_GUESTS or guest_count > MAX_GUESTS_PER_TABLE: + return ( + False, + f"Guest count must be between {MIN_GUESTS} and {MAX_GUESTS_PER_TABLE}", ) - return render_template("reservations/reserve_table.html") + # Validate date is not in the past + reservation_datetime = datetime.combine(reservation_date, reservation_time) + if reservation_datetime < datetime.now(): + return False, "Reservation date and time must be in the future" + # Check if restaurant exists + restaurant = Restaurant.query.get(restaurant_id) + if not restaurant: + return False, "Invalid restaurant selected" -@reservations_bp.route("/reserve/confirmation") -def reservation_confirmation(): - name = request.args.get("name") - return render_template("reservations/reservation_confirmation.html", name=name) + return True, None @reservations_bp.route("/reservations") -def reservation_list(): - reservations = Reservation.query.all() +@login_required +def list_reservations(): + """Display list of all reservations with filtering and pagination""" + page = request.args.get("page", 1, type=int) + per_page = 10 + status = request.args.get("status", "all") + user_id = session.get("user_id") + user = User.query.get(user_id) + + # Base query for reservations, ordered by date and time + query = Reservation.query.order_by( + Reservation.reservation_date.desc(), Reservation.reservation_time.desc() + ) + + # Apply status filter if provided and not 'all' + if status and status.lower() != "all": + query = query.filter(Reservation.status == status.lower()) + + # Apply date range filter if both start and end dates are provided + start_date = request.args.get("start_date") + end_date = request.args.get("end_date") + if start_date and end_date: + query = query.filter(Reservation.reservation_date.between(start_date, end_date)) + + # Get paginated results + pagination = query.paginate(page=page, per_page=per_page, error_out=False) + reservations = [] + + # Fetch related data for each reservation + for reservation in pagination.items: + reservation_user = User.query.get(reservation.user_id) + restaurant = Restaurant.query.get(reservation.restaurant_id) + + reservations.append( + { + "reservation": reservation, + "user": reservation_user, + "restaurant": restaurant, + } + ) + + return render_template( + "reservations/reservation_list.html", + reservations=reservations, + pagination=pagination, + status=status, + is_admin=(user and user.role == "admin"), + ) + + +@reservations_bp.route("/reservations/new", methods=["GET", "POST"]) +@login_required +def create_reservation(): + """Create a new reservation with validation""" + if request.method == "POST": + try: + # Get and validate customer + customer_id = request.form.get("user_id") + if not customer_id: + flash("Please select a customer", "error") + return redirect(url_for("reservations.create_reservation")) + + customer = User.query.get(customer_id) + if not customer: + flash("Invalid customer selected", "error") + return redirect(url_for("reservations.create_reservation")) + + # Get and validate restaurant + restaurant_id = request.form.get("restaurant_id") + if not restaurant_id: + flash("Please select a restaurant", "error") + return redirect(url_for("reservations.create_reservation")) + + # Get reservation details + reservation_date = datetime.strptime( + request.form.get("reservation_date"), "%Y-%m-%d" + ).date() + reservation_time = datetime.strptime( + request.form.get("reservation_time"), "%H:%M" + ).time() + guest_count = int(request.form.get("guest_count")) + table_number = request.form.get("table_number") + notes = request.form.get("notes") + + # Validate reservation parameters + is_valid, error_message = validate_reservation( + int(restaurant_id), reservation_date, reservation_time, guest_count + ) + + if not is_valid: + flash(error_message, "error") + return redirect(url_for("reservations.create_reservation")) + + # Create new reservation + new_reservation = Reservation( + restaurant_id=int(restaurant_id), + user_id=int(customer_id), + reservation_date=reservation_date, + reservation_time=reservation_time, + guest_count=guest_count, + table_number=table_number if table_number else None, + notes=notes, + status="pending", + ) + + db.session.add(new_reservation) + db.session.commit() + + flash("Reservation created successfully", "success") + return redirect( + url_for( + "reservations.reservation_detail", reservation_id=new_reservation.id + ) + ) + + except Exception as e: + db.session.rollback() + flash( + f"An error occurred while creating the reservation: {str(e)}", "error" + ) + return redirect(url_for("reservations.create_reservation")) + + # GET request - show reservation form + restaurants = Restaurant.query.all() + customers = User.query.filter_by(role="customer").all() + today = datetime.now().strftime("%Y-%m-%d") + return render_template( - "reservations/reservation_list.html", reservations=reservations + "reservations/create_reservation.html", + restaurants=restaurants, + users=customers, + today=today, ) + + +@reservations_bp.route("/reservation/<int:reservation_id>") +@login_required +def reservation_detail(reservation_id): + """View detailed information about a specific reservation""" + user_id = session.get("user_id") + user = User.query.get(user_id) + reservation = Reservation.query.get_or_404(reservation_id) + + reservation_user = User.query.get(reservation.user_id) + restaurant = Restaurant.query.get(reservation.restaurant_id) + + return render_template( + "reservations/reservation_detail.html", + reservation=reservation, + user=reservation_user, + restaurant=restaurant, + is_admin=(user and user.role == "admin"), + ) + + +@reservations_bp.route("/api/users/search") +@login_required +def search_users(): + """Search users by phone number for reservation""" + phone = request.args.get("phone", "").strip() + if not phone: + return jsonify({"users": []}) + + users = User.query.filter(User.phone.ilike(f"%{phone}%")).limit(10).all() + return jsonify( + { + "users": [ + { + "id": user.id, + "full_name": user.full_name, + "phone": user.phone, + "email": user.email, + } + for user in users + ] + } + ) + + +@reservations_bp.route("/reservation/<int:reservation_id>/status", methods=["POST"]) +@login_required +def update_reservation_status(reservation_id): + """Update the status of a reservation""" + data = request.get_json() + if not data or "status" not in data: + return jsonify({"success": False, "message": "No status provided"}), 400 + + new_status = data["status"] + valid_statuses = ["pending", "preparing", "ready", "completed", "cancelled"] + if new_status not in valid_statuses: + return jsonify({"success": False, "message": "Invalid status"}), 400 + + try: + reservation = Reservation.query.get_or_404(reservation_id) + reservation.status = new_status + db.session.commit() + return jsonify({"success": True, "message": "Status updated successfully"}) + except Exception as e: + db.session.rollback() + return jsonify({"success": False, "message": str(e)}), 500 diff --git a/app/routes/restaurant.py b/app/routes/restaurant.py new file mode 100644 index 0000000000000000000000000000000000000000..eb55e064c850559d45da2abdf1a7e371458d1d09 --- /dev/null +++ b/app/routes/restaurant.py @@ -0,0 +1,209 @@ +from flask import Blueprint, request, jsonify, render_template, flash, redirect, url_for +from app.models import Restaurant, Order, Reservation, Menu, db +from sqlalchemy import func +from datetime import datetime, timedelta + +restaurant_bp = Blueprint("restaurant", __name__) + + +@restaurant_bp.route("/restaurants", methods=["GET"]) +def get_restaurants(): + # Get query parameters for filtering + city = request.args.get("city") + performance = request.args.get("performance") + + # Base query + query = Restaurant.query + + # Apply filters if provided + if city: + query = query.filter(Restaurant.city == city) + + restaurants = query.all() + + # For each restaurant, calculate performance metrics + restaurant_data = [] + for restaurant in restaurants: + today = datetime.utcnow().date() + + # Calculate current month's data + current_month_start = today.replace(day=1) + current_month_orders = Order.query.filter( + Order.restaurant_id == restaurant.id, + func.date(Order.created_at) >= current_month_start, + Order.payment_status == "paid", + ).all() + + # Calculate previous month's data + previous_month_start = (current_month_start - timedelta(days=1)).replace(day=1) + previous_month_orders = Order.query.filter( + Order.restaurant_id == restaurant.id, + func.date(Order.created_at) >= previous_month_start, + func.date(Order.created_at) < current_month_start, + Order.payment_status == "paid", + ).all() + + # Calculate growth rate + current_month_revenue = sum( + order.total_amount for order in current_month_orders + ) + previous_month_revenue = sum( + order.total_amount for order in previous_month_orders + ) + + growth_rate = 0 + if previous_month_revenue > 0: + growth_rate = round( + ( + (current_month_revenue - previous_month_revenue) + / previous_month_revenue + ) + * 100, + 1, + ) + + # Calculate today's orders + today_orders = [ + order for order in current_month_orders if order.created_at.date() == today + ] + + # Calculate tables booked + tables_booked = Reservation.query.filter( + Reservation.restaurant_id == restaurant.id, + Reservation.reservation_date == today, + ).count() + + restaurant_data.append( + { + "id": restaurant.id, + "name": restaurant.name, + "tables_booked": tables_booked, + "occupancy_rate": round( + (tables_booked / 20) * 100, 1 + ), # Assuming 20 tables per restaurant + "total_orders": len(today_orders), + "daily_revenue": sum(order.total_amount for order in today_orders), + "monthly_revenue": current_month_revenue, + "growth_rate": growth_rate, + "customer_count": len( + set(order.user_id for order in current_month_orders) + ), + } + ) + + return render_template( + "restaurants/restaurant_performance.html", restaurants=restaurant_data + ) + + +@restaurant_bp.route("/restaurants/add", methods=["GET", "POST"]) +def add_restaurant(): + if request.method == "POST": + # Create new restaurant + new_restaurant = Restaurant( + name=request.form.get("name"), + city=request.form.get("city"), + address=request.form.get("address"), + phone=request.form.get("phone"), + email=request.form.get("email"), + ) + + try: + db.session.add(new_restaurant) + db.session.commit() + flash("Restaurant added successfully!", "success") + return redirect(url_for("restaurant.get_restaurants")) + except Exception as e: + db.session.rollback() + flash("An error occurred while adding the restaurant.", "error") + return render_template("restaurants/add.html") + + return render_template("restaurants/add.html") + + +@restaurant_bp.route("/restaurants/<int:restaurant_id>", methods=["GET"]) +def get_restaurant_details(restaurant_id): + restaurant = Restaurant.query.get_or_404(restaurant_id) + today = datetime.utcnow().date() + + # Get monthly orders data + current_month_start = today.replace(day=1) + monthly_orders = Order.query.filter( + Order.restaurant_id == restaurant_id, + func.date(Order.created_at) >= current_month_start, + Order.payment_status == "paid", + ).all() + + # Get reservations for today + today_reservations = ( + Reservation.query.filter( + Reservation.restaurant_id == restaurant_id, + Reservation.reservation_date == today, + ) + .order_by(Reservation.reservation_time) + .all() + ) + + # Get menu items + menu_items = Menu.query.filter_by(restaurant_id=restaurant_id).all() + + # Calculate metrics + metrics = { + "total_orders": len(monthly_orders), + "total_revenue": sum(order.total_amount for order in monthly_orders), + "avg_order_value": round( + sum(order.total_amount for order in monthly_orders) / len(monthly_orders), 2 + ) + if monthly_orders + else 0, + "total_customers": len(set(order.user_id for order in monthly_orders)), + "tables_booked": len(today_reservations), + "menu_items": len(menu_items), + } + + return render_template( + "restaurants/details.html", + restaurant=restaurant, + metrics=metrics, + reservations=today_reservations, + menu_items=menu_items, + ) + + +@restaurant_bp.route("/restaurants/<int:restaurant_id>/edit", methods=["GET", "POST"]) +def edit_restaurant(restaurant_id): + restaurant = Restaurant.query.get_or_404(restaurant_id) + + if request.method == "POST": + # Update restaurant details + restaurant.name = request.form.get("name") + restaurant.city = request.form.get("city") + restaurant.address = request.form.get("address") + restaurant.phone = request.form.get("phone") + restaurant.email = request.form.get("email") + + try: + db.session.commit() + flash("Restaurant updated successfully!", "success") + return redirect(url_for("restaurant.get_restaurants")) + except Exception as e: + db.session.rollback() + flash("An error occurred while updating the restaurant.", "error") + return render_template("restaurants/edit.html", restaurant=restaurant) + + return render_template("restaurants/edit.html", restaurant=restaurant) + + +@restaurant_bp.route("/restaurants/<int:restaurant_id>/delete", methods=["POST"]) +def delete_restaurant(restaurant_id): + restaurant = Restaurant.query.get_or_404(restaurant_id) + + try: + db.session.delete(restaurant) + db.session.commit() + flash("Restaurant deleted successfully!", "success") + except Exception as e: + db.session.rollback() + flash("An error occurred while deleting the restaurant.", "error") + + return redirect(url_for("restaurant.get_restaurants")) diff --git a/app/routes/staff.py b/app/routes/staff.py new file mode 100644 index 0000000000000000000000000000000000000000..9e990595c1117d71b5e22fd7fb7e0ebc5e6d88bf --- /dev/null +++ b/app/routes/staff.py @@ -0,0 +1,134 @@ +from flask import Blueprint, request, render_template +from app.models import db, User, Order, Attendance, Performance +from sqlalchemy import func +from datetime import datetime, timedelta + +staff_bp = Blueprint("staff", __name__) + + +@staff_bp.route("/staff") +def staff_list(): + # Query staff members (excluding customers) + staff = User.query.filter(User.role.in_(["manager", "staff"])).all() + + current_month = datetime.now().strftime("%Y-%m") + staff_data = [] + + for member in staff: + # Get number of orders handled + orders_handled = Order.query.filter_by(user_id=member.id).count() + + # Get latest performance record + performance = Performance.query.filter_by( + user_id=member.id, review_period=current_month + ).first() + + # Get attendance for current month + month_start = datetime.now().replace(day=1) + attendance_records = Attendance.query.filter( + Attendance.user_id == member.id, Attendance.date >= month_start + ).all() + + # Calculate attendance rate + total_days = len(attendance_records) + if total_days > 0: + present_days = sum( + 1 for a in attendance_records if a.status in ["present", "late"] + ) + attendance_rate = (present_days / total_days) * 100 + punctuality_rate = ( + sum(1 for a in attendance_records if a.status == "present") + / total_days + * 100 + ) + else: + attendance_rate = 0 + punctuality_rate = 0 + + staff_data.append( + { + "id": member.id, + "name": member.full_name, + "role": member.role, + "orders_handled": orders_handled, + "performance_rating": performance.rating if performance else 0, + "attendance": round(attendance_rate, 1), + "punctuality": round(punctuality_rate, 1), + "notes": performance.notes + if performance + else "No performance data available", + } + ) + + return render_template("staffs/staff_performance.html", staff=staff_data) + + +@staff_bp.route("/attendance", methods=["GET", "POST"]) +def attendance_check(): + if request.method == "POST": + user_id = request.form.get("user_id") + action = request.form.get("action") # clock-in or clock-out + + current_time = datetime.now().time() + current_date = datetime.now().date() + + # Check if attendance record exists for today + attendance = Attendance.query.filter_by( + user_id=user_id, date=current_date + ).first() + + if action == "clock-in": + if attendance: + return {"status": "error", "message": "Already clocked in today"}, 400 + + # Determine if late (after 9:00 AM) + status = "present" + if current_time > datetime.strptime("09:00", "%H:%M").time(): + status = "late" + + new_attendance = Attendance( + user_id=user_id, date=current_date, clock_in=current_time, status=status + ) + db.session.add(new_attendance) + + elif action == "clock-out": + if not attendance: + return {"status": "error", "message": "No clock-in record found"}, 400 + + attendance.clock_out = current_time + + db.session.commit() + return { + "status": "success", + "message": f"Successfully {action.replace('-', 'ed ')}", + }, 200 + + # GET request - show attendance page + staff = User.query.filter(User.role.in_(["manager", "staff"])).all() + current_date = datetime.now().date() + today_date = current_date.strftime("%B %d, %Y") + + attendance_data = [] + for member in staff: + attendance = Attendance.query.filter_by( + user_id=member.id, date=current_date + ).first() + + attendance_data.append( + { + "id": member.id, + "name": member.full_name, + "role": member.role, + "status": attendance.status if attendance else "not-checked-in", + "clock_in": attendance.clock_in.strftime("%H:%M") + if attendance and attendance.clock_in + else None, + "clock_out": attendance.clock_out.strftime("%H:%M") + if attendance and attendance.clock_out + else None, + } + ) + + return render_template( + "staffs/attendance.html", staff=attendance_data, today_date=today_date + ) diff --git a/app/routes/users.py b/app/routes/users.py index 78f75998719c78662a10f20e63d3b3f9b1a9806a..676cd3f7b53f588938ec48af844f108d880b7690 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -1,5 +1,5 @@ -from flask import Blueprint, render_template, session, request, flash, redirect, url_for -from app.models import User, db +from flask import Blueprint, render_template, session, request, flash, redirect, url_for, jsonify +from app.models import User, db, Restaurant from app.routes.auth import login_required from werkzeug.security import generate_password_hash @@ -19,22 +19,66 @@ def user_profile(): @users_bp.route("/user/manage") @login_required def manage_user(): - users = User.query.all() + # Only show customers, excluding admin and staff roles + users = User.query.filter_by(role="customer").all() return render_template("users/manage_users.html", users=users) @users_bp.route("/user/edit/<int:user_id>", methods=["GET", "POST"]) @login_required def edit_user(user_id): - user = User.query.get(user_id) + user = User.query.get_or_404(user_id) + staff_roles = ['cook', 'dishwasher', 'server', 'host', 'busser'] + if request.method == "POST": user.full_name = request.form["full_name"] user.email = request.form["email"] user.phone = request.form["phone"] - db.session.commit() - flash("User updated successfully!", category="success") - return redirect(url_for("users.manage_user")) - return render_template("users/edit_user.html", user=user) + + # Handle restaurant and role changes for staff/admin + if user.role != 'customer': + new_restaurant_id = request.form.get("restaurant_id") + new_role = request.form.get("role", user.role) + + # Validate restaurant change + if new_restaurant_id != str(user.restaurant_id): + # For admin role, check if target restaurant already has an admin + if user.role == 'admin': + existing_admin = User.query.filter_by( + restaurant_id=new_restaurant_id, + role='admin' + ).first() + if existing_admin: + flash(f"Target restaurant already has an admin: {existing_admin.full_name}", "error") + return redirect(url_for("users.edit_user", user_id=user_id)) + + # For staff roles, check if target restaurant has reached staff limit + if user.role in staff_roles: + staff_count = User.query.filter( + User.restaurant_id == new_restaurant_id, + User.role.in_(staff_roles) + ).count() + if staff_count >= 25: + flash("Target restaurant has reached the maximum number of staff members (25)", "error") + return redirect(url_for("users.edit_user", user_id=user_id)) + + user.restaurant_id = new_restaurant_id + user.role = new_role + + try: + db.session.commit() + flash("User updated successfully!", category="success") + if user.role == 'customer': + return redirect(url_for("users.manage_user")) + else: + return redirect(url_for("staff.staff_list")) + except Exception as e: + db.session.rollback() + flash(f"Error updating user: {str(e)}", "error") + + # Get all restaurants for the selection dropdown + restaurants = Restaurant.query.all() + return render_template("users/edit_user.html", user=user, restaurants=restaurants) @users_bp.route("/user/delete/<int:user_id>", methods=["GET", "POST"]) @@ -53,25 +97,103 @@ def delete_user(user_id): @users_bp.route("/user/add", methods=["GET", "POST"]) @login_required def add_user(): + # Get role from query parameter, default to customer if not specified + role = request.args.get('role', 'customer') + + # Only allow valid roles + staff_roles = ['cook', 'dishwasher', 'server', 'host', 'busser'] + if role == 'staff': + role = 'cook' # Default staff role, will be overridden by form + elif role not in ['customer', 'staff', 'admin'] + staff_roles: + role = 'customer' + if request.method == "POST": full_name = request.form["full_name"] email = request.form["email"] phone = request.form["phone"] password = request.form["password"] repeat_password = request.form["repeat_password"] - role = request.form["role"] + # Use the role from the form if provided, otherwise use the one from query parameter + submitted_role = request.form.get("role", role) + restaurant_id = request.form.get("restaurant_id") + if password != repeat_password: flash("Passwords do not match", category="error") - return redirect(url_for("users.add_user")) + return redirect(url_for("users.add_user", role=role)) + + # Validate restaurant selection for staff and admin + if submitted_role != 'customer' and not restaurant_id: + flash("Please select a restaurant", category="error") + return redirect(url_for("users.add_user", role=role)) + new_user = User( full_name=full_name, email=email, phone=phone, password_hash=generate_password_hash(password), - role=role, + role=submitted_role, + restaurant_id=restaurant_id if submitted_role != 'customer' else None ) - db.session.add(new_user) - db.session.commit() - flash("User added successfully!", category="success") - return redirect(url_for("users.manage_user")) - return render_template("users/add_user.html") + + try: + # Check if restaurant already has an admin (for admin role) + if submitted_role == 'admin' and restaurant_id: + existing_admin = User.query.filter_by( + restaurant_id=restaurant_id, + role='admin' + ).first() + if existing_admin: + flash(f"Restaurant already has an admin: {existing_admin.full_name}", category="error") + return redirect(url_for("users.add_user", role=role)) + + # Check if restaurant has reached staff limit (25 staff members) + if submitted_role in staff_roles and restaurant_id: + staff_count = User.query.filter( + User.restaurant_id == restaurant_id, + User.role.in_(staff_roles) + ).count() + if staff_count >= 25: + flash("This restaurant has reached the maximum number of staff members (25)", category="error") + return redirect(url_for("users.add_user", role=role)) + + db.session.add(new_user) + db.session.commit() + flash("User added successfully!", category="success") + + # Redirect based on role + if submitted_role == 'customer': + return redirect(url_for("users.manage_user")) + else: + return redirect(url_for("staff.staff_list")) + except Exception as e: + db.session.rollback() + flash(f"Error adding user: {str(e)}", category="error") + return redirect(url_for("users.add_user", role=role)) + + # Get all restaurants for the selection dropdown + restaurants = Restaurant.query.all() + return render_template("users/add_user.html", selected_role=role, restaurants=restaurants) + + +@users_bp.route("/api/users/search") +@login_required +def search_users(): + """Search users by phone number""" + phone_query = request.args.get('phone', '').strip() + if not phone_query: + return jsonify({"users": []}) + + # Search for users with phone numbers containing the query + users = User.query.filter( + User.phone.ilike(f"%{phone_query}%"), + User.role == "customer" # Only search for customers + ).limit(10).all() + + users_data = [{ + 'id': user.id, + 'full_name': user.full_name, + 'phone': user.phone, + 'email': user.email + } for user in users] + + return jsonify({"users": users_data}) diff --git a/app/schema.sql b/app/schema.sql index fc126dcea5cb1c9712bb488b2af5338acc2bc52d..381cf1b5a592199c95f218a4ca52ee4872f927e1 100644 --- a/app/schema.sql +++ b/app/schema.sql @@ -40,11 +40,17 @@ CREATE TABLE orders ( user_id INT NULL, table_number INT, total_amount DECIMAL(10,2), + discount_id INT, + discount_amount DECIMAL(10,2), + service_fee DECIMAL(10,2), + vat DECIMAL(10,2), + grand_total DECIMAL(10,2), order_status ENUM('pending', 'preparing', 'ready', 'completed', 'cancelled') DEFAULT 'pending', payment_status ENUM('pending', 'paid', 'failed') DEFAULT 'pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (restaurant_id) REFERENCES restaurants(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL, + FOREIGN KEY (discount_id) REFERENCES discounts(id) ON DELETE SET NULL ); CREATE TABLE order_items ( diff --git a/app/static/css/toasts.css b/app/static/css/toasts.css new file mode 100644 index 0000000000000000000000000000000000000000..f5b3b0fd2d5d41f469f60845d788aae9cc9cf622 --- /dev/null +++ b/app/static/css/toasts.css @@ -0,0 +1,126 @@ +.toast-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 1060; +} + +.toast { + background: white; + border: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + margin-bottom: 1rem; + min-width: 300px; + opacity: 1; + border-radius: 8px; + overflow: hidden; +} + +.toast-header { + background: none; + border: none; + padding: 0.75rem 1rem; + align-items: center; +} + +.toast-body { + padding: 0.75rem 1rem; + color: #666; +} + +.toast-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 50%; + margin-right: 0.5rem; +} + +/* Toast variants */ +.toast.success { + border-left: 4px solid #198754; +} + +.toast.success .toast-icon { + background-color: #d1e7dd; + color: #198754; +} + +.toast.error { + border-left: 4px solid #dc3545; +} + +.toast.error .toast-icon { + background-color: #f8d7da; + color: #dc3545; +} + +.toast.warning { + border-left: 4px solid #ffc107; +} + +.toast.warning .toast-icon { + background-color: #fff3cd; + color: #856404; +} + +.toast.info { + border-left: 4px solid #0dcaf0; +} + +.toast.info .toast-icon { + background-color: #cff4fc; + color: #055160; +} + +/* Toast animations */ +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } +} + +.toast.showing { + animation: slideIn 0.3s ease forwards; +} + +.toast.hide { + animation: slideOut 0.3s ease forwards; +} + +/* Responsive adjustments */ +@media (max-width: 576px) { + .toast-container { + right: 10px; + left: 10px; + } + + .toast { + min-width: auto; + width: 100%; + } +} + +.toast .btn-close { + font-size: 0.875rem; + padding: 0.5rem 0.5rem; + margin: -0.5rem -0.5rem -0.5rem auto; +} \ No newline at end of file diff --git a/app/static/js/login.js b/app/static/js/login.js index cdc17a56f29d9f20ff51c634f9d307f997528f7f..f8a7902ddca382139b2ad592bb88a19b66557016 100644 --- a/app/static/js/login.js +++ b/app/static/js/login.js @@ -1,4 +1,16 @@ -// Initialization for ES Users -import { Input, initMDB } from "mdb-ui-kit"; - -initMDB({ Input }); \ No newline at end of file +// Login form validation +document.addEventListener('DOMContentLoaded', function() { + const loginForm = document.getElementById('loginForm'); + + if (loginForm) { + loginForm.addEventListener('submit', function(e) { + const username = document.getElementById('username').value; + const password = document.getElementById('password').value; + + if (!username || !password) { + e.preventDefault(); + alert('Please enter both username and password'); + } + }); + } +}); \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index b20a3ebf88304e1713806fa94a755658763928ec..a00fb2b557f6eb0d7c876332895062ff86359151 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,252 +1,380 @@ <!DOCTYPE html> <html lang="en"> <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>HRMS</title> - - <!-- Custom CSS --> - <link - rel="stylesheet" - href="{{ url_for('static', filename='css/styles.css') }}" - /> - - <!-- Bootstrap CSS --> - <link - href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" - rel="stylesheet" - integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" - crossorigin="anonymous" - /> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> + <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet" /> + <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}" /> + <link rel="stylesheet" href="{{ url_for('static', filename='css/toasts.css') }}" /> + <title>{% block title %}Dashboard{% endblock %}</title> + <style> + :root { + --primary-color: #00a389; + --primary-light: #e8f7f5; + --text-color: #333; + --text-muted: #666; + --sidebar-width: 250px; + } + + body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + } + + /* Sidebar styles */ + .sidebar { + position: fixed; + top: 0; + left: 0; + height: 100%; + width: var(--sidebar-width); + background-color: #ffffff; + color: var(--text-color); + padding-top: 20px; + transition: all 0.3s ease-in-out; + z-index: 1000; + box-shadow: 2px 0 5px rgba(0,0,0,0.1); + } + + .sidebar .nav-link { + color: var(--text-muted); + padding: 12px 20px; + display: flex; + align-items: center; + gap: 10px; + font-size: 0.95rem; + transition: all 0.2s ease; + border-radius: 0 8px 8px 0; + margin: 2px 0; + } + + .sidebar .nav-link i { + font-size: 1.2rem; + width: 24px; + transition: transform 0.2s ease; + } + + .sidebar .nav-link:hover { + background-color: var(--primary-light); + color: var(--primary-color); + } + + .sidebar .nav-link.active { + color: var(--primary-color); + background-color: var(--primary-light); + font-weight: 500; + } + + .sidebar .nav-link[data-bs-toggle="collapse"] i.bi-chevron-down { + transition: transform 0.2s ease; + } + + .sidebar .nav-link[data-bs-toggle="collapse"][aria-expanded="true"] i.bi-chevron-down { + transform: rotate(-180deg); + } + + .sidebar .dropdown-menu { + background-color: #f8f9fa; + border: none; + padding: 0; + margin: 0; + } + + .sidebar .dropdown-item { + padding: 8px 35px; + color: var(--text-muted); + transition: all 0.2s ease; + font-size: 0.9rem; + } + + .sidebar .dropdown-item:hover, + .sidebar .dropdown-item.active { + background-color: var(--primary-light); + color: var(--primary-color); + } + + .sidebar .dropdown-item i { + width: 20px; + } + + .content { + margin-left: var(--sidebar-width); + padding: 20px; + padding-top: 80px; + background-color: #f8f9fa; + min-height: 100vh; + } + + /* Navbar styles */ + .main-navbar { + position: fixed; + top: 0; + right: 0; + left: var(--sidebar-width); + height: 60px; + background-color: #ffffff; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + z-index: 999; + padding: 0 20px; + } + + .user-profile { + display: flex; + align-items: center; + gap: 10px; + padding: 5px 15px; + border-radius: 30px; + background-color: #f8f9fa; + cursor: pointer; + transition: all 0.2s ease; + } + + .user-profile:hover { + background-color: var(--primary-light); + } + + .user-profile .user-info { + text-align: right; + } + + .user-profile .user-name { + font-size: 0.9rem; + font-weight: 500; + margin: 0; + color: var(--text-color); + } + + .user-profile .user-role { + font-size: 0.8rem; + color: var(--text-muted); + margin: 0; + } + + /* Responsive adjustments */ + @media (max-width: 768px) { + .sidebar { + transform: translateX(-100%); + } + + .sidebar.active { + transform: translateX(0); + } + + .content, .main-navbar { + margin-left: 0; + left: 0; + } + + .search-bar { + display: none; + } + } + </style> + {% block styles %}{% endblock %} </head> <body> - <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> - <div class="container-fluid"> - <!-- Sidebar Toggle Button --> - <button id="sidebarToggle" type="button" class="btn btn-dark me-2"> - ☰ - </button> - - <!-- Centered Navbar Brand --> - <a class="navbar-brand mx-auto" href="{{ url_for('home.homepage') }}"> - <span class="fs-4">HORIZON RESTAURANT MANAGEMENT SYSTEM</span> - </a> - - <!-- User Dropdown (Top-Right) --> - <div class="dropdown ms-auto"> - <a - class="btn btn-dark dropdown-toggle" - href="#" - role="button" - id="userDropdown" - data-bs-toggle="dropdown" - aria-expanded="false" - > - <svg - xmlns="http://www.w3.org/2000/svg" - width="16" - height="16" - fill="currentColor" - class="bi bi-person-fill" - viewBox="0 0 16 16" - > - <path - d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6" - /> - </svg> - </a> - <ul - class="dropdown-menu dropdown-menu-end dropdown-menu-dark" - aria-labelledby="userDropdown" - > - <li> - <a class="dropdown-item" href="#"> {{ session['full_name'] }} </a> - </li> - <li><hr class="dropdown-divider" /></li> - <li> - <a - class="dropdown-item" - href="{{ url_for('users.user_profile') }}" - >Profile</a - > - </li> - <li><a class="dropdown-item" href="#">Settings</a></li> - <li><hr class="dropdown-divider" /></li> - <li> - <a class="dropdown-item" href="{{ url_for('auth.signout') }}" - >Sign Out</a - > - </li> - </ul> - </div> - </div> - </nav> + <!-- Toast Container --> + <div class="toast-container"></div> - <!-- Sidebar (Collapsible) --> - <div class="sidebar bg-dark" id="sidebar"> - <div - class="pt-4 pb-1 px-2 d-flex justify-content-between align-items-center" - > - <a href="#" class="text-white text-decoration-none"> - <span class="fs-4">HRMS</span> - </a> - <!-- Sidebar Collapse Button (next to HRMS) --> - <button - class="btn btn-sm sidebar-toggle-btn text-white" - id="collapseBtn" - > - ☰ - </button> + <!-- Sidebar --> + <div class="sidebar"> + <div class="d-flex align-items-center px-3 mb-4"> + <i class="bi bi-shop text-success fs-4 me-2"></i> + <span class="fs-4 fw-bold text-success">HRMS</span> </div> - <hr class="text-white" /> - <ul class="nav nav-pills flex-column mb-auto"> + <ul class="nav flex-column"> <li class="nav-item"> - <a href="{{ url_for('home.homepage') }}" class="nav-link text-white"> - <span class="d-none d-sm-inline">Dashboard</span> + <a class="nav-link {% if request.endpoint == 'home.homepage' %}active{% endif %}" + href="{{ url_for('home.homepage') }}"> + <i class="bi bi-grid-1x2-fill"></i> + Dashboard </a> </li> - - <!-- Dropdown Menu Item (Business) --> <li class="nav-item"> - <a - class="nav-link text-white" - data-bs-toggle="collapse" - href="#businessCollapse" - role="button" - aria-expanded="false" - aria-controls="businessCollapse" - > - <span class="d-none d-sm-inline">Business</span> - <span class="dropdown-arrow" - ><svg - xmlns="http://www.w3.org/2000/svg" - width="16" - height="16" - fill="currentColor" - class="bi bi-caret-down-fill" - viewBox="0 0 16 16" - > - <path - d="M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z" - /> - </svg> - ></span - > + <a class="nav-link {% if request.endpoint and request.endpoint.startswith('order.') %}active{% endif %}" + href="{{ url_for('order.order_history') }}"> + <i class="bi bi-cart-fill"></i> + Orders </a> </li> - <div class="collapse" id="businessCollapse"> - <ul class="nav flex-column ms-4"> - <li class="nav-item"> - <a class="nav-link text-white" href="#">General Info</a> - </li> - <li class="nav-item"> - <a class="nav-link text-white" href="#">Services & hours</a> - </li> - <li class="nav-item"> - <a - class="nav-link text-white" - href="{{ url_for('reservations.reservation_list') }}" - >Table Reservation</a - > - </li> - <li class="nav-item"> - <a class="nav-link text-white" href="#">Menus</a> - </li> - </ul> - </div> - - <!-- Dropdown Menu Item (Reports) --> <li class="nav-item"> - <a - class="nav-link text-white" - data-bs-toggle="collapse" - href="#reportsCollapse" - role="button" - aria-expanded="false" - aria-controls="reportsCollapse" - > - <span class="d-none d-sm-inline">Reports</span> - <span class="dropdown-arrow" - ><svg - xmlns="http://www.w3.org/2000/svg" - width="16" - height="16" - fill="currentColor" - class="bi bi-caret-down-fill" - viewBox="0 0 16 16" - > - <path - d="M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z" - /></svg - ></span> + <a class="nav-link {% if request.endpoint and request.endpoint.startswith(('reservations.', 'menu.')) %}active{% endif %}" + data-bs-toggle="collapse" + href="#businessDropdown" + aria-expanded="{% if request.endpoint and request.endpoint.startswith(('reservations.', 'menu.')) %}true{% else %}false{% endif %}"> + <i class="bi bi-building"></i> + Business + <i class="bi bi-chevron-down ms-auto"></i> </a> + <div class="collapse {% if request.endpoint and request.endpoint.startswith(('reservations.', 'menu.')) %}show{% endif %}" id="businessDropdown"> + <ul class="nav flex-column"> + <li> + <a class="dropdown-item {% if request.endpoint == 'reservations.list_reservations' %}active{% endif %}" + href="{{ url_for('reservations.list_reservations') }}"> + <i class="bi bi-calendar-check"></i>Table Reservation + </a> + </li> + <li> + <a class="dropdown-item {% if request.endpoint and request.endpoint.startswith('menu.') %}active{% endif %}" + href="{{ url_for('menu.menu') }}"> + <i class="bi bi-journal-text"></i>Menu + </a> + </li> + </ul> + </div> </li> - <div class="collapse" id="reportsCollapse"> - <ul class="nav flex-column ms-4"> - <li class="nav-item"> - <a - class="nav-link text-white" - href="{{ url_for('users.manage_user') }}" - >Users</a - > - </li> - <li class="nav-item"> - <a - class="nav-link text-white" - href="{{ url_for('reports.staff_performance') }}" - >Staff</a - > - </li> - <li class="nav-item"> - <a - class="nav-link text-white" - href="{{ url_for('reports.restaurant_performance') }}" - >Restaurant</a - > - </li> - </ul> - </div> - <li class="nav-item"> - <a - href="{{ url_for('inventory.manage_inventory') }}" - class="nav-link text-white" - > - <span class="d-none d-sm-inline">Inventory</span> + <a class="nav-link {% if request.endpoint and request.endpoint.startswith(('users.', 'staff.', 'restaurant.')) %}active{% endif %}" + data-bs-toggle="collapse" + href="#reportsDropdown" + aria-expanded="{% if request.endpoint and request.endpoint.startswith(('users.', 'staff.', 'restaurant.')) %}true{% else %}false{% endif %}"> + <i class="bi bi-file-earmark-text"></i> + Reports + <i class="bi bi-chevron-down ms-auto"></i> </a> + <div class="collapse {% if request.endpoint and request.endpoint.startswith(('users.', 'staff.', 'restaurant.')) %}show{% endif %}" id="reportsDropdown"> + <ul class="nav flex-column"> + <li> + <a class="dropdown-item {% if request.endpoint == 'users.manage_user' %}active{% endif %}" + href="{{ url_for('users.manage_user') }}"> + <i class="bi bi-people"></i>Users + </a> + </li> + <li> + <a class="dropdown-item {% if request.endpoint == 'staff.staff_list' %}active{% endif %}" + href="{{ url_for('staff.staff_list') }}"> + <i class="bi bi-person-badge"></i>Staff + </a> + </li> + <li> + <a class="dropdown-item {% if request.endpoint == 'restaurant.get_restaurants' %}active{% endif %}" + href="{{ url_for('restaurant.get_restaurants') }}"> + <i class="bi bi-shop-window"></i>Restaurant + </a> + </li> + </ul> + </div> </li> - <li class="nav-item"> - <a - href="{{ url_for('discounts.offers') }}" - class="nav-link text-white" - > - <span class="d-none d-sm-inline">Offers</span> + <a class="nav-link {% if request.endpoint and request.endpoint.startswith('inventory.') %}active{% endif %}" + href="{{ url_for('inventory.manage_inventory') }}"> + <i class="bi bi-box-seam"></i> + Inventory + </a> + </li> + <li class="nav-item"> + <a class="nav-link {% if request.endpoint and request.endpoint.startswith('discounts.') %}active{% endif %}" + href="{{ url_for('discounts.offers') }}"> + <i class="bi bi-tag"></i> + Offers </a> </li> - <li class="nav-item"> - <a - href="{{ url_for('payments.payment') }}" - class="nav-link text-white" - > - <span class="d-none d-sm-inline">Payment</span> + <a class="nav-link {% if request.endpoint and request.endpoint.startswith('payments.') %}active{% endif %}" + href="{{ url_for('payments.payment') }}"> + <i class="bi bi-credit-card"></i> + Payments </a> </li> </ul> </div> - <!-- Main Content Area --> - <div class="content">{% block content %} {% endblock %}</div> + <!-- Navbar --> + <nav class="main-navbar"> + <div class="container-fluid px-0"> + <div class="d-flex justify-content-between align-items-center h-100"> + <div class="d-flex align-items-center"> + <button id="sidebarToggle" type="button" class="btn btn-light d-md-none"> + <i class="bi bi-list"></i> + </button> + </div> + + <!-- User Profile Dropdown --> + <div class="dropdown"> + <div class="user-profile" data-bs-toggle="dropdown"> + <div class="user-info"> + <p class="user-name">{{ session['full_name'] }}</p> + <p class="user-role">Administrator</p> + </div> + <i class="bi bi-chevron-down ms-2"></i> + </div> + <ul class="dropdown-menu dropdown-menu-end"> + <li><a class="dropdown-item" href="{{ url_for('users.user_profile') }}"><i class="bi bi-person me-2"></i>Profile</a></li> + <li><a class="dropdown-item" href="#"><i class="bi bi-gear me-2"></i>Settings</a></li> + <li><hr class="dropdown-divider"></li> + <li><a class="dropdown-item" href="{{ url_for('auth.signout') }}"><i class="bi bi-box-arrow-right me-2"></i>Sign Out</a></li> + </ul> + </div> + </div> + </div> + </nav> - <!-- Custom JS --> - <script src="{{ url_for('static', filename='js/scripts.js') }}"></script> + <!-- Main Content Area --> + <div class="content"> + {% block content %}{% endblock %} + </div> <!-- Bootstrap JS --> - <script - src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" - integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" - crossorigin="anonymous" - ></script> + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> + + <!-- JavaScript for Sidebar Toggle --> + <script> + document.getElementById('sidebarToggle').addEventListener('click', function() { + const sidebar = document.querySelector('.sidebar'); + sidebar.classList.toggle('active'); + }); + + // Optional: Close sidebar when clicking outside on mobile + document.addEventListener('click', function(event) { + const sidebar = document.querySelector('.sidebar'); + const toggleBtn = document.getElementById('sidebarToggle'); + if (window.innerWidth <= 768 && !sidebar.contains(event.target) && event.target !== toggleBtn) { + sidebar.classList.remove('active'); + } + }); + </script> + + <!-- Toast Notification Script --> + <script> + function showToast(message, type = 'info') { + const icons = { + success: '<i class="bi bi-check-circle-fill"></i>', + error: '<i class="bi bi-x-circle-fill"></i>', + warning: '<i class="bi bi-exclamation-triangle-fill"></i>', + info: '<i class="bi bi-info-circle-fill"></i>' + }; + + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.innerHTML = ` + <div class="toast-header"> + <span class="toast-icon ${type}">${icons[type]}</span> + <strong class="me-auto">${type.charAt(0).toUpperCase() + type.slice(1)}</strong> + <button type="button" class="btn-close" data-bs-dismiss="toast"></button> + </div> + <div class="toast-body">${message}</div> + `; + + document.querySelector('.toast-container').appendChild(toast); + const bsToast = new bootstrap.Toast(toast, { delay: 5000 }); + bsToast.show(); + + toast.addEventListener('hidden.bs.toast', () => { + toast.remove(); + }); + } + + // Show flash messages as toasts + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + showToast('{{ message }}', '{{ category }}'); + {% endfor %} + {% endif %} + {% endwith %} + </script> + + <!-- Additional Scripts --> + {% block scripts %}{% endblock %} </body> -</html> +</html> \ No newline at end of file diff --git a/app/templates/discounts/offers.html b/app/templates/discounts/offers.html index 6d2c3f975673c6ab370ba454954d2bf042f16a57..6fc2144be243878bee01c2e1e25ec0d5e2efef1e 100644 --- a/app/templates/discounts/offers.html +++ b/app/templates/discounts/offers.html @@ -1,64 +1,458 @@ -{% extends 'base.html' %} +{% extends 'base.html' %} {% block title %}HRMS - Offers & Discounts{% endblock +%} {% block styles %} +<style> + .offer-status { + padding: 0.5em 0.8em; + border-radius: 20px; + font-size: 0.85em; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + } -{% block content %} -<div class="container"> - <h3 class="text-center">Offers</h3> + .offer-status.active { + background-color: #d1e7dd; + color: #0f5132; + } + .offer-status.expired { + background-color: #f8d7da; + color: #842029; + } + .offer-status.upcoming { + background-color: #cff4fc; + color: #055160; + } - <table class="table table-bordered"> - <thead class="table-dark"> - <tr> - <th>Offer ID</th> + .discount-badge { + background-color: #e9ecef; + color: #495057; + padding: 0.4em 0.8em; + border-radius: 20px; + font-size: 0.85em; + font-weight: 500; + } + + .search-filter-section { + background: white; + padding: 1.5rem; + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + margin-bottom: 1.5rem; + } + + .search-input { + max-width: 300px; + border-radius: 20px; + padding: 0.75rem 1rem 0.75rem 2.5rem; + border: 1px solid #dee2e6; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%236c757d' class='bi bi-search' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: 12px center; + } + + .search-input:focus { + box-shadow: 0 0 0 0.2rem rgba(0, 163, 137, 0.25); + border-color: #00a389; + } + + .offers-table th { + background-color: #f8f9fa; + border-bottom: 2px solid #dee2e6; + white-space: nowrap; + text-transform: uppercase; + font-size: 0.85em; + letter-spacing: 0.5px; + font-weight: 600; + } + + .offers-table td { + vertical-align: middle; + } + + .card { + border-radius: 12px; + border: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + } + + .action-buttons .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 8px; + margin-left: 0.25rem; + } + + .form-label { + font-weight: 500; + color: #495057; + margin-bottom: 0.5rem; + } +</style> +{% endblock %} {% block content %} + +<div class="container-fluid"> + {% with messages = get_flashed_messages(with_categories=true) %} {% if + messages %} {% for category, message in messages %} + <div + class="alert alert-{{ category }} alert-dismissible fade show" + role="alert" + > + {{ message }} + <button + type="button" + class="btn-close" + data-bs-dismiss="alert" + aria-label="Close" + ></button> + </div> + {% endfor %} {% endif %} {% endwith %} + + <div class="container-fluid"> + <!-- Search and Filter Section --> + <div class="search-filter-section"> + <div class="d-flex justify-content-between align-items-center"> + <input + type="text" + class="form-control search-input" + placeholder="Search offers..." + /> + <div> + <button + type="button" + class="btn btn-primary" + data-bs-toggle="modal" + data-bs-target="#addOfferModal" + > + <i class="bi bi-plus-lg"></i> Add New Offer + </button> + <button + class="btn btn-outline-secondary ms-2" + data-bs-toggle="modal" + data-bs-target="#filterModal" + > + <i class="bi bi-funnel"></i> Filter + </button> + </div> + </div> + </div> + + <!-- Offers Table Card --> + <div class="card"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover mb-0 offers-table"> + <thead> + <tr> <th>Offer Name</th> <th>Discount</th> + <th>Valid From</th> <th>Valid Until</th> - <th>Edit</th> - <th>Delete</th> - </tr> - </thead> - <tbody> - <tr> - <td>1</td> - <td>Summer Special</td> - <td>20% Off</td> - <td>2024-12-31</td> + <th>Status</th> + <th class="text-end">Actions</th> + </tr> + </thead> + <tbody> + {% for discount in discounts %} + <tr> + <td class="fw-medium">{{ discount.description }}</td> <td> - <a href="#" class="btn btn-primary btn-sm">Edit</a> + <span class="discount-badge" + >{{ discount.discount_percent }}% Off</span + > </td> + <td>{{ discount.valid_from.strftime('%Y-%m-%d') }}</td> + <td>{{ discount.valid_to.strftime('%Y-%m-%d') }}</td> <td> - <a href="#" class="btn btn-danger btn-sm">Delete</a> + <span class="offer-status {{ discount.dynamic_status }}" + >{{ discount.dynamic_status }}</span + > </td> - </tr> - <tr> - <td>2</td> - <td>Weekend Delight</td> - <td>Buy 1 Get 1 Free</td> - <td>2024-10-31</td> - <td> - <a href="#" class="btn btn-primary btn-sm">Edit</a> + <td class="text-end action-buttons"> + <button + type="button" + class="btn btn-outline-primary" + data-bs-toggle="modal" + data-bs-target="#editModal{{ discount.id }}" + > + <i class="bi bi-pencil"></i> + </button> + <button + type="button" + class="btn btn-outline-danger" + onclick="confirmDelete({{ discount.id }})" + > + <i class="bi bi-trash"></i> + </button> </td> - <td> - <a href="#" class="btn btn-danger btn-sm">Delete</a> - </td> - </tr> - <tr> - <td>3</td> - <td>Festive Feast</td> - <td>30% Off</td> - <td>2024-11-15</td> - <td> - <a href="#" class="btn btn-primary btn-sm">Edit</a> - </td> - <td> - <a href="#" class="btn btn-danger btn-sm">Delete</a> - </td> - </tr> - </tbody> - </table> + </tr> -</div> + <!-- Edit Modal for each discount --> + <div + class="modal fade" + id="editModal{{ discount.id }}" + tabindex="-1" + > + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Edit Offer</h5> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + ></button> + </div> + <form + action="{{ url_for('discounts.edit_offer', id=discount.id) }}" + method="POST" + > + <div class="modal-body"> + <div class="mb-3"> + <label class="form-label">Offer Name</label> + <input + type="text" + name="name" + class="form-control" + value="{{ discount.description }}" + required + /> + </div> + <div class="mb-3"> + <label class="form-label">Discount Value (%)</label> + <input + type="number" + name="discount_value" + class="form-control" + value="{{ discount.discount_percent }}" + required + /> + </div> + <div class="row"> + <div class="col-md-6 mb-3"> + <label class="form-label">Valid From</label> + <input + type="date" + name="valid_from" + class="form-control" + value="{{ discount.valid_from.strftime('%Y-%m-%d') }}" + required + /> + </div> + <div class="col-md-6 mb-3"> + <label class="form-label">Valid Until</label> + <input + type="date" + name="valid_until" + class="form-control" + value="{{ discount.valid_to.strftime('%Y-%m-%d') }}" + required + /> + </div> + </div> + </div> + <div class="modal-footer"> + <button + type="button" + class="btn btn-secondary" + data-bs-dismiss="modal" + > + Cancel + </button> + <button type="submit" class="btn btn-primary"> + Save Changes + </button> + </div> + </form> + </div> + </div> + </div> + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> + </div> -<!-- Add New Offer Button --> -<a href="#" class="btn btn-success">Add New Offer</a> -</div> + <!-- Add Offer Modal --> + <div class="modal fade" id="addOfferModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Add New Offer</h5> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + ></button> + </div> + <form action="/offers/add" method="POST"> + <div class="modal-body"> + <div class="mb-3"> + <label class="form-label">Offer Name</label> + <input + type="text" + name="name" + class="form-control" + placeholder="Enter offer name" + required + /> + </div> + <div class="mb-3"> + <label class="form-label">Discount Type</label> + <select name="discount_type" class="form-select" required> + <option value="" disabled selected>Select discount type</option> + <option value="percentage">Percentage Off</option> + <option value="fixed">Fixed Amount Off</option> + <option value="bogo">Buy One Get One</option> + </select> + </div> + <div class="mb-3"> + <label class="form-label">Discount Value</label> + <input + type="number" + name="discount_value" + class="form-control" + placeholder="Enter discount value" + required + /> + </div> + <div class="row"> + <div class="col-md-6 mb-3"> + <label class="form-label">Valid From</label> + <input + type="date" + name="valid_from" + class="form-control" + required + /> + </div> + <div class="col-md-6 mb-3"> + <label class="form-label">Valid Until</label> + <input + type="date" + name="valid_until" + class="form-control" + required + /> + </div> + </div> + </div> + <div class="modal-footer"> + <button + type="button" + class="btn btn-secondary" + data-bs-dismiss="modal" + > + Cancel + </button> + <button type="submit" class="btn btn-primary">Add Offer</button> + </div> + </form> + </div> + </div> + </div> + + <!-- Filter Modal --> + <div class="modal fade" id="filterModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Filter Offers</h5> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + ></button> + </div> + <div class="modal-body"> + <form id="filterForm"> + <div class="mb-3"> + <label class="form-label">Status</label> + <select class="form-select" name="status"> + <option value="">All Status</option> + <option value="active">Active</option> + <option value="upcoming">Upcoming</option> + <option value="expired">Expired</option> + </select> + </div> + <div class="mb-3"> + <label class="form-label">Discount Type</label> + <select class="form-select" name="discount_type"> + <option value="">All Types</option> + <option value="percentage">Percentage Off</option> + <option value="fixed">Fixed Amount Off</option> + <option value="bogo">Buy One Get One</option> + </select> + </div> + <div class="mb-3"> + <label class="form-label">Date Range</label> + <div class="row"> + <div class="col-md-6"> + <input + type="date" + class="form-control" + name="start_date" + placeholder="Start Date" + /> + </div> + <div class="col-md-6"> + <input + type="date" + class="form-control" + name="end_date" + placeholder="End Date" + /> + </div> + </div> + </div> + </form> + </div> + <div class="modal-footer"> + <button + type="button" + class="btn btn-secondary" + data-bs-dismiss="modal" + > + Cancel + </button> + <button + type="button" + class="btn btn-primary" + onclick="applyFilters()" + > + Apply Filters + </button> + </div> + </div> + </div> + </div> + {% endblock %} {% block scripts %} + <script> + function confirmDelete(offerId) { + if (confirm("Are you sure you want to delete this offer?")) { + window.location.href = `/offers/delete/${offerId}`; + } + } + + function applyFilters() { + const formData = new FormData(document.getElementById("filterForm")); + const params = new URLSearchParams(); -{% endblock %} \ No newline at end of file + for (let [key, value] of formData.entries()) { + if (value) params.append(key, value); + } + + if (params.toString()) { + window.location.href = `${ + window.location.pathname + }?${params.toString()}`; + } + + bootstrap.Modal.getInstance( + document.getElementById("filterModal") + ).hide(); + } + </script> + {% endblock %} +</div> diff --git a/app/templates/home.html b/app/templates/home.html index c146ba73cf43beb62966e85cac4f51085c217407..5be5b0d9dca0324dff3c1590f420e5d0d11e3228 100644 --- a/app/templates/home.html +++ b/app/templates/home.html @@ -1,61 +1,409 @@ -{% extends 'base.html' %} - -{% block content %} -<div class="container"> - <h1 class="text-center pb-4">Dashboard</h1> - - <div class="row"> - <div class="col-md-4 mb-4"> - <div class="card text-white bg-primary"> - <div class="card-body"> - <h5 class="card-title">Total Reservations</h5> - <p class="card-text">150</p> - </div> - </div> +{% extends 'base.html' %} {% block content %} +<div class="container-fluid"> + <h2 class="mb-4">Dashboard</h2> + + <!-- Stats Cards Row --> + <div class="row mb-4"> + <!-- Total Revenue Card --> + <div class="col-md-3 mb-3"> + <div class="card h-100 border-0 shadow-sm"> + <div class="card-body d-flex align-items-center"> + <div class="icon-container bg-light rounded-circle p-3 me-3"> + <i class="bi bi-currency-dollar text-success fs-4"></i> + </div> + <div> + <h6 class="text-muted mb-1">Total Revenue</h6> + <h3 class="mb-0">${{ "%.2f"|format(total_revenue) }}</h3> + </div> </div> - <div class="col-md-4 mb-4"> - <div class="card text-white bg-success"> - <div class="card-body"> - <h5 class="card-title">Total Orders</h5> - <p class="card-text">300</p> - </div> - </div> + </div> + </div> + + <!-- Total Orders Card --> + <div class="col-md-3 mb-3"> + <div class="card h-100 border-0 shadow-sm"> + <div class="card-body d-flex align-items-center"> + <div class="icon-container bg-light rounded-circle p-3 me-3"> + <i class="bi bi-cart text-primary fs-4"></i> + </div> + <div> + <h6 class="text-muted mb-1">Total Orders</h6> + <h3 class="mb-0">{{ total_orders }}</h3> + </div> </div> - <div class="col-md-4 mb-4"> - <div class="card text-white bg-warning"> - <div class="card-body"> - <h5 class="card-title">Pending Payments</h5> - <p class="card-text">20</p> - </div> - </div> + </div> + </div> + + <!-- Total Menu Card --> + <div class="col-md-3 mb-3"> + <div class="card h-100 border-0 shadow-sm"> + <div class="card-body d-flex align-items-center"> + <div class="icon-container bg-light rounded-circle p-3 me-3"> + <i class="bi bi-journal-text text-info fs-4"></i> + </div> + <div> + <h6 class="text-muted mb-1">Total Menu</h6> + <h3 class="mb-0">{{ total_menu }}</h3> + </div> </div> + </div> </div> - <div class="row"> - <div class="col-md-4 mb-4"> - <div class="card text-white bg-info"> - <div class="card-body"> - <h5 class="card-title">Inventory Items</h5> - <p class="card-text">75</p> - </div> - </div> + <!-- Total Staff Card --> + <div class="col-md-3 mb-3"> + <div class="card h-100 border-0 shadow-sm"> + <div class="card-body d-flex align-items-center"> + <div class="icon-container bg-light rounded-circle p-3 me-3"> + <i class="bi bi-people text-warning fs-4"></i> + </div> + <div> + <h6 class="text-muted mb-1">Total Staff</h6> + <h3 class="mb-0">{{ total_staff }}</h3> + </div> </div> - <div class="col-md-4 mb-4"> - <div class="card text-white bg-danger"> - <div class="card-body"> - <h5 class="card-title">Active Promotions</h5> - <p class="card-text">3</p> - </div> + </div> + </div> + + <!-- Charts Row --> + <div class="row mb-4"> + <!-- Revenue Chart --> + <div class="col-md-6 mb-3"> + <div class="card border-0 shadow-sm"> + <div + class="card-header bg-white d-flex justify-content-between align-items-center" + > + <h5 class="card-title mb-0">Total Revenue</h5> + <div class="dropdown"> + <button + class="btn btn-sm btn-outline-secondary dropdown-toggle" + type="button" + id="revenueFilter" + data-bs-toggle="dropdown" + aria-expanded="false" + > + {% if request.args.get('revenue_period') == 'daily' %} Daily {% + elif request.args.get('revenue_period') == 'weekly' %} Weekly {% + elif request.args.get('revenue_period') == 'yearly' %} Yearly {% + else %} Monthly {% endif %} + </button> + <ul class="dropdown-menu" aria-labelledby="revenueFilter"> + <li> + <a + class="dropdown-item" + href="{{ url_for('home.homepage', revenue_period='daily', period=request.args.get('period', 'last_6_months')) }}" + >Daily</a + > + </li> + <li> + <a + class="dropdown-item" + href="{{ url_for('home.homepage', revenue_period='weekly', period=request.args.get('period', 'last_6_months')) }}" + >Weekly</a + > + </li> + <li> + <a + class="dropdown-item" + href="{{ url_for('home.homepage', revenue_period='monthly', period=request.args.get('period', 'last_6_months')) }}" + >Monthly</a + > + </li> + <li> + <a + class="dropdown-item" + href="{{ url_for('home.homepage', revenue_period='yearly', period=request.args.get('period', 'last_6_months')) }}" + >Yearly</a + > + </li> + </ul> </div> + </div> + <div class="card-body"> + <div id="revenueChart" style="height: 300px"></div> + </div> </div> - <div class="col-md-4 mb-4"> - <div class="card text-white bg-secondary"> - <div class="card-body"> - <h5 class="card-title">Daily Revenue</h5> - <p class="card-text">$1,250</p> - </div> + </div> + + <!-- Orders Chart --> + <div class="col-md-6 mb-3"> + <div class="card border-0 shadow-sm"> + <div + class="card-header bg-white d-flex justify-content-between align-items-center" + > + <h5 class="card-title mb-0">Total Orders</h5> + <div class="dropdown"> + <button + class="btn btn-sm btn-outline-secondary dropdown-toggle" + type="button" + id="ordersFilter" + data-bs-toggle="dropdown" + aria-expanded="false" + > + {% if request.args.get('period') == 'last_3_months' %} Last 3 + months {% elif request.args.get('period') == 'this_year' %} This + year {% else %} Last 6 months {% endif %} + </button> + <ul class="dropdown-menu" aria-labelledby="ordersFilter"> + <li> + <a + class="dropdown-item" + href="{{ url_for('home.homepage', period='last_3_months') }}" + >Last 3 months</a + > + </li> + <li> + <a + class="dropdown-item" + href="{{ url_for('home.homepage', period='last_6_months') }}" + >Last 6 months</a + > + </li> + <li> + <a + class="dropdown-item" + href="{{ url_for('home.homepage', period='this_year') }}" + >This year</a + > + </li> + </ul> </div> + </div> + <div class="card-body"> + <div id="ordersChart" style="height: 300px"></div> + </div> + </div> + </div> + </div> + + <!-- New Orders Table --> + <div class="card border-0 shadow-sm mb-4"> + <div + class="card-header bg-white d-flex justify-content-between align-items-center" + > + <h5 class="card-title mb-0">New Orders</h5> + <div class="dropdown"> + <button + class="btn btn-sm btn-outline-secondary dropdown-toggle" + type="button" + id="newOrdersFilter" + data-bs-toggle="dropdown" + aria-expanded="false" + > + Filter + </button> + <ul class="dropdown-menu" aria-labelledby="newOrdersFilter"> + <li><a class="dropdown-item" href="#">Today</a></li> + <li><a class="dropdown-item" href="#">Yesterday</a></li> + <li><a class="dropdown-item" href="#">Last 7 Days</a></li> + </ul> </div> + </div> + <div class="card-body"> + <div class="table-responsive"> + <table class="table table-hover"> + <thead> + <tr> + <th>Order ID</th> + <th>Customer Name</th> + <th>Date</th> + <th>Time</th> + <th>Amount</th> + <th>Payment Type</th> + <th>Status</th> + <th>Action</th> + </tr> + </thead> + <tbody> + {% for order in new_orders %} + <tr> + <td>#{{ order.id }}</td> + <td>{{ order.customer_name }}</td> + <td>{{ order.date }}</td> + <td>{{ order.time }}</td> + <td>${{ "%.2f"|format(order.amount) }}</td> + <td>{{ order.payment_type|title }}</td> + <td> + <span + class="badge bg-{{ 'success' if order.status == 'completed' else 'warning' if order.status == 'pending' else 'danger' if order.status == 'cancelled' else 'info' }}" + >{{ order.status|title }}</span + > + </td> + <td> + <div class="d-flex"> + <a + href="{{ url_for('order.order_detail', order_id=order.id) }}" + class="btn btn-sm btn-success me-2" + >View</a + > + {% if order.status == 'pending' %} + <button class="btn btn-sm btn-light"> + <i class="bi bi-three-dots"></i> + </button> + {% endif %} + </div> + </td> + </tr> + {% else %} + <tr> + <td colspan="8" class="text-center">No orders found</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + <div class="d-flex justify-content-center mt-3"> + <a + href="{{url_for('order.order_history')}}" + class="btn btn-outline-primary" + >See All</a + > + </div> + </div> </div> + </div> + {% endblock %} {% block scripts %} + <script src="https://cdn.jsdelivr.net/npm/apexcharts"></script> + <script> + // Revenue Chart + var revenueOptions = { + series: [{ + name: "Revenue", + data: {{ revenue_amounts|tojson }} + }], + chart: { + type: "area", + height: 300, + toolbar: { + show: false + } + }, + colors: ["#00a389"], + dataLabels: { + enabled: false + }, + stroke: { + curve: "smooth", + width: 2 + }, + fill: { + type: "gradient", + gradient: { + shadeIntensity: 1, + opacityFrom: 0.7, + opacityTo: 0.2, + stops: [0, 90, 100] + } + }, + xaxis: { + categories: {{ revenue_labels|tojson }}, + labels: { + style: { + colors: "#888" + } + } + }, + yaxis: { + labels: { + formatter: function(val) { + return "$" + val.toFixed(2); + }, + style: { + colors: "#888" + } + } + }, + grid: { + borderColor: "#f1f1f1", + row: { + colors: ["transparent", "transparent"], + opacity: 0.5 + } + }, + markers: { + size: 4, + colors: ["#00a389"], + strokeColors: "#fff", + strokeWidth: 2, + hover: { + size: 7 + } + }, + tooltip: { + y: { + formatter: function(val) { + return "$" + val.toFixed(2); + } + } + } + }; + + var revenueChart = new ApexCharts( + document.querySelector("#revenueChart"), + revenueOptions + ); + revenueChart.render(); + + // Orders Chart + var ordersOptions = { + series: [{ + name: "Orders", + data: {{ order_counts|tojson }} + }], + chart: { + type: "bar", + height: 300, + toolbar: { + show: false + } + }, + colors: ["#00a389"], + plotOptions: { + bar: { + borderRadius: 8, + columnWidth: "30%" + } + }, + dataLabels: { + enabled: false + }, + xaxis: { + categories: {{ order_months|tojson }}, + labels: { + style: { + colors: "#888" + } + } + }, + yaxis: { + labels: { + style: { + colors: "#888" + }, + formatter: function(val) { + return Math.round(val); // Show whole numbers only + } + } + }, + grid: { + borderColor: "#f1f1f1", + row: { + colors: ["transparent", "transparent"], + opacity: 0.5 + } + }, + tooltip: { + shared: true, + intersect: false, + y: { + formatter: function(val) { + return val + " orders"; + } + } + } + }; + + var ordersChart = new ApexCharts(document.querySelector("#ordersChart"), ordersOptions); + ordersChart.render(); + </script> + {% endblock %} </div> -{% endblock %} diff --git a/app/templates/inventory/manage_inventory.html b/app/templates/inventory/manage_inventory.html index 8e28be1de282c258d93dd3e4986099285dd861b3..e572b401196e7fdc0c070884c3ebc8b9d5b94887 100644 --- a/app/templates/inventory/manage_inventory.html +++ b/app/templates/inventory/manage_inventory.html @@ -1,36 +1,208 @@ {% extends 'base.html' %} +{% block title %}HRMS - Inventory Management{% endblock %} + +{% block styles %} +<style> + /* Layout styles */ + .page-container { + padding: 2rem; + background-color: #f8f9fa; + min-height: calc(100vh - 60px); + } + + .page-title { + font-size: 1.5rem; + font-weight: 600; + color: #2c3e50; + margin-bottom: 1.5rem; + } + + /* Card styles */ + .stats-card { + background: white; + border-radius: 10px; + padding: 1.25rem; + margin-bottom: 1.5rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.04); + } + + .stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; + } + + .stat-item { + text-align: center; + padding: 1rem; + border-radius: 8px; + background: #f8f9fa; + } + + .stat-value { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 0.5rem; + } + + .stat-label { + color: #6c757d; + font-size: 0.875rem; + } + + /* Search and filter section */ + .search-filter-section { + background: white; + padding: 1.5rem; + border-radius: 10px; + box-shadow: 0 2px 4px rgba(0,0,0,0.04); + margin-bottom: 1.5rem; + } + + .search-input { + max-width: 400px; + border-radius: 8px; + padding: 0.75rem 1rem 0.75rem 2.75rem; + border: 1px solid #e9ecef; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%236c757d' class='bi bi-search' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: 1rem center; + background-size: 16px; + } + + /* Table styles */ + .inventory-table { + background: white; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0,0,0,0.04); + } + + .inventory-table th { + background-color: #f8f9fa; + font-weight: 600; + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.5px; + padding: 1rem; + border-bottom: 2px solid #e9ecef; + color: #495057; + } + + .inventory-table td { + padding: 1rem; + vertical-align: middle; + border-bottom: 1px solid #e9ecef; + } + + .inventory-table tr:hover { + background-color: #f8f9fa; + } + + /* Status badges */ + .status-badge { + padding: 0.4em 1em; + border-radius: 30px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .status-sufficient { + background-color: #d1e7dd; + color: #0f5132; + } + + .status-low { + background-color: #fff3cd; + color: #997404; + } + + .status-out { + background-color: #f8d7da; + color: #842029; + } + + /* Category badges */ + .category-badge { + background-color: #e9ecef; + color: #495057; + padding: 0.4em 1em; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 500; + } + + /* Action buttons */ + .btn-icon { + padding: 0.4rem; + border-radius: 6px; + margin-left: 0.25rem; + } + + .btn-icon i { + font-size: 1rem; + } +</style> +{% endblock %} + {% block content %} +<div class="page-container"> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h1 class="page-title mb-0">Inventory Management</h1> + <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addItemModal"> + <i class="bi bi-plus-lg me-2"></i>Add New Item + </button> + </div> -<div class="container"> - <h1 class="mt-4">Manage Inventory</h1> + <!-- Stats Cards --> + <div class="stats-grid"> + <div class="stat-item"> + <div class="stat-value text-success">{{ inventory|selectattr('status', 'equalto', 'sufficient')|list|length }}</div> + <div class="stat-label">Sufficient Items</div> + </div> + <div class="stat-item"> + <div class="stat-value text-warning">{{ inventory|selectattr('status', 'equalto', 'low')|list|length }}</div> + <div class="stat-label">Low Stock Items</div> + </div> + <div class="stat-item"> + <div class="stat-value text-danger">{{ inventory|selectattr('status', 'equalto', 'out')|list|length }}</div> + <div class="stat-label">Out of Stock</div> + </div> + <div class="stat-item"> + <div class="stat-value">{{ inventory|length }}</div> + <div class="stat-label">Total Items</div> + </div> + </div> - <!-- Add Inventory Form --> - <form action="/inventory/add" method="POST" class="mb-4"> - <div class="row g-3"> - <div class="col-md-3"> - <input type="text" name="name" class="form-control" placeholder="Item Name" required> - </div> - <div class="col-md-2"> - <input type="number" name="quantity" class="form-control" placeholder="Quantity" required> - </div> - <div class="col-md-3"> - <input type="text" name="category" class="form-control" placeholder="Category" required> - </div> - <div class="col-md-2"> - <select name="status" class="form-select" required> - <option value="low">Low</option> - <option value="sufficient">Sufficient</option> - <option value="out">Out of Stock</option> - </select> - </div> - <div class="col-md-2"> - <button type="submit" class="btn btn-primary">Add Item</button> + <!-- Search and Filter Section --> + <div class="search-filter-section"> + <div class="d-flex justify-content-between align-items-center flex-wrap gap-3"> + <form action="{{ url_for('inventory.manage_inventory') }}" method="GET" class="d-flex align-items-center flex-grow-1 me-3"> + <div class="input-group"> + <input type="text" name="search" class="form-control search-input" + placeholder="Search inventory items..." value="{{ request.args.get('search', '') }}"> + <button type="submit" class="btn btn-primary">Search</button> + </div> + </form> + <div class="d-flex gap-2"> + <button class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#filterModal"> + <i class="bi bi-funnel me-2"></i>Filter + </button> + {% if request.args %} + <a href="{{ url_for('inventory.manage_inventory') }}" class="btn btn-outline-secondary"> + <i class="bi bi-x-circle me-2"></i>Clear Filters + </a> + {% endif %} </div> </div> - </form> + </div> <!-- Inventory Table --> +<<<<<<< HEAD <table class="table table-bordered"> <thead class="table-dark"> <tr> @@ -70,7 +242,225 @@ {% endfor %} </tbody> </table> +======= + <div class="inventory-table"> + <div class="table-responsive"> + <table class="table table-hover mb-0"> + <thead> + <tr> + <th>Item Name</th> + <th>Category</th> + <th class="text-center">Quantity</th> + <th class="text-center">Status</th> + <th class="text-end">Actions</th> + </tr> + </thead> + <tbody> + {% for item in inventory %} + <tr> + <td class="fw-medium">{{ item.name }}</td> + <td><span class="category-badge">{{ item.category }}</span></td> + <td class="text-center">{{ item.quantity }}</td> + <td class="text-center"> + <span class="status-badge status-{{ item.status.lower() }}"> + {{ item.status }} + </span> + </td> + <td class="text-end"> + <button type="button" class="btn btn-outline-primary btn-icon" + data-bs-toggle="modal" data-bs-target="#editModal{{ item.id }}"> + <i class="bi bi-pencil"></i> + </button> + <button type="button" class="btn btn-outline-danger btn-icon" + data-bs-toggle="modal" data-bs-target="#deleteModal{{ item.id }}"> + <i class="bi bi-trash"></i> + </button> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + + {% if not inventory %} + <div class="text-center py-5"> + <div class="text-muted">No inventory items found</div> + </div> + {% endif %} +>>>>>>> dev +</div> + +<!-- Add Item Modal --> +<div class="modal fade" id="addItemModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Add New Item</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + <form action="{{ url_for('inventory.add_item') }}" method="POST"> + <div class="modal-body"> + <div class="mb-3"> + <label class="form-label">Item Name</label> + <input type="text" name="name" class="form-control" required> + </div> + <div class="mb-3"> + <label class="form-label">Category</label> + <select name="category" class="form-select" required> + <option value="" disabled selected>Select category</option> + {% for category in categories %} + <option value="{{ category }}">{{ category }}</option> + {% endfor %} + </select> + </div> + <div class="mb-3"> + <label class="form-label">Quantity</label> + <input type="number" name="quantity" class="form-control" min="0" required> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> + <button type="submit" class="btn btn-primary">Add Item</button> + </div> + </form> + </div> + </div> +</div> + +<!-- Filter Modal --> +<div class="modal fade" id="filterModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Filter Inventory</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + <form action="{{ url_for('inventory.manage_inventory') }}" method="GET"> + <div class="modal-body"> + <div class="mb-3"> + <label class="form-label">Category</label> + <select class="form-select" name="category"> + <option value="">All Categories</option> + {% for category in categories %} + <option value="{{ category }}" {% if request.args.get('category') == category %}selected{% endif %}> + {{ category }} + </option> + {% endfor %} + </select> + </div> + <div class="mb-3"> + <label class="form-label">Status</label> + <select class="form-select" name="status"> + <option value="">All Status</option> + <option value="low" {% if request.args.get('status') == 'low' %}selected{% endif %}>Low Stock</option> + <option value="sufficient" {% if request.args.get('status') == 'sufficient' %}selected{% endif %}>Sufficient</option> + <option value="out" {% if request.args.get('status') == 'out' %}selected{% endif %}>Out of Stock</option> + </select> + </div> + <div class="mb-3"> + <label class="form-label">Quantity Range</label> + <div class="input-group"> + <input type="number" class="form-control" name="min_quantity" + placeholder="Min" value="{{ request.args.get('min_quantity', '') }}" min="0"> + <span class="input-group-text">to</span> + <input type="number" class="form-control" name="max_quantity" + placeholder="Max" value="{{ request.args.get('max_quantity', '') }}" min="0"> + </div> + </div> + </div> + <div class="modal-footer"> + <a href="{{ url_for('inventory.manage_inventory') }}" class="btn btn-secondary">Clear Filters</a> + <button type="submit" class="btn btn-primary">Apply Filters</button> + </div> + </form> + </div> + </div> +</div> + +<!-- Edit/Delete Modals for each item --> +{% for item in inventory %} +<!-- Edit Modal --> +<div class="modal fade" id="editModal{{ item.id }}" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Edit Item</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + <form action="{{ url_for('inventory.edit_item') }}" method="POST"> + <div class="modal-body"> + <input type="hidden" name="item_id" value="{{ item.id }}"> + <div class="mb-3"> + <label class="form-label">Item Name</label> + <input type="text" name="name" value="{{ item.name }}" class="form-control" required> + </div> + <div class="mb-3"> + <label class="form-label">Category</label> + <select name="category" class="form-select" required> + {% for category in categories %} + <option value="{{ category }}" {% if item.category == category %}selected{% endif %}> + {{ category }} + </option> + {% endfor %} + </select> + </div> + <div class="mb-3"> + <label class="form-label">Quantity</label> + <input type="number" name="quantity" value="{{ item.quantity }}" class="form-control" min="0" required> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> + <button type="submit" class="btn btn-primary">Save Changes</button> + </div> + </form> + </div> + </div> +</div> + +<!-- Delete Modal --> +<div class="modal fade" id="deleteModal{{ item.id }}" tabindex="-1"> + <div class="modal-dialog modal-sm"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Confirm Delete</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + <div class="modal-body"> + <p class="mb-0">Are you sure you want to delete "{{ item.name }}"?</p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> + <a href="{{ url_for('inventory.delete_item', item_id=item.id) }}" class="btn btn-danger">Delete</a> + </div> + </div> + </div> </div> +{% endfor %} +{% endblock %} +{% block scripts %} +<script> +function confirmDelete(itemId) { + if (confirm('Are you sure you want to delete this item?')) { + window.location.href = `/inventory/delete/${itemId}`; + } +} +function applyFilters() { + const formData = new FormData(document.getElementById('filterForm')); + const params = new URLSearchParams(); + + for (let [key, value] of formData.entries()) { + if (value) params.append(key, value); + } + + if (params.toString()) { + window.location.href = `${window.location.pathname}?${params.toString()}`; + } + + bootstrap.Modal.getInstance(document.getElementById('filterModal')).hide(); +} +</script> {% endblock %} \ No newline at end of file diff --git a/app/templates/js/login.js b/app/templates/js/login.js new file mode 100644 index 0000000000000000000000000000000000000000..cdc17a56f29d9f20ff51c634f9d307f997528f7f --- /dev/null +++ b/app/templates/js/login.js @@ -0,0 +1,4 @@ +// Initialization for ES Users +import { Input, initMDB } from "mdb-ui-kit"; + +initMDB({ Input }); \ No newline at end of file diff --git a/app/static/js/scripts.js b/app/templates/js/scripts.js similarity index 100% rename from app/static/js/scripts.js rename to app/templates/js/scripts.js diff --git a/app/templates/menu/add_new_dish.html b/app/templates/menu/add_new_dish.html new file mode 100644 index 0000000000000000000000000000000000000000..d2f7eac3c5cfbe19dcaecf72ca83605b4570fdab --- /dev/null +++ b/app/templates/menu/add_new_dish.html @@ -0,0 +1,48 @@ +<!-- templates/orders/menu.html --> +{% extends 'base.html' %} + +{% block title %}HRMS - {% if menu_item %}Edit{% else %}Add New{% endif %} Dish{% endblock %} + +{% block page_title %}{% if menu_item %}Edit{% else %}Add New{% endif %} Dish{% endblock %} + +{% block content %} +<div class="card"> + <div class="card-body"> + <form method="POST" action="{% if menu_item %}{{ url_for('menu.update_menu_item', item_id=menu_item.id) }}{% else %}{{ url_for('menu.add_new_menu_item') }}{% endif %}"> + <div class="mb-3"> + <label for="name" class="form-label">Dish Name</label> + <input type="text" class="form-control" id="name" name="name" value="{{ menu_item.name if menu_item else '' }}" required> + </div> + + <div class="mb-3"> + <label for="price" class="form-label">Price</label> + <div class="input-group"> + <span class="input-group-text">$</span> + <input type="number" class="form-control" id="price" name="price" step="0.01" value="{{ menu_item.price if menu_item else '' }}" required> + </div> + </div> + + <div class="mb-3"> + <label for="description" class="form-label">Description</label> + <textarea class="form-control" id="description" name="description" rows="3">{{ menu_item.description if menu_item else '' }}</textarea> + </div> + + {% if menu_item %} + <div class="mb-3"> + <div class="form-check"> + <input class="form-check-input" type="checkbox" id="available" name="available" {% if menu_item.available %}checked{% endif %}> + <label class="form-check-label" for="available"> + Available + </label> + </div> + </div> + {% endif %} + + <div class="d-flex justify-content-between"> + <a href="{{ url_for('menu.menu') }}" class="btn btn-secondary">Cancel</a> + <button type="submit" class="btn btn-primary">{% if menu_item %}Update{% else %}Add{% endif %} Dish</button> + </div> + </form> + </div> +</div> +{% endblock %} \ No newline at end of file diff --git a/app/templates/orders/new_order.html b/app/templates/menu/cancel_new_dish.html similarity index 100% rename from app/templates/orders/new_order.html rename to app/templates/menu/cancel_new_dish.html diff --git a/app/templates/menu/menu.html b/app/templates/menu/menu.html index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..90d4ff00ed47b53130de64db27f6468f38294a6d 100644 --- a/app/templates/menu/menu.html +++ b/app/templates/menu/menu.html @@ -0,0 +1,132 @@ +<!-- templates/orders/menu.html --> +{% extends 'base.html' %} {% block title %}HRMS - Menu Items{% endblock %} {% +block page_title %}Menu Items{% endblock %} {% block content %} +<!-- Search and Add New Dish --> +<div class="d-flex justify-content-between mb-3"> + <div class="input-group" style="width: 300px"> + <input type="text" class="form-control" placeholder="Search" /> + <span class="input-group-text"><i class="bi bi-search"></i></span> + </div> + <div> + <a href="{{url_for ('menu.add_new_menu_item')}}" class="btn btn-success" + ><i class="bi bi-plus"></i> Add New Dish</a + > + <button class="btn btn-outline-secondary ms-2"> + <i class="bi bi-funnel"></i> Filter + </button> + </div> +</div> + +<!-- Menu Items Table --> +<div class="table-responsive"> + <table class="table table-hover"> + <thead> + <tr> + <th scope="col">Sr No.</th> + <th scope="col">Dish Name</th> + <th scope="col">Price</th> + <th scope="col">Status</th> + <th scope="col">Action</th> + </tr> + </thead> + <tbody> + {% for item in menu_items %} + <tr> + <td>{{ loop.index }}</td> + <td>{{ item.name }}</td> + <td>${{ item.price }}</td> + <td> + <div class="form-check form-switch"> + <input + class="form-check-input" + type="checkbox" + role="switch" + id="statusSwitch{{ item.id }}" + {% + if + item.is_active + %}checked{% + endif + %} + disabled + /> + </div> + </td> + <td> + <a + href="{{url_for('menu.update_menu_item', item_id=item.id)}}" + class="me-2" + ><i class="bi bi-pencil"></i + ></a> + <form + method="POST" + action="{{ url_for('menu.delete_menu_item', item_id=item.id) }}" + style="display: inline" + onsubmit="return confirm('Are you sure delete it?');" + > + <button + type="submit" + class="btn btn-link p-0 m-0" + style="border: none; background: none" + > + <i class="bi bi-trash text-danger"></i> + </button> + </form> + </td> + </tr> + {% else %} + <tr> + <td colspan="5" class="text-center">No menu items found.</td> + </tr> + {% endfor %} + </tbody> + </table> +</div> + +<!-- Pagination --> +<div class="d-flex justify-content-between align-items-center"> + <span + >Showing {{ pagination.items|length }} of {{ pagination.total }} + records</span + > + <nav> + <ul class="pagination"> + {% if pagination.has_prev %} + <li class="page-item"> + <a + class="page-link" + href="{{ url_for('menu.menu', page=pagination.prev_num) }}" + >Previous</a + > + </li> + {% else %} + <li class="page-item disabled"> + <a class="page-link" href="#">Previous</a> + </li> + {% endif %} {% for page in pagination.iter_pages() %} {% if page %} + <li class="page-item {% if page == pagination.page %}active{% endif %}"> + <a class="page-link" href="{{ url_for('menu.menu', page=page) }}" + >{{ page }}</a + > + </li> + {% else %} + <li class="page-item disabled"> + <span class="page-link">...</span> + </li> + {% endif %} {% endfor %} {% if pagination.has_next %} + <li class="page-item"> + <a + class="page-link" + href="{{ url_for('menu.menu', page=pagination.next_num) }}" + >Next</a + > + </li> + {% else %} + <li class="page-item disabled"> + <a class="page-link" href="#">Next</a> + </li> + {% endif %} + </ul> + </nav> +</div> +{% endblock %} diff --git a/app/templates/orders/create_order.html b/app/templates/orders/create_order.html new file mode 100644 index 0000000000000000000000000000000000000000..1714d43c6c3abcf81c431915ef1c5e464430f5c0 --- /dev/null +++ b/app/templates/orders/create_order.html @@ -0,0 +1,691 @@ +{% extends 'base.html' %} {% block title %}HRMS - New Order{% endblock %} {% +block styles %} +<style> + .order-item { + background-color: #fff; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + padding: 1rem; + margin-bottom: 1rem; + } + + .order-item:hover { + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); + } + + .remove-item { + width: 36px; + height: 36px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + } + + .total-section { + background-color: #f8f9fa; + border-radius: 8px; + padding: 1.5rem; + margin-top: 2rem; + } + + .total-amount { + font-size: 1.5rem; + font-weight: 500; + color: var(--primary-color); + } + + #add-item { + border-style: dashed; + width: 100%; + padding: 0.75rem; + } + + .alert { + border-radius: 8px; + margin-bottom: 1.5rem; + } +</style> +{% endblock %} {% block page_title %} +<nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"> + <a href="{{ url_for('order.order_history') }}">Orders</a> + </li> + <li class="breadcrumb-item active">New Order</li> + </ol> +</nav> +{% endblock %} {% block content %} +<div class="container-fluid"> + <div class="row justify-content-center"> + <div class="col-lg-8"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title mb-4">Create New Order</h5> + + {% with messages = get_flashed_messages(with_categories=true) %} {% if + messages %} {% for category, message in messages %} + <div class="alert alert-{{ category }} alert-dismissible fade show"> + {{ message }} + <button + type="button" + class="btn-close" + data-bs-dismiss="alert" + ></button> + </div> + {% endfor %} {% endif %} {% endwith %} + + <form + method="POST" + action="{{ url_for('order.create_order') }}" + id="orderForm" + > + <div class="mb-4"> + <label for="restaurant_id" class="form-label">Restaurant</label> + <select + class="form-select form-select-lg" + id="restaurant_id" + name="restaurant_id" + required + > + <option value="">Select restaurant...</option> + {% for restaurant in restaurants %} + <option value="{{ restaurant.id }}"> + {{ restaurant.name }} ({{ restaurant.address }}) + </option> + {% endfor %} + </select> + </div> + + <div class="mb-4"> + <label for="user_search" class="form-label" + >Customer Information</label + > + <div class="position-relative"> + <input + type="text" + class="form-control form-control-lg" + id="user_search" + placeholder="Search by phone number..." + autocomplete="off" + /> + <input type="hidden" id="user_id" name="user_id" required /> + <div + id="searchResults" + class="position-absolute w-100 mt-1 d-none" + style=" + z-index: 1000; + max-height: 200px; + overflow-y: auto; + background: white; + border: 1px solid #dee2e6; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + " + ></div> + </div> + <div id="selectedCustomerInfo" class="mt-2 d-none"> + <div class="card"> + <div class="card-body"> + <h6 class="card-subtitle mb-2 text-muted"> + Selected Customer + </h6> + <p class="mb-1"> + <strong>Name:</strong> <span id="customerName"></span> + </p> + <p class="mb-1"> + <strong>Phone:</strong> <span id="customerPhone"></span> + </p> + <p class="mb-0"> + <strong>Email:</strong> <span id="customerEmail"></span> + </p> + </div> + </div> + </div> + </div> + + <div class="mb-4"> + <label + class="form-label d-flex justify-content-between align-items-center" + > + <span>Order Items</span> + <button + type="button" + class="btn btn-outline-primary btn-sm" + id="add-item" + > + <i class="bi bi-plus"></i> Add Item + </button> + </label> + + <div id="order-items"> + <div class="order-item"> + <div class="row g-3"> + <div class="col-md-6"> + <select + class="form-select item-select" + name="menu_id[]" + required + > + <option value="">Select item...</option> + {% for item in menu_items %} + <option + value="{{ item.id }}" + data-price="{{ item.price }}" + > + {{ item.name }} (${{ "%.2f"|format(item.price) }}) + </option> + {% endfor %} + </select> + </div> + <div class="col-md-4"> + <div class="input-group"> + <span class="input-group-text">Qty</span> + <input + type="number" + class="form-control quantity-input" + name="quantity[]" + min="1" + value="1" + required + /> + </div> + </div> + <div class="col-md-2"> + <button + type="button" + class="btn btn-outline-danger remove-item" + > + <i class="bi bi-trash"></i> + </button> + </div> + </div> + </div> + </div> + </div> + + <!-- Apply discounts --> + <div class="mb-4"> + <label class="form-label">Apply Discount</label> + <div class="row g-3"> + <div class="col-md-8"> + <select + class="form-select" + id="discount_select" + name="discount_code" + > + <option value="">Select discount...</option> + </select> + </div> + <div class="col-md-4"> + <button + type="button" + class="btn btn-outline-secondary w-100" + onclick="removeDiscount()" + > + Remove Discount + </button> + </div> + </div> + <div id="discount-info" class="mt-2 d-none"> + <div class="alert alert-success"> + <span id="discount-message"></span> + </div> + </div> + </div> + + <!-- Total section --> + <div class="total-section"> + <div class="d-flex justify-content-between align-items-center"> + <div> + <p class="mb-1 text-muted">Subtotal</p> + <div class="mb-2"> + $<span id="subtotal-amount">0.00</span> + </div> + <p class="mb-1 text-muted">Discount</p> + <div class="mb-2"> + -$<span id="discount-amount">0.00</span> + </div> + <p class="mb-1 text-muted">Total Amount</p> + <div class="total-amount"> + $<span id="total-amount">0.00</span> + </div> + </div> + <div class="d-flex gap-2"> + <a + href="{{ url_for('order.order_history') }}" + class="btn btn-outline-secondary" + >Cancel</a + > + <button type="submit" class="btn btn-primary"> + Create Order + </button> + </div> + </div> + <input + type="hidden" + name="applied_discount_id" + id="applied_discount_id" + /> + </div> + </form> + </div> + </div> + </div> + </div> +</div> + +<script> + // Add this before your existing script content + let debounceTimeout; + + function debounce(func, wait) { + return function executedFunction(...args) { + const later = () => { + clearTimeout(debounceTimeout); + func(...args); + }; + clearTimeout(debounceTimeout); + debounceTimeout = setTimeout(later, wait); + }; + } + + async function searchUsers(query) { + try { + const response = await fetch( + `/api/users/search?phone=${encodeURIComponent(query)}` + ); + const data = await response.json(); + + const resultsDiv = document.getElementById("searchResults"); + resultsDiv.innerHTML = ""; + + if (data.users && data.users.length > 0) { + data.users.forEach((user) => { + const div = document.createElement("div"); + div.className = "p-2 hover-bg-light cursor-pointer"; + div.style.cursor = "pointer"; + div.innerHTML = ` + <div class="d-flex justify-content-between"> + <div> + <div class="fw-bold">${user.phone}</div> + <div>${user.full_name}</div> + </div> + <div class="text-muted small">${user.email}</div> + </div> + `; + div.addEventListener("click", () => selectUser(user)); + resultsDiv.appendChild(div); + }); + resultsDiv.classList.remove("d-none"); + } else { + resultsDiv.innerHTML = + '<div class="p-2 text-muted">No users found</div>'; + resultsDiv.classList.remove("d-none"); + } + } catch (error) { + console.error("Error searching users:", error); + } + } + + function selectUser(user) { + document.getElementById("user_id").value = user.id; + document.getElementById("user_search").value = user.phone; + document.getElementById("customerName").textContent = user.full_name; + document.getElementById("customerPhone").textContent = user.phone; + document.getElementById("customerEmail").textContent = user.email; + document.getElementById("selectedCustomerInfo").classList.remove("d-none"); + document.getElementById("searchResults").classList.add("d-none"); + } + + document.addEventListener("DOMContentLoaded", function () { + const searchInput = document.getElementById("user_search"); + const debouncedSearch = debounce((query) => searchUsers(query), 300); + + searchInput.addEventListener("input", (e) => { + const query = e.target.value.trim(); + if (query.length >= 3) { + debouncedSearch(query); + } else { + document.getElementById("searchResults").classList.add("d-none"); + } + }); + + // Close search results when clicking outside + document.addEventListener("click", (e) => { + if ( + !e.target.closest("#user_search") && + !e.target.closest("#searchResults") + ) { + document.getElementById("searchResults").classList.add("d-none"); + } + }); + }); + + // Function to get menu items for the selected restaurant + async function loadMenuItemsByRestaurant(restaurantId) { + console.log("Loading menu items for restaurant:", restaurantId); + if (!restaurantId) { + console.log("No restaurant ID provided, clearing items"); + clearMenuItems(); + return; + } + + try { + const response = await fetch( + `/api/restaurant/${restaurantId}/menu-items` + ); + const result = await response.json(); + console.log("API Response:", result); + + if (result.success) { + console.log("Updating menu items:", result.data); + updateMenuSelects(result.data); + } else { + console.error("Error loading menu items:", result); + clearMenuItems(); + } + } catch (error) { + console.error("Error fetching menu items:", error); + clearMenuItems(); + } + } + + // Clear all items in menu select + function clearMenuItems() { + console.log("Clearing menu items"); + document.querySelectorAll(".item-select").forEach((select) => { + select.innerHTML = '<option value="">Select item...</option>'; + }); + calculateTotal(); + } + + // Update select menus with new items list + function updateMenuSelects(items) { + console.log("Updating select menus with items:", items); + document.querySelectorAll(".item-select").forEach((select) => { + const selectedValue = select.value; + + // Clear all current options + select.innerHTML = '<option value="">Select item...</option>'; + + // Add new options + items.forEach((item) => { + const option = document.createElement("option"); + option.value = item.id; + option.textContent = `${item.name} ($${item.price.toFixed(2)})`; + option.dataset.price = item.price; + select.appendChild(option); + console.log("Added option:", item.name); + }); + + // Try to keep the previously selected value if possible + if (selectedValue && items.some((item) => item.id == selectedValue)) { + select.value = selectedValue; + } + }); + + // Update total amount + calculateTotal(); + } + + async function loadAvailableDiscounts() { + try { + const response = await fetch("/api/discounts/available"); + const result = await response.json(); + + const discountSelect = document.getElementById("discount_select"); + discountSelect.innerHTML = '<option value="">Select discount...</option>'; + + if (result.success && result.discounts.length > 0) { + result.discounts.forEach((discount) => { + const option = document.createElement("option"); + option.value = discount.code; + option.textContent = `${discount.description} (${discount.discount_percent}% off)`; + option.dataset.id = discount.id; + option.dataset.percent = discount.discount_percent; + option.dataset.description = discount.description; + discountSelect.appendChild(option); + }); + } + } catch (error) { + console.error("Error loading discounts:", error); + } + } + document + .getElementById("discount_select") + .addEventListener("change", function () { + if (this.value) { + const selectedOption = this.options[this.selectedIndex]; + const discount = { + id: selectedOption.dataset.id, + discount_percent: selectedOption.dataset.percent, + description: selectedOption.dataset.description, + }; + applySelectedDiscount(discount); + } else { + removeDiscount(); + } + }); + + function applySelectedDiscount(discount) { + currentDiscount = { + id: discount.id, + percent: discount.discount_percent, + }; + + document.getElementById("discount-info").classList.remove("d-none"); + document.getElementById( + "discount-message" + ).textContent = `${discount.description} (${discount.discount_percent}% off)`; + document.getElementById("applied_discount_id").value = discount.id; + document.getElementById("discount_select").disabled = true; + + calculateTotal(); + } + + function removeDiscount() { + currentDiscount = null; + document.getElementById("discount-info").classList.add("d-none"); + document.getElementById("discount_select").value = ""; + document.getElementById("discount_select").disabled = false; + document.getElementById("applied_discount_id").value = ""; + calculateTotal(); + } + + // Listen for changes in the restaurant dropdown + document.addEventListener("DOMContentLoaded", function () { + const restaurantSelect = document.getElementById("restaurant_id"); + console.log("Restaurant select element:", restaurantSelect); + + restaurantSelect.addEventListener("change", function () { + console.log("Restaurant selected:", this.value); + loadMenuItemsByRestaurant(this.value); + }); + + loadAvailableDiscounts(); + }); + + let currentSubtotal = 0; + let currentDiscount = null; + + function calculateTotal() { + let subtotal = 0; + document.querySelectorAll(".order-item").forEach((item) => { + const select = item.querySelector(".item-select"); + const quantityInput = item.querySelector(".quantity-input"); + // Convert price string to number + const price = parseFloat( + select.options[select.selectedIndex]?.dataset.price || "0" + ); + const quantity = parseInt(quantityInput.value || "0", 10); + if (!isNaN(price) && !isNaN(quantity)) { + subtotal += price * quantity; + } + }); + + currentSubtotal = subtotal; + document.getElementById("subtotal-amount").textContent = + subtotal.toFixed(2); + + // Parse discount percent as float from string + const discountAmount = currentDiscount + ? (subtotal * parseFloat(currentDiscount.percent)) / 100 + : 0; + + document.getElementById("discount-amount").textContent = + discountAmount.toFixed(2); + + const finalTotal = subtotal - discountAmount; + document.getElementById("total-amount").textContent = finalTotal.toFixed(2); + } + + async function applyDiscount() { + const code = document.getElementById("discount_code").value; + if (!code) { + alert("Please enter a discount code"); + return; + } + + try { + const response = await fetch( + `/api/discounts/validate?code=${encodeURIComponent(code)}` + ); + const data = await response.json(); + + if (data.success) { + currentDiscount = { + id: data.discount.id, + percent: data.discount.discount_percent, + }; + + document.getElementById("discount-info").classList.remove("d-none"); + document.getElementById( + "discount-message" + ).textContent = `${data.discount.description} (${data.discount.discount_percent}% off)`; + document.getElementById("applied_discount_id").value = data.discount.id; + document.getElementById("discount_code").disabled = true; + + calculateTotal(); + } else { + alert(data.message || "Invalid discount code"); + } + } catch (error) { + console.error("Error applying discount:", error); + alert("Error applying discount code"); + } + } + + function removeDiscount() { + currentDiscount = null; + document.getElementById("discount-info").classList.add("d-none"); + document.getElementById("discount_code").value = ""; + document.getElementById("discount_code").disabled = false; + document.getElementById("applied_discount_id").value = ""; + calculateTotal(); + } + + function createNewItem() { + const template = document.querySelector(".order-item").cloneNode(true); + template.querySelector(".item-select").value = ""; + template.querySelector(".quantity-input").value = "1"; + + // Add event listeners to new elements + template + .querySelector(".item-select") + .addEventListener("change", calculateTotal); + template + .querySelector(".quantity-input") + .addEventListener("input", calculateTotal); + template + .querySelector(".remove-item") + .addEventListener("click", function (e) { + if (document.querySelectorAll(".order-item").length > 1) { + this.closest(".order-item").remove(); + calculateTotal(); + } else { + alert("You must have at least one item in the order."); + } + }); + + document.getElementById("order-items").appendChild(template); + } + + // Add event listener to "Add Item" button + document.getElementById("add-item").addEventListener("click", createNewItem); + + // Add event listeners to initial item + document + .querySelector(".item-select") + .addEventListener("change", calculateTotal); + document + .querySelector(".quantity-input") + .addEventListener("input", calculateTotal); + document + .querySelector(".remove-item") + .addEventListener("click", function (e) { + if (document.querySelectorAll(".order-item").length > 1) { + this.closest(".order-item").remove(); + calculateTotal(); + } else { + alert("You must have at least one item in the order."); + } + }); + + // Form validation + document.getElementById("orderForm").addEventListener("submit", function (e) { + // Prevent default form submission first for validation + e.preventDefault(); + + console.log("Form submission validation started"); + + // Check for restaurant selection + const restaurantId = document.getElementById("restaurant_id").value; + if (!restaurantId) { + alert("Please select a restaurant."); + return; + } + console.log("Restaurant selected:", restaurantId); + + // Check for user selection + const userId = document.getElementById("user_id").value; + if (!userId) { + alert("Please select a customer."); + return; + } + console.log("User selected:", userId); + + // Check for items selection + const items = document.querySelectorAll(".item-select"); + let hasItems = false; + let itemData = []; + + items.forEach((item, index) => { + if (item.value) { + hasItems = true; + const qty = item + .closest(".order-item") + .querySelector(".quantity-input").value; + itemData.push({ + menu_id: item.value, + quantity: qty, + }); + console.log(`Item ${index + 1}: ID=${item.value}, Qty=${qty}`); + } + }); + + if (!hasItems) { + alert("Please add at least one item to the order."); + return; + } + + console.log("Form data validation successful, submitting form..."); + + // If all validation passes, submit the form + this.submit(); + }); + + // Initial calculation + calculateTotal(); +</script> +{% endblock %} diff --git a/app/templates/orders/order_detail.html b/app/templates/orders/order_detail.html index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..46627323c471376f86bba0d1d8f6008297b6c5b6 100644 --- a/app/templates/orders/order_detail.html +++ b/app/templates/orders/order_detail.html @@ -0,0 +1,345 @@ +<!-- templates/orders/order_detail.html --> +{% extends 'base.html' %} + +{% block title %}HRMS - Order #{{ order.id }}{% endblock %} + +{% block styles %} +<style> + .order-status { + padding: 0.5em 1em; + border-radius: 20px; + font-size: 0.85em; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .order-status.pending { background-color: #fff3cd; color: #856404; } + .order-status.preparing { background-color: #cff4fc; color: #055160; } + .order-status.ready { background-color: #d4f5fe; color: #005b84; } + .order-status.completed { background-color: #d1e7dd; color: #0f5132; } + .order-status.cancelled { background-color: #f8d7da; color: #842029; } + + .order-summary { + background-color: #f8f9fa; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + } + + .order-items { + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + } + + .order-items th { + background-color: #f8f9fa; + font-weight: 600; + text-transform: uppercase; + font-size: 0.85em; + letter-spacing: 0.5px; + } + + .order-total { + font-size: 1.5rem; + font-weight: 600; + color: #2c3e50; + } + + .detail-label { + color: #6c757d; + font-size: 0.875rem; + margin-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 500; + } + + .detail-value { + font-weight: 500; + margin-bottom: 1.25rem; + color: #2c3e50; + } + + .restaurant-info { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 2rem; + border-left: 4px solid #00a389; + } + + .restaurant-name { + font-size: 1.25rem; + font-weight: 600; + color: #2c3e50; + margin-bottom: 0.5rem; + } + + .restaurant-location { + color: #6c757d; + font-size: 0.9rem; + } + + .restaurant-location i { + color: #00a389; + margin-right: 0.5rem; + } + + .customer-info { + background: white; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + margin-bottom: 2rem; + } + + .table-hover tbody tr:hover { + background-color: #f8f9fa; + transition: background-color 0.2s ease; + } + + .subtotal-row { + background-color: #f8f9fa; + font-weight: 500; + } +</style> +{% endblock %} + +{% block page_title %} +<nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"><a href="{{ url_for('order.order_history') }}">Orders</a></li> + <li class="breadcrumb-item active">Order #{{ order.id }}</li> + </ol> +</nav> +{% endblock %} + +{% block content %} +<div class="container-fluid"> + <div class="row"> + <!-- Order Details --> + <div class="col-md-8 mb-4"> + <div class="card h-100"> + <div class="card-body"> + <div class="d-flex justify-content-between align-items-center mb-4"> + <div> + <h5 class="card-title mb-1">Order Details</h5> + <p class="text-muted mb-0">Created on {{ order.created_at.strftime('%B %d, %Y at %I:%M %p') }}</p> + </div> + <div class="d-flex align-items-center"> + {% if is_admin and order.order_status not in ['completed', 'cancelled'] %} + <div class="dropdown me-2"> + <button class="btn btn-outline-secondary dropdown-toggle" type="button" id="statusDropdown" data-bs-toggle="dropdown" aria-expanded="false"> + Update Status + </button> + <ul class="dropdown-menu" aria-labelledby="statusDropdown"> + {% if order.order_status == 'pending' %} + <li><a class="dropdown-item" href="javascript:void(0)" onclick="updateOrderStatus('preparing')">Preparing</a></li> + <li><a class="dropdown-item" href="javascript:void(0)" onclick="updateOrderStatus('cancelled')">Cancel Order</a></li> + {% elif order.order_status == 'preparing' %} + <li><a class="dropdown-item" href="javascript:void(0)" onclick="updateOrderStatus('ready')">Ready</a></li> + {% elif order.order_status == 'ready' %} + <li><a class="dropdown-item" href="javascript:void(0)" onclick="updateOrderStatus('completed')">Complete</a></li> + {% endif %} + </ul> + </div> + {% endif %} + <span class="order-status {{ order.order_status }}"> + {{ order.order_status|title }} + </span> + </div> + </div> + + <!-- Restaurant Information --> + <div class="restaurant-info"> + <div class="restaurant-name">{{ restaurant.name }}</div> + <div class="restaurant-location"> + <i class="bi bi-geo-alt-fill"></i> + {{ restaurant.address }}, {{ restaurant.city }} + </div> + <div class="restaurant-location"> + <i class="bi bi-telephone-fill"></i> + {{ restaurant.phone }} + </div> + </div> + + <!-- Customer Information --> + <div class="customer-info"> + <div class="row"> + <div class="col-md-6"> + <p class="detail-label">Customer Name</p> + <p class="detail-value">{{ user.full_name }}</p> + + <p class="detail-label">Contact Number</p> + <p class="detail-value">{{ user.phone if user.phone else 'N/A' }}</p> + </div> + <div class="col-md-6"> + <p class="detail-label">Email</p> + <p class="detail-value">{{ user.email }}</p> + </div> + </div> + </div> + + <!-- Order Items Table --> + <h6 class="mb-3">Order Items</h6> + <div class="table-responsive order-items"> + <table class="table table-hover mb-0"> + <thead> + <tr> + <th>Item</th> + <th class="text-center">Quantity</th> + <th class="text-end">Price</th> + <th class="text-end">Subtotal</th> + </tr> + </thead> + <tbody> + {% for item in items %} + <tr> + <td>{{ item.name }}</td> + <td class="text-center">{{ item.quantity }}</td> + <td class="text-end">${{ "%.2f"|format(item.price|float) }}</td> + <td class="text-end">${{ "%.2f"|format(item.subtotal|float) }}</td> + </tr> + {% endfor %} + <tr class="subtotal-row"> + <td colspan="2">Total Items: {{ total_items }}</td> + <td class="text-end">Subtotal:</td> + <td class="text-end">${{ "%.2f"|format(items[0].price|float * items[0].quantity|float) }}</td> + </tr> + {% if order.discount_amount %} + <tr> + <td colspan="2"></td> + <td class="text-end">Discount:</td> + <td class="text-end text-danger">-${{ "%.2f"|format(order.discount_amount|float) }}</td> + </tr> + {% endif %} + <tr> + <td colspan="2"></td> + <td class="text-end">Service Fee (5%):</td> + <td class="text-end">${{ "%.2f"|format(order.service_fee|float) }}</td> + </tr> + <tr> + <td colspan="2"></td> + <td class="text-end">VAT (10%):</td> + <td class="text-end">${{ "%.2f"|format(order.vat|float) }}</td> + </tr> + <tr class="fw-bold bg-light"> + <td colspan="2"></td> + <td class="text-end">Grand Total:</td> + <td class="text-end">${{ "%.2f"|format(order.grand_total|float) }}</td> + </tr> + </tbody> + </table> + </div> + </div> + </div> + </div> + + <!-- Order Summary --> + <div class="col-md-4 mb-4"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title mb-4">Order Summary</h5> + + <div class="order-summary"> + <div class="d-flex justify-content-between mb-3"> + <span class="detail-label">Subtotal</span> + <span class="fw-bold">${{ "%.2f"|format(items[0].price|float * items[0].quantity|float) }}</span> + </div> + {% if order.discount_amount %} + <div class="d-flex justify-content-between mb-3"> + <span class="detail-label">Discount</span> + <span class="text-danger fw-bold">-${{ "%.2f"|format(order.discount_amount|float) }}</span> + </div> + {% endif %} + <div class="d-flex justify-content-between mb-3"> + <span class="detail-label">Service Fee (5%)</span> + <span class="fw-bold">${{ "%.2f"|format(order.service_fee|float) }}</span> + </div> + <div class="d-flex justify-content-between mb-3"> + <span class="detail-label">VAT (10%)</span> + <span class="fw-bold">${{ "%.2f"|format(order.vat|float) }}</span> + </div> + <div class="d-flex justify-content-between mb-3"> + <span class="detail-label">Items</span> + <span class="fw-bold">{{ total_items }}</span> + </div> + <hr> + <div class="d-flex justify-content-between order-total"> + <span>Grand Total</span> + <span>${{ "%.2f"|format(order.grand_total|float) }}</span> + </div> + </div> + + {% if order.order_status == 'pending' %} + <div class="mt-4"> + <button type="button" class="btn btn-danger w-100" + onclick="confirmCancelOrder({{ order.id }})"> + <i class="bi bi-x-circle"></i> Cancel Order + </button> + </div> + {% endif %} + </div> + </div> + </div> + </div> +</div> +{% endblock %} + +{% block scripts %} +<script> +// Cancel order function +function confirmCancelOrder(orderId) { + if (confirm('Are you sure you want to cancel this order?')) { + fetch(`/order/${orderId}/cancel`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast('Order cancelled successfully', 'success'); + setTimeout(() => window.location.href = "{{ url_for('order.order_history') }}", 1000); + } else { + showToast(data.message || 'Error cancelling order', 'error'); + } + }) + .catch(() => { + showToast('Error cancelling order', 'error'); + }); + } +} + +// Admin functions +{% if is_admin %} +const orderId = {{ order.id }}; +function updateOrderStatus(status) { + if (confirm(`Are you sure you want to update the order status to ${status}?`)) { + fetch(`/order/${orderId}/status`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ status: status }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast('Order status updated successfully', 'success'); + setTimeout(() => window.location.reload(), 1000); + } else { + showToast(data.message || 'Error updating order status', 'error'); + } + }) + .catch(() => { + showToast('Error updating order status', 'error'); + }); + } +} +{% endif %} +</script> +{% endblock %} \ No newline at end of file diff --git a/app/templates/orders/order_history.html b/app/templates/orders/order_history.html index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8433aee8a79bde0f66ae5f439e62a48b7c69fcc1 100644 --- a/app/templates/orders/order_history.html +++ b/app/templates/orders/order_history.html @@ -0,0 +1,514 @@ +<!-- history_order.html --> +{% extends 'base.html' %} {% block title %}HRMS - My Orders{% endblock %} {% +block styles %} +<link + rel="stylesheet" + href="{{ url_for('static', filename='css/toasts.css') }}" +/> +<style> + .order-table th { + background-color: #f8f9fa; + border-bottom: 2px solid #dee2e6; + white-space: nowrap; + text-transform: uppercase; + font-size: 0.85em; + letter-spacing: 0.5px; + font-weight: 600; + } + + .order-table td { + vertical-align: middle; + } + + .status-badge { + padding: 0.5em 0.8em; + border-radius: 20px; + font-size: 0.85em; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .status-badge.pending { + background-color: #fff3cd; + color: #856404; + } + .status-badge.preparing { + background-color: #cff4fc; + color: #055160; + } + .status-badge.ready { + background-color: #d4f5fe; + color: #005b84; + } + .status-badge.completed { + background-color: #d1e7dd; + color: #0f5132; + } + .status-badge.cancelled { + background-color: #f8d7da; + color: #842029; + } + + .action-buttons .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 8px; + margin-left: 0.25rem; + } + + .nav-tabs .nav-link { + color: #6c757d; + padding: 0.75rem 1.25rem; + font-weight: 500; + border: none; + position: relative; + } + + .nav-tabs .nav-link.active { + color: #00a389; + background: none; + } + + .nav-tabs .nav-link.active::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background-color: #00a389; + } + + .search-filter-section { + background: white; + padding: 1.5rem; + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + margin-bottom: 1.5rem; + } + + .search-input { + max-width: 300px; + border-radius: 20px; + padding: 0.75rem 1rem 0.75rem 2.5rem; + border: 1px solid #dee2e6; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%236c757d' class='bi bi-search' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: 12px center; + transition: all 0.2s ease; + } + + .search-input:focus { + box-shadow: 0 0 0 0.2rem rgba(0, 163, 137, 0.25); + border-color: #00a389; + } + + .restaurant-info { + font-size: 0.9rem; + color: #6c757d; + } + + .restaurant-info i { + color: #00a389; + margin-right: 0.25rem; + } + + .card { + border-radius: 12px; + border: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + } + + .pagination .page-link { + border-radius: 8px; + margin: 0 2px; + color: #00a389; + } + + .pagination .page-item.active .page-link { + background-color: #00a389; + border-color: #00a389; + } + + .modal-content { + border-radius: 12px; + border: none; + } + + .modal-header { + border-bottom: 1px solid #e9ecef; + background-color: #f8f9fa; + border-radius: 12px 12px 0 0; + } + + .modal-footer { + border-top: 1px solid #e9ecef; + background-color: #f8f9fa; + border-radius: 0 0 12px 12px; + } +</style> +{% endblock %} {% block content %} +<div class="container-fluid"> + <!-- Search and Filter Section --> + <div class="search-filter-section"> + <div class="d-flex justify-content-between align-items-center"> + <input + type="text" + class="form-control search-input" + placeholder="Search orders..." + /> + <div> + <a href="{{ url_for('order.create_order') }}" class="btn btn-primary"> + <i class="bi bi-plus-lg"></i> New Order + </a> + <button + class="btn btn-outline-secondary ms-2" + data-bs-toggle="modal" + data-bs-target="#filterModal" + > + <i class="bi bi-funnel"></i> Filter + </button> + </div> + </div> + </div> + + <!-- Order Status Tabs --> + <ul class="nav nav-tabs border-0 mb-4"> + <li class="nav-item"> + <a + class="nav-link {% if status == 'all' %}active{% endif %}" + href="{{ url_for('order.order_history') }}" + > + All Orders + </a> + </li> + <li class="nav-item"> + <a + class="nav-link {% if status == 'pending' %}active{% endif %}" + href="{{ url_for('order.order_history', status='pending') }}" + > + Pending + </a> + </li> + <li class="nav-item"> + <a + class="nav-link {% if status == 'preparing' %}active{% endif %}" + href="{{ url_for('order.order_history', status='preparing') }}" + > + Preparing + </a> + </li> + <li class="nav-item"> + <a + class="nav-link {% if status == 'ready' %}active{% endif %}" + href="{{ url_for('order.order_history', status='ready') }}" + > + Ready + </a> + </li> + <li class="nav-item"> + <a + class="nav-link {% if status == 'completed' %}active{% endif %}" + href="{{ url_for('order.order_history', status='completed') }}" + > + Completed + </a> + </li> + <li class="nav-item"> + <a + class="nav-link {% if status == 'cancelled' %}active{% endif %}" + href="{{ url_for('order.order_history', status='cancelled') }}" + > + Cancelled + </a> + </li> + </ul> + + <!-- Orders Table --> + <div class="card"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover mb-0 order-table"> + <thead> + <tr> + <th>Order ID</th> + <th>Customer</th> + <th>Restaurant</th> + <th>Date & Time</th> + <th>Items</th> + <th>Total</th> + <th>Status</th> + <th class="text-end">Actions</th> + </tr> + </thead> + <tbody> + {% for order in orders %} + <tr> + <td class="fw-medium">#{{ order.order.id }}</td> + <td>{{ order.user.full_name }}</td> + <td> + <div>{{ order.restaurant.name }}</div> + <div class="restaurant-info"> + <i class="bi bi-geo-alt-fill"></i>{{ order.restaurant.city }} + </div> + </td> + <td>{{ order.order.created_at.strftime('%b %d, %Y %H:%M') }}</td> + <td> + {% for item in order.menu_items[:2] %} {{ item.name }} (×{{ + item.quantity }}){% if not loop.last %}, {% endif %} {% endfor + %} {% if order.menu_items|length > 2 %} + <span class="text-muted" + >and {{ order.menu_items|length - 2 }} more...</span + > + {% endif %} + </td> + <td class="fw-medium"> + ${{ "%.2f"|format(order.order.grand_total) }} + </td> + <td> + <span class="status-badge {{ order.order.order_status }}"> + {{ order.order.order_status|title }} + </span> + </td> + <td class="text-end action-buttons"> + <a + href="{{ url_for('order.order_detail', order_id=order.order.id) }}" + class="btn btn-light" + title="View Details" + > + <i class="bi bi-eye"></i> + </a> + {% if order.order.order_status == 'pending' %} + <button + type="button" + class="btn btn-light text-success" + onclick="updateOrderStatus({{ order.order.id }}, 'preparing')" + title="Start Preparing" + > + <i class="bi bi-check-circle"></i> + </button> + <button + type="button" + class="btn btn-light text-danger" + onclick="confirmCancelOrder({{ order.order.id }})" + title="Cancel Order" + > + <i class="bi bi-x-circle"></i> + </button> + {% elif order.order.order_status == 'preparing' %} + <button + type="button" + class="btn btn-light text-info" + onclick="updateOrderStatus({{ order.order.id }}, 'ready')" + title="Mark as Ready" + > + <i class="bi bi-check2-circle"></i> + </button> + {% elif order.order.order_status == 'ready' %} + <button + type="button" + class="btn btn-light text-success" + onclick="updateOrderStatus({{ order.order.id }}, 'completed')" + title="Mark as Completed" + > + <i class="bi bi-check2-all"></i> + </button> + {% endif %} + </td> + </tr> + {% else %} + <tr> + <td colspan="8" class="text-center py-4"> + <div class="text-muted"> + <i class="bi bi-inbox fs-4 d-block mb-2"></i> + No orders found + </div> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> + + <!-- Pagination --> + {% if pagination.pages > 1 %} + <div class="d-flex justify-content-between align-items-center mt-4"> + <span class="text-muted"> + Showing {{ pagination.items|length }} of {{ pagination.total }} orders + </span> + <nav> + <ul class="pagination mb-0"> + <li + class="page-item {% if not pagination.has_prev %}disabled{% endif %}" + > + <a + class="page-link" + href="{{ url_for('order.order_history', page=pagination.prev_num, status=status) }}" + > + <i class="bi bi-chevron-left"></i> + </a> + </li> + + {% for page in pagination.iter_pages() %} {% if page %} + <li class="page-item {% if page == pagination.page %}active{% endif %}"> + <a + class="page-link" + href="{{ url_for('order.order_history', page=page, status=status) }}" + > + {{ page }} + </a> + </li> + {% else %} + <li class="page-item disabled"> + <span class="page-link">...</span> + </li> + {% endif %} {% endfor %} + + <li + class="page-item {% if not pagination.has_next %}disabled{% endif %}" + > + <a + class="page-link" + href="{{ url_for('order.order_history', page=pagination.next_num, status=status) }}" + > + <i class="bi bi-chevron-right"></i> + </a> + </li> + </ul> + </nav> + </div> + {% endif %} +</div> + +<!-- Filter Modal --> +<div class="modal fade" id="filterModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Filter Orders</h5> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + ></button> + </div> + <div class="modal-body"> + <form id="filterForm"> + <div class="mb-3"> + <label class="form-label">Date Range</label> + <div class="input-group"> + <input type="date" class="form-control" name="start_date" /> + <span class="input-group-text">to</span> + <input type="date" class="form-control" name="end_date" /> + </div> + </div> + <div class="mb-3"> + <label class="form-label">Price Range</label> + <div class="input-group"> + <span class="input-group-text">$</span> + <input + type="number" + class="form-control" + name="min_price" + placeholder="Min" + /> + <span class="input-group-text">to</span> + <input + type="number" + class="form-control" + name="max_price" + placeholder="Max" + /> + </div> + </div> + <div class="mb-3"> + <label class="form-label">Payment Method</label> + <select class="form-select" name="payment_method"> + <option value="">All</option> + <option value="cash">Cash</option> + <option value="card">Card</option> + <option value="online">Online</option> + </select> + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> + Cancel + </button> + <button type="button" class="btn btn-primary" onclick="applyFilters()"> + Apply Filters + </button> + </div> + </div> + </div> +</div> + +{% endblock %} {% block scripts %} +<script> + function confirmCancelOrder(orderId) { + if (confirm("Are you sure you want to cancel this order?")) { + fetch(`/order/${orderId}/cancel`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showToast("Order cancelled successfully", "success"); + setTimeout(() => window.location.reload(), 1000); + } else { + showToast(data.message || "Error cancelling order", "error"); + } + }) + .catch(() => { + showToast("Error cancelling order", "error"); + }); + } + } + + function updateOrderStatus(orderId, status) { + if (confirm(`Are you sure you want to mark this order as ${status}?`)) { + fetch(`/order/${orderId}/status`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ status: status }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showToast("Order status updated successfully", "success"); + setTimeout(() => window.location.reload(), 1000); + } else { + showToast(data.message || "Error updating order status", "error"); + } + }) + .catch(() => { + showToast("Error updating order status", "error"); + }); + } + } + + function applyFilters() { + const formData = new FormData(document.getElementById("filterForm")); + const params = new URLSearchParams(); + + for (let [key, value] of formData.entries()) { + if (value) params.append(key, value); + } + + if (params.toString()) { + window.location.href = `${window.location.pathname}?${params.toString()}`; + } + + bootstrap.Modal.getInstance(document.getElementById("filterModal")).hide(); + } +</script> +{% endblock %} diff --git a/app/templates/payments/payment.html b/app/templates/payments/payment.html index 71b1c2ad8bfe2c14c53ca48fd776de2a074d339a..2ba0adc758cb03c4c2bcdc7cf947181de54ec627 100644 --- a/app/templates/payments/payment.html +++ b/app/templates/payments/payment.html @@ -1,39 +1,165 @@ {% extends 'base.html' %} +{% block content %} +<div class="container-fluid mt-4"> + <!-- Header Section --> + <div class="d-flex justify-content-between align-items-center mb-4"> + <div> + <h1 class="h3 mb-0">Revenue Management</h1> + <p class="text-muted">Track and manage restaurant revenues</p> + </div> + <div class="d-flex gap-2"> + <button class="btn btn-outline-primary" onclick="exportToExcel()"> + <i class="bi bi-file-earmark-excel"></i> Export to Excel + </button> + <button class="btn btn-primary" onclick="printReport()"> + <i class="bi bi-printer"></i> Print Report + </button> + </div> + </div> -{% block title %}Payment Page{% endblock %} + <!-- Overall Stats Cards --> + <div class="row g-3 mb-4"> + <div class="col-md-3"> + <div class="card bg-primary text-white h-100"> + <div class="card-body"> + <h6 class="card-title">Total Revenue</h6> + <h2 class="mb-0">{{ stats.total_paid }}</h2> + <small class="text-white-50">All restaurants combined</small> + </div> + </div> + </div> + <div class="col-md-3"> + <div class="card bg-success text-white h-100"> + <div class="card-body"> + <h6 class="card-title">Net Vendor Revenue</h6> + <h2 class="mb-0">{{ stats.vendor_get }}</h2> + <small class="text-white-50">After service fee</small> + </div> + </div> + </div> + <div class="col-md-3"> + <div class="card bg-warning h-100"> + <div class="card-body"> + <h6 class="card-title">Pending Payments</h6> + <h2 class="mb-0">{{ stats.total_unpaid }}</h2> + <small class="text-muted">Across all restaurants</small> + </div> + </div> + </div> + <div class="col-md-3"> + <div class="card bg-info text-white h-100"> + <div class="card-body"> + <h6 class="card-title">Total VAT</h6> + <h2 class="mb-0">{{ stats.vat }}</h2> + <small class="text-white-50">10% of (Revenue + Service Fee)</small> + </div> + </div> + </div> + </div> -{% block content %} -<div class="payment-container" style="background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); width: 100%; max-width: 400px; margin: 0 auto;"> - - <form id="card" class="payment-form"> - <h3 style="margin-bottom: 20px; text-align: center;">Card Payment</h3> - - <label for="name" style="display: block; margin-bottom: 8px; font-weight: bold;">Name on Card</label> - <input type="text" id="name" name="name" required aria-label="Name on Card" style="width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;"> - - <label for="email" style="display: block; margin-bottom: 8px; font-weight: bold;">Email</label> - <input type="email" id="email" name="email" required aria-label="Email" style="width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;"> - - <label for="card-number" style="display: block; margin-bottom: 8px; font-weight: bold;">Card Number</label> - <input type="number" id="card-number" name="card-number" required aria-label="Card Number" pattern="\d{16}" style="width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;"> - - <label for="expiry-date" style="display: block; margin-bottom: 8px; font-weight: bold;">Expiry Date</label> - <input type="text" id="expiry-date" name="expiry-date" placeholder="MM/YY" required aria-label="Expiry Date" pattern="\d{2}/\d{2}" style="width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;"> - - <label for="cvv" style="display: block; margin-bottom: 8px; font-weight: bold;">CVV</label> - <input type="password" id="cvv" name="cvv" required aria-label="CVV" pattern="\d{3}" style="width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;"> - - <input type="submit" value="Submit Payment" style="width: 100%; padding: 10px; margin: 10px 0; background-color: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; box-sizing: border-box;"> - </form> - + <!-- Restaurant Selection --> + <div class="card mb-4"> + <div class="card-body"> + <div class="row"> + <div class="col-md-4"> + <label class="form-label">Select Restaurant</label> + <select class="form-select" onchange="filterByRestaurant(this.value)"> + <option value="">All Restaurants</option> + {% for restaurant in restaurants %} + <option value="{{ restaurant.id }}" {% if selected_restaurant_id == restaurant.id %}selected{% endif %}> + {{ restaurant.name }} - {{ restaurant.city }} + </option> + {% endfor %} + </select> + </div> + </div> + </div> + </div> + + <!-- Restaurant Revenue Cards --> + <div class="row g-3"> + {% for stat in restaurant_stats %} + <div class="col-md-6 col-lg-4"> + <div class="card h-100"> + <div class="card-header bg-light"> + <h5 class="card-title mb-0">{{ stat.name }}</h5> + <small class="text-muted">{{ stat.city }}</small> + </div> + <div class="card-body"> + <div class="d-flex justify-content-between mb-3"> + <div> + <h6 class="mb-1 text-muted">Total Revenue</h6> + <h4 class="mb-0">${{ "%.2f"|format(stat.total_paid) }}</h4> + </div> + <div class="text-end"> + <h6 class="mb-1 text-muted">Pending</h6> + <h4 class="mb-0 text-warning">${{ "%.2f"|format(stat.total_unpaid) }}</h4> + </div> + </div> + <hr> + <div class="row g-2"> + <div class="col-6"> + <small class="text-muted d-block">Service Fee (5%)</small> + <span>${{ "%.2f"|format(stat.service_fee) }}</span> + </div> + <div class="col-6"> + <small class="text-muted d-block">VAT (10%)</small> + <span>${{ "%.2f"|format(stat.vat) }}</span> + </div> + <div class="col-12"> + <small class="text-muted d-block">Net Revenue</small> + <span class="text-success">${{ "%.2f"|format(stat.vendor_get) }}</span> + </div> + </div> + </div> + </div> + </div> + {% endfor %} + </div> </div> +<style> +.card { + border: none; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + transition: transform 0.2s ease-in-out; +} + +.card:hover { + transform: translateY(-2px); +} + +.badge { + padding: 0.5em 0.75em; + font-weight: 500; +} + +.table th { + font-weight: 600; + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.5px; +} +</style> + <script> - function showForm(method) { - document.querySelectorAll('.payment-form').forEach(form => form.classList.remove('active')); - document.getElementById(method).classList.add('active'); - document.querySelectorAll('.payment-methods button').forEach(button => button.classList.remove('active')); - document.getElementById(method + '-btn').classList.add('active'); +function filterByRestaurant(restaurantId) { + const url = new URL(window.location); + if (restaurantId) { + url.searchParams.set('restaurant_id', restaurantId); + } else { + url.searchParams.delete('restaurant_id'); } + window.location = url; +} + +function exportToExcel() { + // Implement Excel export functionality + alert('Export to Excel functionality will be implemented here'); +} + +function printReport() { + window.print(); +} </script> {% endblock %} diff --git a/app/templates/reporting/restaurant_performance.html b/app/templates/reporting/restaurant_performance.html deleted file mode 100644 index 1493b2630c85545000b9451762eae6d0f6cff05f..0000000000000000000000000000000000000000 --- a/app/templates/reporting/restaurant_performance.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends 'base.html' %} - -{% block content %} -<div class="container"> - <h1 class="mt-4">Restaurant Performance</h1> - - <table class="table table-bordered"> - <thead class="table-dark"> - <tr> - <th>Month</th> - <th>Total Sales</th> - <th>Average Rating</th> - <th>Total Customers</th> - </tr> - </thead> - <tbody> - {% for data in performance_data %} - <tr> - <td>{{ data['month'] }}</td> - <td>{{ data['total_sales'] }}</td> - <td>{{ data['average_rating'] }}</td> - <td>{{ data['total_customers'] }}</td> - </tr> - {% endfor %} - </tbody> - </table> -</div> -{% endblock %} diff --git a/app/templates/reporting/staff_performance.html b/app/templates/reporting/staff_performance.html deleted file mode 100644 index 92fe2022108f58dd308a283a36a6176b615e01cc..0000000000000000000000000000000000000000 --- a/app/templates/reporting/staff_performance.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends 'base.html' %} - -{% block content %} -<div class="container"> - <h1 class="mt-4">Staff Performance</h1> - - <table class="table table-bordered"> - <thead class="table-dark"> - <tr> - <th>Name</th> - <th>Role</th> - <th>Performance Rating</th> - </tr> - </thead> - <tbody> - {% for staff_member in staff %} - <tr> - <td>{{ staff_member['name'] }}</td> - <td>{{ staff_member['role'] }}</td> - <td>{{ staff_member['performance_rating'] }}</td> - </tr> - {% endfor %} - </tbody> - </table> -</div> -{% endblock %} diff --git a/app/templates/reservations/create_reservation.html b/app/templates/reservations/create_reservation.html new file mode 100644 index 0000000000000000000000000000000000000000..71e37bb3b4638288980dac5feee2787a636a552b --- /dev/null +++ b/app/templates/reservations/create_reservation.html @@ -0,0 +1,296 @@ +{% extends 'base.html' %} + +{% block title %}HRMS - New Reservation{% endblock %} + +{% block styles %} +<style> + .customer-search-results { + position: absolute; + width: 100%; + max-height: 200px; + overflow-y: auto; + background: white; + border: 1px solid #ddd; + border-radius: 8px; + z-index: 1000; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + .customer-search-item { + padding: 12px; + cursor: pointer; + border-bottom: 1px solid #eee; + } + .customer-search-item:hover { + background-color: #f8f9fa; + } + .customer-search-item:last-child { + border-bottom: none; + } + .selected-customer-info { + display: none; + padding: 1rem; + background-color: #f8f9fa; + border-radius: 8px; + margin-top: 1rem; + border: 1px solid #e9ecef; + } + .form-label { + font-weight: 500; + color: #495057; + margin-bottom: 0.5rem; + } + .form-control, .form-select { + padding: 0.75rem 1rem; + border-radius: 8px; + border: 1px solid #dee2e6; + transition: all 0.2s ease; + } + .form-control:focus, .form-select:focus { + border-color: #00a389; + box-shadow: 0 0 0 0.2rem rgba(0, 163, 137, 0.25); + } + .btn { + padding: 0.75rem 1.5rem; + border-radius: 8px; + font-weight: 500; + } + .btn-primary { + background-color: #00a389; + border-color: #00a389; + } + .btn-primary:hover { + background-color: #008c75; + border-color: #008c75; + } + .card { + border: none; + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + } +</style> +{% endblock %} + +{% block page_title %} +<nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"><a href="{{ url_for('reservations.list_reservations') }}">Reservations</a></li> + <li class="breadcrumb-item active">New Reservation</li> + </ol> +</nav> +{% endblock %} + +{% block content %} +<div class="container-fluid"> + <div class="row justify-content-center"> + <div class="col-lg-8"> + <div class="card"> + <div class="card-body p-4"> + <h5 class="card-title mb-4">Create New Reservation</h5> + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + <div class="alert alert-{{ category }} alert-dismissible fade show"> + {{ message }} + <button type="button" class="btn-close" data-bs-dismiss="alert"></button> + </div> + {% endfor %} + {% endif %} + {% endwith %} + + <form id="reservationForm" method="POST"> + <!-- Customer Search Section --> + <div class="mb-4"> + <label for="customerSearch" class="form-label">Search Customer by Phone</label> + <div class="position-relative"> + <input type="text" + id="customerSearch" + class="form-control form-control-lg" + placeholder="Enter customer phone number" + autocomplete="off" + required> + <input type="hidden" id="selectedUserId" name="user_id" required> + <div id="customerSearchResults" class="customer-search-results"></div> + </div> + <div id="selectedCustomerInfo" class="selected-customer-info"> + <div class="row"> + <div class="col-md-4"> + <p class="mb-1 text-muted">Name</p> + <p class="mb-3 fw-bold" id="customerName"></p> + </div> + <div class="col-md-4"> + <p class="mb-1 text-muted">Phone</p> + <p class="mb-3 fw-bold" id="customerPhone"></p> + </div> + <div class="col-md-4"> + <p class="mb-1 text-muted">Email</p> + <p class="mb-3 fw-bold" id="customerEmail"></p> + </div> + </div> + </div> + </div> + + <!-- Restaurant Selection --> + <div class="mb-4"> + <label class="form-label">Restaurant</label> + <select name="restaurant_id" class="form-select form-select-lg" required> + <option value="">Select Restaurant</option> + {% for restaurant in restaurants %} + <option value="{{ restaurant.id }}">{{ restaurant.name }} - {{ restaurant.city }}</option> + {% endfor %} + </select> + </div> + + <!-- Date and Time --> + <div class="row mb-4"> + <div class="col-md-6"> + <label class="form-label">Date</label> + <input type="date" + name="reservation_date" + class="form-control" + required + min="{{ today }}"> + </div> + <div class="col-md-6"> + <label class="form-label">Time</label> + <input type="time" + name="reservation_time" + class="form-control" + required> + </div> + </div> + + <!-- Guest Count and Table Number --> + <div class="row mb-4"> + <div class="col-md-6"> + <label class="form-label">Number of Guests</label> + <input type="number" + name="guest_count" + class="form-control" + required + min="1" + max="10"> + <div class="form-text">Maximum 10 guests per table</div> + </div> + <div class="col-md-6"> + <label class="form-label">Table Number (Optional)</label> + <input type="text" + name="table_number" + class="form-control" + maxlength="10"> + </div> + </div> + + <!-- Notes --> + <div class="mb-4"> + <label class="form-label">Additional Notes</label> + <textarea name="notes" + class="form-control" + rows="3" + placeholder="Any special requests or notes"></textarea> + </div> + + <!-- Submit Button --> + <div class="d-flex justify-content-end gap-2"> + <a href="{{ url_for('reservations.list_reservations') }}" + class="btn btn-outline-secondary">Cancel</a> + <button type="submit" class="btn btn-primary"> + Create Reservation + </button> + </div> + </form> + </div> + </div> + </div> + </div> +</div> +{% endblock %} + +{% block scripts %} +<script> +let debounceTimeout; + +function debounce(func, wait) { + return function executedFunction(...args) { + const later = () => { + clearTimeout(debounceTimeout); + func(...args); + }; + clearTimeout(debounceTimeout); + debounceTimeout = setTimeout(later, wait); + }; +} + +async function searchUsers(query) { + try { + const response = await fetch(`/api/users/search?phone=${encodeURIComponent(query)}`); + const data = await response.json(); + + const resultsDiv = document.getElementById('customerSearchResults'); + resultsDiv.innerHTML = ''; + resultsDiv.style.display = 'block'; + + if (data.users && data.users.length > 0) { + data.users.forEach(user => { + const div = document.createElement('div'); + div.className = 'customer-search-item'; + div.innerHTML = ` + <div class="d-flex justify-content-between"> + <div> + <div class="fw-bold">${user.full_name}</div> + <div class="text-muted">${user.phone}</div> + </div> + <div class="text-muted small">${user.email}</div> + </div> + `; + div.addEventListener('click', () => selectUser(user)); + resultsDiv.appendChild(div); + }); + } else { + resultsDiv.innerHTML = '<div class="customer-search-item text-muted">No users found</div>'; + } + } catch (error) { + console.error('Error searching users:', error); + } +} + +function selectUser(user) { + document.getElementById('selectedUserId').value = user.id; + document.getElementById('customerSearch').value = user.phone; + document.getElementById('customerName').textContent = user.full_name; + document.getElementById('customerPhone').textContent = user.phone; + document.getElementById('customerEmail').textContent = user.email; + document.getElementById('selectedCustomerInfo').style.display = 'block'; + document.getElementById('customerSearchResults').style.display = 'none'; +} + +document.addEventListener('DOMContentLoaded', function() { + const searchInput = document.getElementById('customerSearch'); + const debouncedSearch = debounce((query) => searchUsers(query), 300); + + searchInput.addEventListener('input', (e) => { + const query = e.target.value.trim(); + if (query.length >= 3) { + debouncedSearch(query); + } else { + document.getElementById('customerSearchResults').style.display = 'none'; + } + }); + + // Close search results when clicking outside + document.addEventListener('click', (e) => { + if (!e.target.closest('#customerSearch') && !e.target.closest('#customerSearchResults')) { + document.getElementById('customerSearchResults').style.display = 'none'; + } + }); + + // Form validation + document.getElementById('reservationForm').addEventListener('submit', function(e) { + if (!document.getElementById('selectedUserId').value) { + e.preventDefault(); + alert('Please select a customer from the search results'); + } + }); +}); +</script> +{% endblock %} \ No newline at end of file diff --git a/app/templates/reservations/reservation_confirmation.html b/app/templates/reservations/reservation_confirmation.html deleted file mode 100644 index 05d38b0c8f5b9af4fedfd2cc99ae66d93d045ae5..0000000000000000000000000000000000000000 --- a/app/templates/reservations/reservation_confirmation.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'base.html' %} - -{% block content %} - -<div class="container"> - <h1 class="mt-4">Reservation Confirmed</h1> - <p>Thank you, {{ name }}! Your table reservation has been confirmed.</p> - <a href="/" class="btn btn-primary">Back to Home</a> -</div> - -{% endblock %} \ No newline at end of file diff --git a/app/templates/reservations/reservation_detail.html b/app/templates/reservations/reservation_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..9e96818bd9413c430c440f9cfd6d0810d00a6699 --- /dev/null +++ b/app/templates/reservations/reservation_detail.html @@ -0,0 +1,253 @@ +{% extends 'base.html' %} + +{% block title %}HRMS - Reservation #{{ reservation.id }}{% endblock %} + +{% block styles %} +<style> + .reservation-status { + padding: 0.5em 0.8em; + border-radius: 20px; + font-size: 0.85em; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .reservation-status.pending { background-color: #fff3cd; color: #856404; } + .reservation-status.confirmed { background-color: #d1e7dd; color: #0f5132; } + .reservation-status.cancelled { background-color: #f8d7da; color: #842029; } + .reservation-status.completed { background-color: #cff4fc; color: #055160; } + + .detail-label { + color: #6c757d; + font-size: 0.875rem; + margin-bottom: 0.25rem; + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 500; + } + + .detail-value { + font-weight: 500; + margin-bottom: 1.25rem; + color: #2c3e50; + } + + .restaurant-info { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 2rem; + border-left: 4px solid #00a389; + } + + .restaurant-name { + font-size: 1.25rem; + font-weight: 600; + color: #2c3e50; + margin-bottom: 0.5rem; + } + + .restaurant-location { + color: #6c757d; + font-size: 0.9rem; + } + + .restaurant-location i { + color: #00a389; + margin-right: 0.5rem; + } + + .customer-info { + background: white; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + margin-bottom: 2rem; + } + + .reservation-summary { + background-color: #f8f9fa; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + } + + .notes-section { + background: white; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + margin-bottom: 2rem; + } + + .notes-content { + color: #2c3e50; + white-space: pre-wrap; + } +</style> +{% endblock %} + +{% block page_title %} +<nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li class="breadcrumb-item"><a href="{{ url_for('reservations.list_reservations') }}">Reservations</a></li> + <li class="breadcrumb-item active">Reservation #{{ reservation.id }}</li> + </ol> +</nav> +{% endblock %} + +{% block content %} +<div class="container-fluid"> + <div class="row"> + <!-- Reservation Details --> + <div class="col-md-8 mb-4"> + <div class="card h-100"> + <div class="card-body"> + <div class="d-flex justify-content-between align-items-center mb-4"> + <div> + <h5 class="card-title mb-1">Reservation Details</h5> + <p class="text-muted mb-0">Created on {{ reservation.created_at.strftime('%B %d, %Y at %I:%M %p') }}</p> + </div> + <div class="d-flex align-items-center"> + {% if is_admin %} + <div class="dropdown me-2"> + <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown"> + Update Status + </button> + <ul class="dropdown-menu"> + <li><a class="dropdown-item" href="#" onclick="updateStatus('pending')">Pending</a></li> + <li><a class="dropdown-item" href="#" onclick="updateStatus('confirmed')">Confirmed</a></li> + <li><a class="dropdown-item" href="#" onclick="updateStatus('completed')">Completed</a></li> + <li><a class="dropdown-item" href="#" onclick="updateStatus('cancelled')">Cancelled</a></li> + </ul> + </div> + {% endif %} + <span class="reservation-status {{ reservation.status }}"> + {{ reservation.status|title }} + </span> + </div> + </div> + + <!-- Restaurant Information --> + <div class="restaurant-info"> + <div class="restaurant-name">{{ restaurant.name }}</div> + <div class="restaurant-location"> + <i class="bi bi-geo-alt-fill"></i> + {{ restaurant.address }}, {{ restaurant.city }} + </div> + <div class="restaurant-location"> + <i class="bi bi-telephone-fill"></i> + {{ restaurant.phone }} + </div> + </div> + + <!-- Customer Information --> + <div class="customer-info"> + <div class="row"> + <div class="col-md-6"> + <p class="detail-label">Customer Name</p> + <p class="detail-value">{{ user.full_name }}</p> + + <p class="detail-label">Contact Number</p> + <p class="detail-value">{{ user.phone if user.phone else 'N/A' }}</p> + </div> + <div class="col-md-6"> + <p class="detail-label">Email</p> + <p class="detail-value">{{ user.email }}</p> + + <p class="detail-label">Reservation Date & Time</p> + <p class="detail-value"> + {{ reservation.reservation_date.strftime('%B %d, %Y') }}<br> + <small class="text-muted">{{ reservation.reservation_time.strftime('%I:%M %p') }}</small> + </p> + </div> + </div> + </div> + + <!-- Notes Section --> + {% if reservation.notes %} + <div class="notes-section"> + <h6 class="mb-3">Additional Notes</h6> + <p class="notes-content mb-0">{{ reservation.notes }}</p> + </div> + {% endif %} + </div> + </div> + </div> + + <!-- Reservation Summary --> + <div class="col-md-4 mb-4"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title mb-4">Reservation Summary</h5> + + <div class="reservation-summary"> + <div class="d-flex justify-content-between mb-3"> + <span class="detail-label">Guest Count</span> + <span class="fw-bold">{{ reservation.guest_count }}</span> + </div> + <div class="d-flex justify-content-between mb-3"> + <span class="detail-label">Table Number</span> + <span class="fw-bold">{% if reservation.table_number %}#{{ reservation.table_number }}{% else %}Not Assigned{% endif %}</span> + </div> + <div class="d-flex justify-content-between mb-3"> + <span class="detail-label">Status</span> + <span class="reservation-status {{ reservation.status }}">{{ reservation.status|title }}</span> + </div> + <div class="d-flex justify-content-between mb-3"> + <span class="detail-label">Created</span> + <span class="text-muted">{{ reservation.created_at.strftime('%b %d, %Y') }}</span> + </div> + {% if reservation.updated_at != reservation.created_at %} + <div class="d-flex justify-content-between"> + <span class="detail-label">Last Updated</span> + <span class="text-muted">{{ reservation.updated_at.strftime('%b %d, %Y') }}</span> + </div> + {% endif %} + </div> + + {% if reservation.status == 'pending' %} + <div class="mt-4"> + <button type="button" class="btn btn-danger w-100" + onclick="updateStatus('cancelled')"> + <i class="bi bi-x-circle"></i> Cancel Reservation + </button> + </div> + {% endif %} + </div> + </div> + </div> + </div> +</div> +{% endblock %} + +{% block scripts %} +<script> +async function updateStatus(status) { + if (!confirm(`Are you sure you want to mark this reservation as ${status}?`)) { + return; + } + + try { + const response = await fetch(`/reservation/{{ reservation.id }}/status`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ status: status }) + }); + + const result = await response.json(); + if (result.success) { + location.reload(); + } else { + alert(result.message || 'Error updating reservation status'); + } + } catch (error) { + console.error('Error:', error); + alert('Error updating reservation status'); + } +} +</script> +{% endblock %} \ No newline at end of file diff --git a/app/templates/reservations/reservation_list.html b/app/templates/reservations/reservation_list.html index cae380719fba21125c8d96d18f5d8780029a8aff..c4cd5652ef8e0ad1eee9240e9ff521a130da68eb 100644 --- a/app/templates/reservations/reservation_list.html +++ b/app/templates/reservations/reservation_list.html @@ -1,41 +1,351 @@ -{% extends 'base.html' %} {% block content %} - -<div class="container"> - <h1 class="mt-4">Current Reservations</h1> - - <!-- Add a Button to Go to the Reservation Form --> - <div class="mb-4 text-end"> - <a - href="{{ url_for('reservations.reserve_table') }}" - class="btn btn-primary" - >Make a New Reservation</a - > - </div> - - <table class="table table-bordered"> - <thead class="table-dark"> - <tr> - <th>Name</th> - <th>Contact</th> - <th>Date</th> - <th>Time</th> - <th>Guests</th> - <th>Notes</th> - </tr> - </thead> - <tbody> - {% for reservation in reservations %} - <tr> - <td>{{ reservation['name'] }}</td> - <td>{{ reservation['contact'] }}</td> - <td>{{ reservation['date'] }}</td> - <td>{{ reservation['time'] }}</td> - <td>{{ reservation['guests'] }}</td> - <td>{{ reservation['notes'] }}</td> - </tr> - {% endfor %} - </tbody> - </table> +{% extends 'base.html' %} + +{% block title %}HRMS - Table Reservations{% endblock %} + +{% block styles %} +<style> + .reservation-table th { + background-color: #f8f9fa; + border-bottom: 2px solid #dee2e6; + white-space: nowrap; + text-transform: uppercase; + font-size: 0.85em; + letter-spacing: 0.5px; + font-weight: 600; + } + + .reservation-table td { + vertical-align: middle; + } + + .status-badge { + padding: 0.5em 0.8em; + border-radius: 20px; + font-size: 0.85em; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .status-badge.pending { background-color: #fff3cd; color: #856404; } + .status-badge.preparing { background-color: #cff4fc; color: #055160; } + .status-badge.ready { background-color: #d4f5fe; color: #005b84; } + .status-badge.completed { background-color: #d1e7dd; color: #0f5132; } + .status-badge.cancelled { background-color: #f8d7da; color: #842029; } + + .action-buttons .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 8px; + margin-left: 0.25rem; + } + + .search-filter-section { + background: white; + padding: 1.5rem; + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + margin-bottom: 1.5rem; + } + + .search-input { + max-width: 300px; + border-radius: 20px; + padding: 0.75rem 1rem 0.75rem 2.5rem; + border: 1px solid #dee2e6; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%236c757d' class='bi bi-search' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: 12px center; + transition: all 0.2s ease; + } + + .restaurant-info { + font-size: 0.9rem; + color: #6c757d; + } + + .restaurant-info i { + color: #00a389; + margin-right: 0.25rem; + } + + .card { + border-radius: 12px; + border: none; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + } + + .nav-tabs .nav-link { + color: #6c757d; + padding: 0.75rem 1.25rem; + font-weight: 500; + border: none; + position: relative; + } + + .nav-tabs .nav-link.active { + color: #00a389; + background: none; + border-bottom: 2px solid #00a389; + } +</style> +{% endblock %} + +{% block content %} +<div class="container-fluid"> + <!-- Search and Filter Section --> + <div class="search-filter-section"> + <div class="d-flex justify-content-between align-items-center"> + <div class="d-flex align-items-center gap-2"> + <input type="text" class="form-control search-input" placeholder="Search reservations..."> + <button class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#filterModal"> + <i class="bi bi-funnel"></i> Filter + </button> + </div> + <a href="{{ url_for('reservations.create_reservation') }}" class="btn btn-primary"> + <i class="bi bi-plus-lg"></i> New Reservation + </a> + </div> + </div> + + <!-- Status Tabs --> + <ul class="nav nav-tabs border-0 mb-4"> + <li class="nav-item"> + <a class="nav-link {% if status == 'all' %}active{% endif %}" + href="{{ url_for('reservations.list_reservations') }}"> + All Reservations + </a> + </li> + <li class="nav-item"> + <a class="nav-link {% if status == 'pending' %}active{% endif %}" + href="{{ url_for('reservations.list_reservations', status='pending') }}"> + Pending + </a> + </li> + <li class="nav-item"> + <a class="nav-link {% if status == 'preparing' %}active{% endif %}" + href="{{ url_for('reservations.list_reservations', status='preparing') }}"> + Preparing + </a> + </li> + <li class="nav-item"> + <a class="nav-link {% if status == 'ready' %}active{% endif %}" + href="{{ url_for('reservations.list_reservations', status='ready') }}"> + Ready + </a> + </li> + <li class="nav-item"> + <a class="nav-link {% if status == 'completed' %}active{% endif %}" + href="{{ url_for('reservations.list_reservations', status='completed') }}"> + Completed + </a> + </li> + <li class="nav-item"> + <a class="nav-link {% if status == 'cancelled' %}active{% endif %}" + href="{{ url_for('reservations.list_reservations', status='cancelled') }}"> + Cancelled + </a> + </li> + </ul> + + <!-- Reservations Table --> + <div class="card"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover mb-0 reservation-table"> + <thead> + <tr> + <th>RESERVATION ID</th> + <th>CUSTOMER</th> + <th>RESTAURANT</th> + <th>DATE & TIME</th> + <th>GUESTS</th> + <th>TABLE</th> + <th>STATUS</th> + <th class="text-end">ACTIONS</th> + </tr> + </thead> + <tbody> + {% for reservation in reservations %} + <tr> + <td class="fw-medium">#{{ reservation.reservation.id }}</td> + <td> + <div>{{ reservation.user.full_name }}</div> + <small class="text-muted">{{ reservation.user.phone }}</small> + </td> + <td> + <div>{{ reservation.restaurant.name }}</div> + <div class="restaurant-info"> + <i class="bi bi-geo-alt-fill"></i>{{ reservation.restaurant.city }} + </div> + </td> + <td> + <div>{{ reservation.reservation.reservation_date.strftime('%b %d, %Y') }}</div> + <small class="text-muted">{{ reservation.reservation.reservation_time.strftime('%I:%M %p') }}</small> + </td> + <td>{{ reservation.reservation.guest_count }}</td> + <td>{% if reservation.reservation.table_number %}#{{ reservation.reservation.table_number }}{% else %}-{% endif %}</td> + <td> + <span class="status-badge {{ reservation.reservation.status }}"> + {{ reservation.reservation.status|title }} + </span> + </td> + <td class="text-end action-buttons"> + <a href="{{ url_for('reservations.reservation_detail', reservation_id=reservation.reservation.id) }}" + class="btn btn-light" title="View Details"> + <i class="bi bi-eye"></i> + </a> + {% if reservation.reservation.status == 'pending' %} + <button type="button" class="btn btn-light text-success" + onclick="updateStatus({{ reservation.reservation.id }}, 'preparing')" title="Start Preparing"> + <i class="bi bi-check-circle"></i> + </button> + <button type="button" class="btn btn-light text-danger" + onclick="updateStatus({{ reservation.reservation.id }}, 'cancelled')" title="Cancel"> + <i class="bi bi-x-circle"></i> + </button> + {% elif reservation.reservation.status == 'preparing' %} + <button type="button" class="btn btn-light text-info" + onclick="updateStatus({{ reservation.reservation.id }}, 'ready')" title="Mark as Ready"> + <i class="bi bi-check2-circle"></i> + </button> + {% elif reservation.reservation.status == 'ready' %} + <button type="button" class="btn btn-light text-success" + onclick="updateStatus({{ reservation.reservation.id }}, 'completed')" title="Mark as Completed"> + <i class="bi bi-check2-all"></i> + </button> + {% endif %} + </td> + </tr> + {% else %} + <tr> + <td colspan="8" class="text-center py-4"> + <div class="text-muted"> + <i class="bi bi-calendar-x fs-4 d-block mb-2"></i> + No reservations found + </div> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> + + <!-- Pagination --> + {% if pagination.pages > 1 %} + <div class="d-flex justify-content-between align-items-center mt-4"> + <span class="text-muted"> + Showing {{ pagination.items|length }} of {{ pagination.total }} reservations + </span> + <nav> + <ul class="pagination mb-0"> + <li class="page-item {% if not pagination.has_prev %}disabled{% endif %}"> + <a class="page-link" href="{{ url_for('reservations.list_reservations', page=pagination.prev_num, status=status) }}"> + <i class="bi bi-chevron-left"></i> + </a> + </li> + + {% for page in pagination.iter_pages() %} + {% if page %} + <li class="page-item {% if page == pagination.page %}active{% endif %}"> + <a class="page-link" href="{{ url_for('reservations.list_reservations', page=page, status=status) }}"> + {{ page }} + </a> + </li> + {% else %} + <li class="page-item disabled"> + <span class="page-link">...</span> + </li> + {% endif %} + {% endfor %} + + <li class="page-item {% if not pagination.has_next %}disabled{% endif %}"> + <a class="page-link" href="{{ url_for('reservations.list_reservations', page=pagination.next_num, status=status) }}"> + <i class="bi bi-chevron-right"></i> + </a> + </li> + </ul> + </nav> + </div> + {% endif %} </div> +<!-- Filter Modal --> +<div class="modal fade" id="filterModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Filter Reservations</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + <div class="modal-body"> + <form id="filterForm" method="GET"> + <div class="mb-3"> + <label class="form-label">Date Range</label> + <div class="input-group"> + <input type="date" name="start_date" class="form-control"> + <span class="input-group-text">to</span> + <input type="date" name="end_date" class="form-control"> + </div> + </div> + <div class="mb-3"> + <label class="form-label">Guest Count</label> + <div class="input-group"> + <input type="number" name="min_guests" class="form-control" placeholder="Min"> + <span class="input-group-text">to</span> + <input type="number" name="max_guests" class="form-control" placeholder="Max"> + </div> + </div> + <div class="mb-3"> + <label class="form-label">Status</label> + <select name="status" class="form-select"> + <option value="all">All Status</option> + <option value="pending">Pending</option> + <option value="preparing">Preparing</option> + <option value="ready">Ready</option> + <option value="completed">Completed</option> + <option value="cancelled">Cancelled</option> + </select> + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> + <button type="button" class="btn btn-primary" onclick="document.getElementById('filterForm').submit()">Apply Filters</button> + </div> + </div> + </div> +</div> {% endblock %} + +{% block scripts %} +<script> +async function updateStatus(reservationId, status) { + if (!confirm(`Are you sure you want to mark this reservation as ${status}?`)) { + return; + } + + try { + const response = await fetch(`/reservation/${reservationId}/status`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ status: status }) + }); + + const result = await response.json(); + if (result.success) { + location.reload(); + } else { + alert(result.message || 'Error updating reservation status'); + } + } catch (error) { + console.error('Error:', error); + alert('Error updating reservation status'); + } +} +</script> +{% endblock %} \ No newline at end of file diff --git a/app/templates/reservations/reserve_table.html b/app/templates/reservations/reserve_table.html deleted file mode 100644 index 8c14e43036df9845cf9e4691312e599e9af358e2..0000000000000000000000000000000000000000 --- a/app/templates/reservations/reserve_table.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends 'base.html' %} - -{% block content %} - -<div class="container"> - <h1 class="mt-4">Reserve a Table</h1> - <form action="/reserve" method="POST" class="mb-4"> - <div class="row g-3"> - <!-- Name --> - <div class="col-md-6"> - <input type="text" name="name" class="form-control" placeholder="Your Name" required> - </div> - - <!-- Contact Information --> - <div class="col-md-6"> - <input type="text" name="contact" class="form-control" placeholder="Contact Info (Phone/Email)" required> - </div> - - <!-- Reservation Date --> - <div class="col-md-6"> - <input type="date" name="date" class="form-control" required> - </div> - - <!-- Reservation Time --> - <div class="col-md-6"> - <input type="time" name="time" class="form-control" required> - </div> - - <!-- Number of Guests --> - <div class="col-md-6"> - <input type="number" name="guests" class="form-control" placeholder="Number of Guests" required> - </div> - - <!-- Additional Notes --> - <div class="col-md-6"> - <textarea name="notes" class="form-control" placeholder="Additional Notes (optional)"></textarea> - </div> - - <!-- Submit Button --> - <div class="col-md-12 text-end"> - <button type="submit" class="btn btn-primary">Submit Reservation</button> - </div> - </div> - </form> -</div> - -{% endblock %} \ No newline at end of file diff --git a/app/templates/restaurants/add.html b/app/templates/restaurants/add.html new file mode 100644 index 0000000000000000000000000000000000000000..93e20485d7be58ac961621da4d6c6530be8619c3 --- /dev/null +++ b/app/templates/restaurants/add.html @@ -0,0 +1,91 @@ +{% extends 'base.html' %} {% block title %}Add New Restaurant{% endblock %} {% +block styles %} +<style> + .form-section { + background: white; + border-radius: 12px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + } +</style> +{% endblock %} {% block content %} +<div class="container-fluid py-4"> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h1 class="h3 mb-0">Add New Restaurant</h1> + <a + href="{{ url_for('restaurant.get_restaurants') }}" + class="btn btn-outline-secondary" + > + <i class="bi bi-arrow-left"></i> Back to List + </a> + </div> + + <div class="form-section"> + <form method="POST" action="{{ url_for('restaurant.add_restaurant') }}"> + <div class="row g-4"> + <div class="col-md-6"> + <div class="mb-3"> + <label for="name" class="form-label">Restaurant Name</label> + <input + type="text" + class="form-control" + id="name" + name="name" + required + /> + </div> + </div> + <div class="col-md-6"> + <div class="mb-3"> + <label for="city" class="form-label">City</label> + <select class="form-select" id="city" name="city" required> + <option value="">Select a city</option> + <option value="london">London</option> + <option value="manchester">Manchester</option> + <option value="birmingham">Birmingham</option> + </select> + </div> + </div> + <div class="col-12"> + <div class="mb-3"> + <label for="address" class="form-label">Address</label> + <input + type="text" + class="form-control" + id="address" + name="address" + required + /> + </div> + </div> + <div class="col-md-6"> + <div class="mb-3"> + <label for="phone" class="form-label">Phone</label> + <input type="tel" class="form-control" id="phone" name="phone" /> + </div> + </div> + <div class="col-md-6"> + <div class="mb-3"> + <label for="email" class="form-label">Email</label> + <input type="email" class="form-control" id="email" name="email" /> + </div> + </div> + <div class="col-12"> + <div class="d-flex justify-content-end gap-2"> + <button + type="button" + class="btn btn-secondary" + onclick="history.back()" + > + Cancel + </button> + <button type="submit" class="btn btn-primary"> + Add Restaurant + </button> + </div> + </div> + </div> + </form> + </div> +</div> +{% endblock %} diff --git a/app/templates/restaurants/details.html b/app/templates/restaurants/details.html new file mode 100644 index 0000000000000000000000000000000000000000..fe3baa9b7ff86e98c9eba725989c87f6131ccc1f --- /dev/null +++ b/app/templates/restaurants/details.html @@ -0,0 +1,162 @@ +{% extends 'base.html' %} {% block title %}{{ restaurant.name }} - Details{% +endblock %} {% block styles %} +<style> + .metric-card { + background: white; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + height: 100%; + } + + .metric-value { + font-size: 2rem; + font-weight: 600; + color: #2c3e50; + margin: 0.5rem 0; + } + + .metric-label { + color: #6c757d; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .section-title { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 1.5rem; + color: #2c3e50; + } + + .reservation-card { + background: white; + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; + border-left: 4px solid #00a389; + } + + .menu-item { + background: white; + border-radius: 8px; + padding: 1rem; + margin-bottom: 0.5rem; + display: flex; + justify-content: space-between; + align-items: center; + } + + .menu-price { + font-weight: 600; + color: #00a389; + } +</style> +{% endblock %} {% block content %} +<div class="container-fluid py-4"> + <!-- Restaurant Header --> + <div class="d-flex justify-content-between align-items-center mb-4"> + <div> + <h1 class="h3 mb-0">{{ restaurant.name }}</h1> + <p class="text-muted mb-0"> + {{ restaurant.address }}, {{ restaurant.city }} + </p> + </div> + <div> + <a + href="{{ url_for('restaurant.get_restaurants') }}" + class="btn btn-outline-secondary" + > + <i class="bi bi-arrow-left"></i> Back to List + </a> + </div> + </div> + + <!-- Metrics Grid --> + <div class="row g-4 mb-4"> + <div class="col-md-4 col-lg-2"> + <div class="metric-card"> + <div class="metric-label">Monthly Orders</div> + <div class="metric-value">{{ metrics.total_orders }}</div> + </div> + </div> + <div class="col-md-4 col-lg-2"> + <div class="metric-card"> + <div class="metric-label">Revenue</div> + <div class="metric-value">${{ metrics.total_revenue }}</div> + </div> + </div> + <div class="col-md-4 col-lg-2"> + <div class="metric-card"> + <div class="metric-label">Avg Order Value</div> + <div class="metric-value">${{ metrics.avg_order_value }}</div> + </div> + </div> + <div class="col-md-4 col-lg-2"> + <div class="metric-card"> + <div class="metric-label">Customers</div> + <div class="metric-value">{{ metrics.total_customers }}</div> + </div> + </div> + <div class="col-md-4 col-lg-2"> + <div class="metric-card"> + <div class="metric-label">Tables Booked</div> + <div class="metric-value">{{ metrics.tables_booked }}</div> + </div> + </div> + <div class="col-md-4 col-lg-2"> + <div class="metric-card"> + <div class="metric-label">Menu Items</div> + <div class="metric-value">{{ metrics.menu_items }}</div> + </div> + </div> + </div> + + <!-- Today's Reservations and Menu --> + <div class="row"> + <div class="col-md-6"> + <div class="section-title">Today's Reservations</div> + {% if reservations %} {% for reservation in reservations %} + <div class="reservation-card"> + <div class="d-flex justify-content-between"> + <div> + <div class="fw-medium">{{ reservation.user.full_name }}</div> + <div class="text-muted"> + Table {{ reservation.table_number }} • {{ reservation.guest_count + }} guests + </div> + </div> + <div class="text-end"> + <div class="fw-medium"> + {{ reservation.reservation_time.strftime('%H:%M') }} + </div> + <div + class="badge bg-{{ 'success' if reservation.status == 'confirmed' else 'warning' }}" + > + {{ reservation.status }} + </div> + </div> + </div> + </div> + {% endfor %} {% else %} + <p class="text-muted">No reservations for today</p> + {% endif %} + </div> + <div class="col-md-6"> + <div class="section-title">Menu Items</div> + {% if menu_items %} {% for item in menu_items %} + <div class="menu-item"> + <div> + <div class="fw-medium">{{ item.name }}</div> + <div class="text-muted small">{{ item.category }}</div> + </div> + <div class="menu-price">${{ item.price }}</div> + </div> + {% endfor %} {% else %} + <p class="text-muted">No menu items available</p> + {% endif %} + </div> + </div> +</div> +{% endblock %} diff --git a/app/templates/restaurants/edit.html b/app/templates/restaurants/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..cc5297b3d6fd195d599522c071a98b36403406fc --- /dev/null +++ b/app/templates/restaurants/edit.html @@ -0,0 +1,108 @@ +{% extends 'base.html' %} {% block title %}Edit {{ restaurant.name }}{% endblock +%} {% block styles %} +<style> + .form-section { + background: white; + border-radius: 12px; + padding: 2rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + } +</style> +{% endblock %} {% block content %} +<div class="container-fluid py-4"> + <div class="d-flex justify-content-between align-items-center mb-4"> + <h1 class="h3 mb-0">Edit Restaurant</h1> + <a + href="{{ url_for('restaurant.get_restaurants') }}" + class="btn btn-outline-secondary" + > + <i class="bi bi-arrow-left"></i> Back to List + </a> + </div> + + <div class="form-section"> + <form + method="POST" + action="{{ url_for('restaurant.edit_restaurant', restaurant_id=restaurant.id) }}" + > + <div class="row g-4"> + <div class="col-md-6"> + <div class="mb-3"> + <label for="name" class="form-label">Restaurant Name</label> + <input + type="text" + class="form-control" + id="name" + name="name" + value="{{ restaurant.name }}" + required + /> + </div> + </div> + <div class="col-md-6"> + <div class="mb-3"> + <label for="city" class="form-label">City</label> + <input + type="text" + class="form-control" + id="city" + name="city" + value="{{ restaurant.city }}" + required + /> + </div> + </div> + <div class="col-12"> + <div class="mb-3"> + <label for="address" class="form-label">Address</label> + <input + type="text" + class="form-control" + id="address" + name="address" + value="{{ restaurant.address }}" + required + /> + </div> + </div> + <div class="col-md-6"> + <div class="mb-3"> + <label for="phone" class="form-label">Phone</label> + <input + type="tel" + class="form-control" + id="phone" + name="phone" + value="{{ restaurant.phone }}" + /> + </div> + </div> + <div class="col-md-6"> + <div class="mb-3"> + <label for="email" class="form-label">Email</label> + <input + type="email" + class="form-control" + id="email" + name="email" + value="{{ restaurant.email }}" + /> + </div> + </div> + <div class="col-12"> + <div class="d-flex justify-content-end gap-2"> + <button + type="button" + class="btn btn-secondary" + onclick="history.back()" + > + Cancel + </button> + <button type="submit" class="btn btn-primary">Save Changes</button> + </div> + </div> + </div> + </form> + </div> +</div> +{% endblock %} diff --git a/app/templates/restaurants/restaurant_performance.html b/app/templates/restaurants/restaurant_performance.html new file mode 100644 index 0000000000000000000000000000000000000000..74b7f1102713001ffa158b071671a0c861b207d0 --- /dev/null +++ b/app/templates/restaurants/restaurant_performance.html @@ -0,0 +1,318 @@ +{% extends 'base.html' %} {% block title %}HRMS - Restaurant Performance{% +endblock %} {% block styles %} +<style> + .performance-badge { + padding: 0.5em 0.8em; + border-radius: 20px; + font-size: 0.85em; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .restaurant-table th { + background-color: #f8f9fa; + border-bottom: 2px solid #dee2e6; + white-space: nowrap; + text-transform: uppercase; + font-size: 0.85em; + letter-spacing: 0.5px; + font-weight: 600; + } + + .restaurant-table td { + vertical-align: middle; + } + + .search-filter-section { + background: white; + padding: 1.5rem; + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + margin-bottom: 1.5rem; + } + + .search-input { + max-width: 300px; + border-radius: 20px; + padding: 0.75rem 1rem 0.75rem 2.5rem; + border: 1px solid #dee2e6; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%236c757d' class='bi bi-search' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: 12px center; + } + + .card { + border-radius: 12px; + border: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + } + + .restaurant-info { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .restaurant-icon { + width: 32px; + height: 32px; + border-radius: 50%; + background-color: #e9ecef; + display: flex; + align-items: center; + justify-content: center; + font-weight: 500; + color: #495057; + } + + .metrics { + font-size: 0.875rem; + color: #6c757d; + } + + .metrics i { + margin-right: 0.25rem; + color: #00a389; + } +</style> +{% endblock %} {% block content %} +<div class="container-fluid"> + <!-- Search and Filter Section --> + <div class="search-filter-section"> + <div class="d-flex justify-content-between align-items-center"> + <input + type="text" + class="form-control search-input" + placeholder="Search restaurant..." + /> + <div> + <a + href="{{ url_for('restaurant.add_restaurant') }}" + class="btn btn-primary me-2" + > + <i class="bi bi-plus-lg"></i> Add Restaurant + </a> + <button + class="btn btn-outline-secondary" + data-bs-toggle="modal" + data-bs-target="#filterModal" + > + <i class="bi bi-funnel"></i> Filter + </button> + </div> + </div> + </div> + + <!-- Restaurant Performance Table Card --> + <div class="card"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover mb-0 restaurant-table"> + <thead> + <tr> + <th>Restaurant</th> + <th>Tables Booked</th> + <th>Orders</th> + <th>Revenue</th> + <th>Performance</th> + <th class="text-end">Actions</th> + </tr> + </thead> + <tbody> + {% for restaurant in restaurants %} + <tr> + <td> + <div class="restaurant-info"> + <div class="restaurant-icon">{{ restaurant.name[:1] }}</div> + <div> + <div class="fw-medium">{{ restaurant.name }}</div> + <div class="text-muted small">ID: {{ restaurant.id }}</div> + </div> + </div> + </td> + <td> + <div class="metrics"> + <div> + <i class="bi bi-calendar2-check"></i> Total: {{ + restaurant.tables_booked }} + </div> + <div> + <i class="bi bi-percent"></i> Occupancy: {{ + restaurant.occupancy_rate }}% + </div> + </div> + </td> + <td> + <div class="metrics"> + <div> + <i class="bi bi-bag-check"></i> Total: {{ + restaurant.total_orders }} + </div> + </div> + </td> + <td> + <div class="metrics"> + <div> + <i class="bi bi-currency-dollar"></i> Today: ${{ + restaurant.daily_revenue }} + </div> + <div> + <i class="bi bi-graph-up"></i> Monthly: ${{ + restaurant.monthly_revenue }} + </div> + </div> + </td> + <td> + <div class="metrics"> + <div> + <i class="bi bi-arrow-up-right"></i> Growth: {{ + restaurant.growth_rate }}% + </div> + <div> + <i class="bi bi-people"></i> Customers: {{ + restaurant.customer_count }} + </div> + </div> + </td> + <td class="text-end"> + <div class="btn-group"> + <a + href="{{ url_for('restaurant.get_restaurant_details', restaurant_id=restaurant.id) }}" + class="btn btn-outline-primary btn-sm" + title="Details" + > + <i class="bi bi-graph-up"></i> + </a> + <a + href="{{ url_for('restaurant.edit_restaurant', restaurant_id=restaurant.id) }}" + class="btn btn-outline-secondary btn-sm" + title="Edit" + > + <i class="bi bi-pencil"></i> + </a> + <button + type="button" + class="btn btn-outline-danger btn-sm" + data-bs-toggle="modal" + data-bs-target="#deleteModal{{ restaurant.id }}" + title="Delete" + > + <i class="bi bi-trash"></i> + </button> + </div> + <div + class="modal fade" + id="deleteModal{{ restaurant.id }}" + tabindex="-1" + > + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Delete Restaurant</h5> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + ></button> + </div> + <div class="modal-body"> + <p> + Are you sure you want to delete {{ restaurant.name }}? + This action cannot be undone. + </p> + </div> + <div class="modal-footer"> + <button + type="button" + class="btn btn-secondary" + data-bs-dismiss="modal" + > + Cancel + </button> + <form + action="{{ url_for('restaurant.delete_restaurant', restaurant_id=restaurant.id) }}" + method="POST" + class="d-inline" + > + <button type="submit" class="btn btn-danger"> + Delete + </button> + </form> + </div> + </div> + </div> + </div> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> +</div> + +<!-- Filter Modal --> +<div class="modal fade" id="filterModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Filter Restaurants</h5> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + ></button> + </div> + <div class="modal-body"> + <form id="filterForm"> + <div class="mb-3"> + <label class="form-label">City</label> + <select class="form-select" name="city"> + <option value="">All Cities</option> + <option value="london">London</option> + <option value="manchester">Manchester</option> + <option value="birmingham">Birmingham</option> + </select> + </div> + <div class="mb-3"> + <label class="form-label">Performance</label> + <select class="form-select" name="performance"> + <option value="">All Performance Levels</option> + <option value="high">High Performing</option> + <option value="medium">Medium Performing</option> + <option value="low">Low Performing</option> + </select> + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> + Cancel + </button> + <button type="button" class="btn btn-primary" onclick="applyFilters()"> + Apply Filters + </button> + </div> + </div> + </div> +</div> +{% endblock %} {% block scripts %} +<script> + function applyFilters() { + const formData = new FormData(document.getElementById("filterForm")); + const params = new URLSearchParams(); + + for (let [key, value] of formData.entries()) { + if (value) params.append(key, value); + } + + if (params.toString()) { + window.location.href = `${window.location.pathname}?${params.toString()}`; + } + + bootstrap.Modal.getInstance(document.getElementById("filterModal")).hide(); + } +</script> +{% endblock %} diff --git a/app/templates/staffs/attendance.html b/app/templates/staffs/attendance.html new file mode 100644 index 0000000000000000000000000000000000000000..607df9cf6b66d92e881be69b61ef158a52d0b7cb --- /dev/null +++ b/app/templates/staffs/attendance.html @@ -0,0 +1,184 @@ +{% extends 'base.html' %} {% block title %}HRMS - Manage Staff Attendance{% +endblock %} {% block styles %} +<style> + .attendance-card { + background: white; + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + margin-bottom: 1rem; + padding: 1.5rem; + } + + .status-badge { + padding: 0.5em 0.8em; + border-radius: 20px; + font-size: 0.85em; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .status-badge.present { + background-color: #d1e7dd; + color: #0f5132; + } + .status-badge.late { + background-color: #fff3cd; + color: #856404; + } + .status-badge.not-checked-in { + background-color: #e9ecef; + color: #495057; + } + + .staff-info { + display: flex; + align-items: center; + gap: 1rem; + } + + .staff-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background-color: #e9ecef; + display: flex; + align-items: center; + justify-content: center; + font-weight: 500; + color: #495057; + } + + .action-buttons .btn { + min-width: 100px; + } +</style> +{% endblock %} {% block content %} +<div class="container-fluid py-4"> + <div class="row mb-4"> + <div class="col-12"> + <div class="attendance-card"> + <div class="d-flex justify-content-between align-items-center mb-4"> + <div> + <h3 class="mb-0">Staff Attendance Management</h3> + <p class="text-muted mb-0">{{ today_date }}</p> + </div> + <div class="time-display h4" id="currentTime"></div> + </div> + + <div class="table-responsive"> + <table class="table"> + <thead> + <tr> + <th>Staff Member</th> + <th>Role</th> + <th>Status</th> + <th>Check In</th> + <th>Check Out</th> + <th class="text-center">Actions</th> + </tr> + </thead> + <tbody> + {% for member in staff %} + <tr> + <td> + <div class="staff-info"> + <div class="staff-avatar">{{ member.name[:1] }}</div> + <div>{{ member.name }}</div> + </div> + </td> + <td> + <span class="role-badge {{ member.role }}" + >{{ member.role }}</span + > + </td> + <td> + <span class="status-badge {{ member.status }}" + >{{ member.status }}</span + > + </td> + <td>{{ member.clock_in or '-' }}</td> + <td>{{ member.clock_out or '-' }}</td> + <td class="text-center"> + <div class="action-buttons"> + {% if not member.clock_in %} + <button + class="btn btn-primary" + onclick="updateAttendance({{ member.id }}, 'check-in')" + > + <i class="bi bi-box-arrow-in-right"></i> Check In + </button> + {% elif not member.clock_out %} + <button + class="btn btn-secondary" + onclick="updateAttendance({{ member.id }}, 'check-out')" + > + <i class="bi bi-box-arrow-right"></i> Check Out + </button> + {% else %} + <button class="btn btn-outline-secondary" disabled> + Completed + </button> + {% endif %} + </div> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> + </div> +</div> +{% endblock %} {% block scripts %} +<script> + function updateTime() { + const now = new Date(); + document.getElementById("currentTime").textContent = now.toLocaleTimeString( + "en-US", + { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + } + ); + } + + setInterval(updateTime, 1000); + updateTime(); + + function updateAttendance(userId, action) { + if ( + !confirm( + `Are you sure you want to ${action.replace( + "-", + " " + )} this staff member?` + ) + ) { + return; + } + + fetch("/attendance/manage", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: `user_id=${userId}&action=${action}`, + }) + .then((response) => response.json()) + .then((data) => { + if (data.status === "success") { + location.reload(); + } else { + alert(data.message); + } + }) + .catch((error) => { + console.error("Error:", error); + alert("An error occurred. Please try again."); + }); + } +</script> +{% endblock %} diff --git a/app/templates/staffs/staff_performance.html b/app/templates/staffs/staff_performance.html new file mode 100644 index 0000000000000000000000000000000000000000..63d1f53c32ccc8211c8ced09b805d3c9eedc3970 --- /dev/null +++ b/app/templates/staffs/staff_performance.html @@ -0,0 +1,277 @@ +{% extends 'base.html' %} {% block title %}HRMS - Staff Performance{% endblock +%} {% block styles %} +<style> + .performance-badge { + padding: 0.5em 0.8em; + border-radius: 20px; + font-size: 0.85em; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .performance-badge.excellent { + background-color: #d1e7dd; + color: #0f5132; + } + .performance-badge.good { + background-color: #cff4fc; + color: #055160; + } + .performance-badge.average { + background-color: #fff3cd; + color: #856404; + } + .performance-badge.poor { + background-color: #f8d7da; + color: #842029; + } + + .staff-table th { + background-color: #f8f9fa; + border-bottom: 2px solid #dee2e6; + white-space: nowrap; + text-transform: uppercase; + font-size: 0.85em; + letter-spacing: 0.5px; + font-weight: 600; + } + + .staff-table td { + vertical-align: middle; + } + + .search-filter-section { + background: white; + padding: 1.5rem; + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + margin-bottom: 1.5rem; + } + + .search-input { + max-width: 300px; + border-radius: 20px; + padding: 0.75rem 1rem 0.75rem 2.5rem; + border: 1px solid #dee2e6; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%236c757d' class='bi bi-search' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: 12px center; + } + + .card { + border-radius: 12px; + border: none; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + } + + .staff-info { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .staff-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background-color: #e9ecef; + display: flex; + align-items: center; + justify-content: center; + font-weight: 500; + color: #495057; + } + + .metrics { + font-size: 0.875rem; + color: #6c757d; + } + + .metrics i { + margin-right: 0.25rem; + color: #00a389; + } +</style> +{% endblock %} {% block content %} +<div class="container-fluid"> + <!-- Search and Filter Section --> + <div class="search-filter-section"> + <div class="d-flex justify-content-between align-items-center"> + <input + type="text" + class="form-control search-input" + placeholder="Search staff..." + /> + + <div> + <div class="btn-group"> + <button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> + <i class="bi bi-person-plus"></i> Add New + </button> + <ul class="dropdown-menu"> + <li><a class="dropdown-item" href="{{ url_for('users.add_user', role='staff') }}">Add Staff</a></li> + <li><a class="dropdown-item" href="{{ url_for('users.add_user', role='admin') }}">Add Admin</a></li> + </ul> + </div> + <a + href="{{ url_for('staff.attendance_check') }}" + class="btn btn-primary ms-2" + > + <i class="bi bi-calendar-check"></i> Attendance + </a> + <button + class="btn btn-outline-secondary ms-2" + data-bs-toggle="modal" + data-bs-target="#filterModal" + > + <i class="bi bi-funnel"></i> Filter + </button> + </div> + </div> + </div> + + <!-- Staff Performance Table Card --> + <div class="card"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover mb-0 staff-table"> + <thead> + <tr> + <th>Staff Member</th> + <th>Role</th> + <th>Performance Metrics</th> + <th>Attendance</th> + <th>Notes</th> + <th class="text-end">Actions</th> + </tr> + </thead> + <tbody> + {% for staff_member in staff %} + <tr> + <td> + <div class="staff-info"> + <div class="staff-avatar">{{ staff_member.name[:1] }}</div> + <div> + <div class="fw-medium">{{ staff_member.name }}</div> + <div class="text-muted small"> + ID: {{ staff_member.id }} + </div> + </div> + </div> + </td> + <td> + <span class="role-badge {{ staff_member.role.lower() }}" + >{{ staff_member.role }}</span + > + </td> + <td> + <div class="metrics"> + <div> + <i class="bi bi-clipboard-check"></i> Orders Handled: {{ + staff_member.orders_handled }} + </div> + <div> + <i class="bi bi-star"></i> Rating: {{ + staff_member.performance_rating }}/5 + </div> + </div> + </td> + <td> + <div class="metrics"> + <div> + <i class="bi bi-calendar-check"></i> {{ + staff_member.attendance }}% + </div> + <div> + <i class="bi bi-clock"></i> On-time: {{ + staff_member.punctuality }}% + </div> + </div> + </td> + <td> + <div class="text-muted small"> + {{ staff_member.notes|truncate(50) }} + </div> + </td> + <td class="text-end"> + <button + class="btn btn-outline-primary btn-sm" + data-bs-toggle="modal" + data-bs-target="#performanceModal{{ staff_member.id }}" + > + <i class="bi bi-graph-up"></i> Details + </button> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> +</div> + +<!-- Filter Modal --> +<div class="modal fade" id="filterModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Filter Staff</h5> + <button + type="button" + class="btn-close" + data-bs-dismiss="modal" + ></button> + </div> + <div class="modal-body"> + <form id="filterForm"> + <div class="mb-3"> + <label class="form-label">Role</label> + <select class="form-select" name="role"> + <option value="">All Roles</option> + <option value="manager">Manager</option> + <option value="staff">Staff</option> + </select> + </div> + <div class="mb-3"> + <label class="form-label">Performance Rating</label> + <select class="form-select" name="rating"> + <option value="">All Ratings</option> + <option value="excellent">Excellent</option> + <option value="good">Good</option> + <option value="average">Average</option> + <option value="poor">Poor</option> + </select> + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> + Cancel + </button> + <button type="button" class="btn btn-primary" onclick="applyFilters()"> + Apply Filters + </button> + </div> + </div> + </div> +</div> +{% endblock %} {% block scripts %} +<script> + function applyFilters() { + const formData = new FormData(document.getElementById("filterForm")); + const params = new URLSearchParams(); + + for (let [key, value] of formData.entries()) { + if (value) params.append(key, value); + } + + if (params.toString()) { + window.location.href = `${window.location.pathname}?${params.toString()}`; + } + + bootstrap.Modal.getInstance(document.getElementById("filterModal")).hide(); + } +</script> +{% endblock %} diff --git a/app/templates/users/add_user.html b/app/templates/users/add_user.html index a394c299a9e22db49e47d205ae965eca7f6488b1..6620071c223e8caf80524998e13e1e82f44774cb 100644 --- a/app/templates/users/add_user.html +++ b/app/templates/users/add_user.html @@ -1,49 +1,169 @@ -{% extends 'base.html' %} {% block content %} -<div class="container"> - <h2 align="center" class="mb-4">Add New User</h2> -</div> +{% extends 'base.html' %} -<div class="container py-5"> - <form method="POST" action="{{ url_for('users.add_user') }}"> - <div> - <label for="full_name">Full Name:</label> - <input type="text" id="full_name" name="full_name" required /> - </div> - <div> - <label for="email">Email:</label> - <input type="email" id="email" name="email" required /> - </div> - <div> - <label for="phone">Phone:</label> - <input type="text" id="phone" name="phone" required /> - </div> - <div> - <label for="phone">Password:</label> - <input type="password" id="password" name="password" required /> - </div> - <div> - <label for="repeat_password">Repeat Password:</label> - <input - type="password" - id="repeat_password" - name="repeat_password" - required - /> - </div> - <div> - <label for="role">Role:</label> - <select id="role" name="role" required> - <option name="admin" value="admin">Admin</option> - <option name="manager" value="manager">Manager</option> - <option name="staff" value="staff">Staff</option> - <option name="customer" value="customer">Customer</option> - </select> +{% block title %}HRMS - Add New User{% endblock %} + +{% block styles %} +<style> + .card { + border-radius: 12px; + border: none; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + } + + .form-label { + font-weight: 500; + color: #495057; + margin-bottom: 0.5rem; + } + + .form-control, .form-select { + border-radius: 8px; + padding: 0.75rem 1rem; + border: 1px solid #dee2e6; + } + + .form-control:focus, .form-select:focus { + box-shadow: 0 0 0 0.2rem rgba(0, 163, 137, 0.25); + border-color: #00a389; + } +</style> +{% endblock %} + +{% block content %} +<div class="container-fluid py-4"> + <div class="row justify-content-center"> + <div class="col-lg-8"> + <div class="card"> + <div class="card-body p-4"> + <h5 class="card-title mb-4"> + {% if selected_role == 'customer' %} + Add New Customer + {% elif selected_role == 'staff' %} + Add New Staff Member + {% else %} + Add New Admin + {% endif %} + </h5> + + <form method="POST" action="{{ url_for('users.add_user', role=selected_role) }}"> + <div class="row g-3"> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label" for="full_name">Full Name</label> + <input type="text" class="form-control" id="full_name" name="full_name" + placeholder="Enter full name" required> + </div> + </div> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label" for="email">Email Address</label> + <input type="email" class="form-control" id="email" name="email" + placeholder="Enter email address" required> + </div> + </div> + <div class="mb-3"> + <label class="form-label" for="role">Role</label> + <select class="form-select" id="role" name="role" required> + <option value="" disabled selected>Select role</option> + <option value="admin">Admin</option> + <option value="manager">Manager</option> + <option value="staff">Staff</option> + <option value="customer">Customer</option> + </select> + </div> + <div class="mb-3"> + <label class="form-label" for="phone">Phone Number</label> + <input type="tel" class="form-control" id="phone" name="phone" + placeholder="Enter phone number" required> + </div> + </div> + {% if selected_role != 'customer' %} + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label" for="restaurant">Restaurant</label> + <select class="form-select" id="restaurant" name="restaurant_id" required> + <option value="" disabled selected>Select restaurant</option> + {% for restaurant in restaurants %} + <option value="{{ restaurant.id }}">{{ restaurant.name }}</option> + {% endfor %} + </select> + </div> + </div> + {% if selected_role == 'staff' %} + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label" for="staff_role">Staff Role</label> + <select class="form-select" id="staff_role" name="role" required> + <option value="" disabled selected>Select role</option> + <option value="cook">Cook</option> + <option value="dishwasher">Dishwasher</option> + <option value="server">Server</option> + <option value="host">Host</option> + <option value="busser">Busser</option> + </select> + </div> + </div> + {% else %} + <input type="hidden" name="role" value="{{ selected_role }}"> + {% endif %} + {% else %} + <input type="hidden" name="role" value="{{ selected_role }}"> + {% endif %} + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label" for="password">Password</label> + <input type="password" class="form-control" id="password" name="password" + placeholder="Enter password" required> + </div> + </div> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label" for="repeat_password">Confirm Password</label> + <input type="password" class="form-control" id="repeat_password" + name="repeat_password" placeholder="Confirm password" required> + </div> + </div> + </div> + + <div class="d-flex justify-content-between mt-4"> + {% if selected_role == 'customer' %} + <a href="{{ url_for('users.manage_user') }}" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left"></i> Back to Users + </a> + {% else %} + <a href="{{ url_for('staff.staff_list') }}" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left"></i> Back to Staff + </a> + {% endif %} + <button type="submit" class="btn btn-primary"> + <i class="bi bi-person-plus"></i> + {% if selected_role == 'customer' %} + Add Customer + {% elif selected_role == 'staff' %} + Add Staff Member + {% else %} + Add Admin + {% endif %} + </button> + </div> + </form> + </div> + </div> + </div> </div> - <br /> - <button type="submit" class="btn-primary btn">Add User</button> - <br /> - <a href="{{ url_for('users.manage_user')}}">Return</a> - </form> </div> +{% endblock %} +{% block scripts %} +<script> +document.querySelector('form').addEventListener('submit', function(e) { + const password = document.getElementById('password').value; + const repeatPassword = document.getElementById('repeat_password').value; + + if (password !== repeatPassword) { + e.preventDefault(); + alert('Passwords do not match!'); + } +}); +</script> {% endblock %} diff --git a/app/templates/users/edit_user.html b/app/templates/users/edit_user.html index 1949d1bc41747da3d887d1661de2ff61f4387bfc..360adb99ceedcb39d9df4887cc65b5632f1d7df6 100644 --- a/app/templates/users/edit_user.html +++ b/app/templates/users/edit_user.html @@ -1,61 +1,109 @@ -{% extends 'base.html' %} {% block content %} +{% extends 'base.html' %} -<div class="container"> - <h2 align="center" class="mb-4">Edit User</h2> -</div> +{% block title %}HRMS - Edit User{% endblock %} -<div class="container py-5"> - <div class="container"> - <form - action="{{ url_for('users.edit_user', user_id=user.id) }}" - method="POST" - > - <div class="form-group"> - <label for="full_name">Full Name</label> - <input - type="text" - class="form-control" - id="full_name" - name="full_name" - value="{{ user.full_name }}" - /> - </div> - <div class="form-group"> - <label for="email">Email</label> - <input - type="text" - class="form-control" - id="email" - name="email" - value="{{ user.email }}" - /> - </div> - <div class="form-group"> - <label for="phone">Phone</label> - <input - type="text" - class="form-control" - id="phone" - name="phone" - value="{{ user.phone }}" - /> - </div> - <div class="form-group"> - <label for="role">Phone</label> - <input - type="text" - class="form-control" - id="role" - name="role" - value="{{ user.role }}" - /> - </div> - <br /> - <div align="center"> - <button type="submit" class="btn btn-primary">Submit</button> - </div> - </form> - </div> -</div> +{% block styles %} +<style> + .card { + border-radius: 12px; + border: none; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + } + + .form-label { + font-weight: 500; + color: #495057; + margin-bottom: 0.5rem; + } + + .form-control, .form-select { + border-radius: 8px; + padding: 0.75rem 1rem; + border: 1px solid #dee2e6; + } + + .form-control:focus, .form-select:focus { + box-shadow: 0 0 0 0.2rem rgba(0, 163, 137, 0.25); + border-color: #00a389; + } + .user-avatar { + width: 64px; + height: 64px; + border-radius: 50%; + background-color: #e9ecef; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + font-weight: 500; + color: #495057; + margin-bottom: 1rem; + } +</style> +{% endblock %} + +{% block content %} +<div class="container-fluid py-4"> + <div class="row justify-content-center"> + <div class="col-lg-8"> + <div class="card"> + <div class="card-body p-4"> + <div class="text-center mb-4"> + <div class="user-avatar mx-auto"> + {{ user.full_name[:1] }} + </div> + <h5 class="card-title">Edit User Profile</h5> + </div> + + <form action="{{ url_for('users.edit_user', user_id=user.id) }}" method="POST"> + <div class="row g-3"> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label" for="full_name">Full Name</label> + <input type="text" class="form-control" id="full_name" name="full_name" + value="{{ user.full_name }}" placeholder="Enter full name" required> + </div> + </div> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label" for="email">Email Address</label> + <input type="email" class="form-control" id="email" name="email" + value="{{ user.email }}" placeholder="Enter email address" required> + </div> + </div> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label" for="phone">Phone Number</label> + <input type="tel" class="form-control" id="phone" name="phone" + value="{{ user.phone }}" placeholder="Enter phone number" required> + </div> + </div> + <div class="col-md-6"> + <div class="mb-3"> + <label class="form-label" for="role">Role</label> + <select class="form-select" id="role" name="role" required> + <option value="admin" {% if user.role == 'admin' %}selected{% endif %}>Admin</option> + <option value="manager" {% if user.role == 'manager' %}selected{% endif %}>Manager</option> + <option value="staff" {% if user.role == 'staff' %}selected{% endif %}>Staff</option> + <option value="customer" {% if user.role == 'customer' %}selected{% endif %}>Customer</option> + </select> + </div> + </div> + </div> + + <div class="d-flex justify-content-between mt-4"> + <a href="{{ url_for('users.manage_user') }}" class="btn btn-outline-secondary"> + <i class="bi bi-arrow-left"></i> Back to Users + </a> + <button type="submit" class="btn btn-primary"> + <i class="bi bi-check-lg"></i> Save Changes + </button> + </div> + </form> + </div> + </div> + </div> + </div> +</div> {% endblock %} diff --git a/app/templates/users/manage_users.html b/app/templates/users/manage_users.html index 5fa91fc72d67441f5132b6c36d3824229742fc2b..b11243b4ee5c6c36c933c7a8d1753aa54d5e93ec 100644 --- a/app/templates/users/manage_users.html +++ b/app/templates/users/manage_users.html @@ -1,51 +1,222 @@ -{% extends 'base.html' %} {% block content %} +{% extends 'base.html' %} -<div class="container"> - <h2 align="center" class="mb-4">Manage Users</h2> +{% block title %}HRMS - Manage Users{% endblock %} + +{% block styles %} +<style> + .role-badge { + padding: 0.5em 0.8em; + border-radius: 20px; + font-size: 0.85em; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .role-badge.admin { background-color: #f8d7da; color: #842029; } + .role-badge.manager { background-color: #cff4fc; color: #055160; } + .role-badge.staff { background-color: #fff3cd; color: #856404; } + .role-badge.customer { background-color: #d1e7dd; color: #0f5132; } + + .users-table th { + background-color: #f8f9fa; + border-bottom: 2px solid #dee2e6; + white-space: nowrap; + text-transform: uppercase; + font-size: 0.85em; + letter-spacing: 0.5px; + font-weight: 600; + } + + .users-table td { + vertical-align: middle; + } + + .search-filter-section { + background: white; + padding: 1.5rem; + border-radius: 12px; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + margin-bottom: 1.5rem; + } + + .search-input { + max-width: 300px; + border-radius: 20px; + padding: 0.75rem 1rem 0.75rem 2.5rem; + border: 1px solid #dee2e6; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%236c757d' class='bi bi-search' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: 12px center; + } + + .search-input:focus { + box-shadow: 0 0 0 0.2rem rgba(0, 163, 137, 0.25); + border-color: #00a389; + } + + .card { + border-radius: 12px; + border: none; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + } + + .action-buttons .btn { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + border-radius: 8px; + margin-left: 0.25rem; + } + + .user-info { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .user-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background-color: #e9ecef; + display: flex; + align-items: center; + justify-content: center; + font-weight: 500; + color: #495057; + } + + .contact-info { + font-size: 0.875rem; + color: #6c757d; + } + + .contact-info i { + margin-right: 0.25rem; + color: #00a389; + } +</style> +{% endblock %} + +{% block content %} +<div class="container-fluid"> + <!-- Search and Filter Section --> + <div class="search-filter-section"> + <div class="d-flex justify-content-between align-items-center"> + <input type="text" class="form-control search-input" placeholder="Search users..."> + <div> + <a href="{{ url_for('users.add_user') }}" class="btn btn-primary"> + <i class="bi bi-person-plus"></i> Add New User + </a> + <button class="btn btn-outline-secondary ms-2" data-bs-toggle="modal" data-bs-target="#filterModal"> + <i class="bi bi-funnel"></i> Filter + </button> + </div> + </div> + </div> + + <!-- Users Table Card --> + <div class="card"> + <div class="card-body p-0"> + <div class="table-responsive"> + <table class="table table-hover mb-0 users-table"> + <thead> + <tr> + <th>User</th> + <th>Contact Information</th> + <th>Role</th> + <th class="text-end">Actions</th> + </tr> + </thead> + <tbody> + {% for user in users %} + <tr> + <td> + <div class="user-info"> + <div class="user-avatar"> + {{ user.full_name[:1] }} + </div> + <div> + <div class="fw-medium">{{ user.full_name }}</div> + <div class="text-muted small">ID: {{ user.id }}</div> + </div> + </div> + </td> + <td> + <div class="contact-info"> + <div><i class="bi bi-envelope"></i> {{ user.email }}</div> + <div><i class="bi bi-telephone"></i> {{ user.phone }}</div> + </div> + </td> + <td> + <span class="role-badge {{ user.role.lower() }}">{{ user.role }}</span> + </td> + <td class="text-end action-buttons"> + <a href="{{ url_for('users.edit_user', user_id=user.id) }}" + class="btn btn-outline-primary"> + <i class="bi bi-pencil"></i> + </a> + <a href="{{ url_for('users.delete_user', user_id=user.id) }}" + class="btn btn-outline-danger" + onclick="return confirm('Are you sure you want to delete this user?')"> + <i class="bi bi-trash"></i> + </a> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> </div> -<div class="container py-5"> - <table class="table table-bordered"> - <thead class="table-dark"> - <tr> - <th>ID</th> - <th>User Name</th> - <th>Email</th> - <th>Phone Number</th> - <th>Role</th> - <th>Edit</th> - <th>Delete</th> - </tr> - </thead> - <tbody> - {% for user in users %} - <tr> - <td>{{ user.id }}</td> - <td>{{ user.full_name }}</td> - <td>{{ user.email }}</td> - <td>{{ user.phone }}</td> - <td>{{ user.role }}</td> - <td> - <a - href="{{ url_for('users.edit_user', user_id=user.id) }}" - class="btn btn-primary btn-sm" - >Edit</a - > - </td> - <td> - <a - href="{{ url_for('users.delete_user', user_id=user.id) }}" - class="btn btn-danger btn-sm" - >Delete</a - > - </td> - </tr> - {% endfor %} - </tbody> - </table> - - <a href="{{ url_for('users.add_user') }}" class="btn btn-primary" - >Add New User</a - > + +<!-- Filter Modal --> +<div class="modal fade" id="filterModal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Filter Users</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal"></button> + </div> + <div class="modal-body"> + <form id="filterForm"> + <div class="mb-3"> + <label class="form-label">Role</label> + <select class="form-select" name="role"> + <option value="">All Roles</option> + <option value="admin">Admin</option> + <option value="manager">Manager</option> + <option value="staff">Staff</option> + <option value="customer">Customer</option> + </select> + </div> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> + <button type="button" class="btn btn-primary" onclick="applyFilters()">Apply Filters</button> + </div> + </div> + </div> </div> +{% endblock %} +{% block scripts %} +<script> +function applyFilters() { + const formData = new FormData(document.getElementById('filterForm')); + const params = new URLSearchParams(); + + for (let [key, value] of formData.entries()) { + if (value) params.append(key, value); + } + + if (params.toString()) { + window.location.href = `${window.location.pathname}?${params.toString()}`; + } + + bootstrap.Modal.getInstance(document.getElementById('filterModal')).hide(); +} +</script> {% endblock %} diff --git a/main.py b/main.py index a430d64b0062bcb9fb9d4765fbf03b7c25cf982a..827ca262bc876f28fca2894c633ff4ed2859a987 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,31 @@ from app import create_app +from app.models import db +from app.dummydata import create_dummy_data +# Create Flask application instance using the application factory app = create_app() + +def init_database(): + """Initialize database and create dummy data only if not exists""" + with app.app_context(): + # Create tables only if they don't exist + db.create_all() + print("Ensured all tables exist.") + + # Check if dummy data already exists before inserting + from app.models import User + + if not User.query.first(): + create_dummy_data() + print("Inserted dummy data.") + else: + print("Dummy data already exists. Skipping insertion.") + + if __name__ == "__main__": - app.run(debug=True, host = "0.0.0.0") + # Initialize database before running the app + init_database() + + # Run the application in debug mode when executed directly + app.run(debug=True) diff --git a/requirements.txt b/requirements.txt index 468d062826f6fb56021c16c562606a7a92eaa412..91564023aedfa73a0ba9f25c45e8126f45a849f5 100644 Binary files a/requirements.txt and b/requirements.txt differ