diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..e673d9c3d1c4296147a823701d5ae5053ea5f45a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,39 @@ +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text +*.db filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.webp filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..53ef067ac0303eaba4b7a51e811edddae93e83ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.venv/ +/frontend/node_modules/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..26bd18c3c4a5e209f60fe5d0b6f2c97519b1cdbf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# Use Python 3.10 slim image as base +FROM python:3.10-slim + +# Set working directory +WORKDIR /code + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first to leverage Docker cache +COPY requirements.txt . + +# Install Python dependencies +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application +COPY app app/ +COPY templates templates/ +COPY run.py . +COPY tabble_new.db . + +# Set environment variables +ENV PORT=7860 +ENV HOST=0.0.0.0 + +# Expose the port +EXPOSE 7860 + +# Command to run the application +CMD ["python", "run.py"] \ No newline at end of file diff --git a/HUGGINGFACE_README.md b/HUGGINGFACE_README.md new file mode 100644 index 0000000000000000000000000000000000000000..9ac65f3cc90497bef074b212ad3bc45b7119db3c --- /dev/null +++ b/HUGGINGFACE_README.md @@ -0,0 +1,75 @@ +# Deploying to Hugging Face Spaces + +This guide explains how to deploy the FastAPI backend to Hugging Face Spaces using Docker. + +## Prerequisites + +1. A Hugging Face account +2. Git installed on your machine +3. Docker installed (for local testing) + +## Steps to Deploy + +1. Create a new Space on Hugging Face: + - Go to https://huggingface.co/spaces + - Click "Create new Space" + - Select "Docker" as the SDK + - Choose a name for your space + - Set visibility (public/private) + +2. Clone the new Space repository: +```bash +git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME +``` + +3. Copy the following files to the cloned repository: + - `Dockerfile` + - `requirements.txt` + - `app/` directory + - `templates/` directory + - `run.py` + - `tabble_new.db` + +4. Update your environment variables: + - Go to your Space's Settings + - Add any necessary environment variables + - Make sure to add any sensitive information as secrets + +5. Commit and push your changes: +```bash +git add . +git commit -m "Initial commit" +git push +``` + +## Local Testing + +Before deploying to Hugging Face, you can test locally using Docker Compose: + +1. Build and run the container: +```bash +docker-compose up --build +``` + +2. Access the API at `http://localhost:7860` + +## Important Notes + +1. The Dockerfile uses port 7860 as required by Hugging Face Spaces +2. Make sure your database is properly initialized before deployment +3. Static files and images should be stored in the appropriate directories +4. Any changes to the code require a new commit and push to update the Space + +## Monitoring + +- Monitor your Space's logs through the Hugging Face interface +- Check the Space's metrics for usage statistics +- Use the Space's terminal for debugging if needed + +## Troubleshooting + +If you encounter issues: +1. Check the Space's logs for error messages +2. Verify all environment variables are set correctly +3. Ensure all required files are present in the repository +4. Test the application locally using Docker Compose before deploying \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..cce6ac73bf288651726627a9a43d219ce6925eff Binary files /dev/null and b/README.md differ diff --git a/app/__pycache__/database.cpython-311.pyc b/app/__pycache__/database.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be440bdebbba8b8985b1a290972b3d04331f54d6 Binary files /dev/null and b/app/__pycache__/database.cpython-311.pyc differ diff --git a/app/__pycache__/database.cpython-312.pyc b/app/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..465305b1835dfa62adaccd41df6d1b94021cb565 Binary files /dev/null and b/app/__pycache__/database.cpython-312.pyc differ diff --git a/app/__pycache__/database.cpython-313.pyc b/app/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e2db9941c86bfd0e670a78a6f022dc08bb70ebe4 Binary files /dev/null and b/app/__pycache__/database.cpython-313.pyc differ diff --git a/app/__pycache__/firebase_config.cpython-312.pyc b/app/__pycache__/firebase_config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8affaf59be671fe2bd7f485f21da3df364dd068b Binary files /dev/null and b/app/__pycache__/firebase_config.cpython-312.pyc differ diff --git a/app/__pycache__/main.cpython-311.pyc b/app/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c4fe82f7ff791af0c9b135b182b02592e91365e Binary files /dev/null and b/app/__pycache__/main.cpython-311.pyc differ diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89286aafd750e5f9ef16f61d651af0bb4e5e72a6 Binary files /dev/null and b/app/__pycache__/main.cpython-312.pyc differ diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99ed8154f3b311e7fd29d6e4768941df9ce4e831 Binary files /dev/null and b/app/__pycache__/main.cpython-313.pyc differ diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000000000000000000000000000000000000..f4385858faa932f8db8a00f7c63d91ad900f85cc --- /dev/null +++ b/app/database.py @@ -0,0 +1,204 @@ +from sqlalchemy import ( + create_engine, + Column, + Integer, + String, + Float, + ForeignKey, + DateTime, + Text, + Boolean, +) +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, relationship +from datetime import datetime, timezone +import os + +# Database connection - Using SQLite +DATABASE_URL = "sqlite:///./tabble_new.db" # Using the new database with offers feature +engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + + +# Database models +class Dish(Base): + __tablename__ = "dishes" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, index=True) + description = Column(Text, nullable=True) + category = Column(String, index=True) + price = Column(Float) + quantity = Column(Integer, default=0) + image_path = Column(String, nullable=True) + discount = Column(Float, default=0) # Discount amount (percentage) + is_offer = Column(Integer, default=0) # 0 = not an offer, 1 = is an offer + is_special = Column(Integer, default=0) # 0 = not special, 1 = today's special + created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + updated_at = Column( + DateTime, + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + ) + + # Relationship with OrderItem + order_items = relationship("OrderItem", back_populates="dish") + + +class Order(Base): + __tablename__ = "orders" + + id = Column(Integer, primary_key=True, index=True) + table_number = Column(Integer) + unique_id = Column(String, index=True) + person_id = Column(Integer, ForeignKey("persons.id"), nullable=True) + status = Column(String, default="pending") # pending, completed, paid + created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + updated_at = Column( + DateTime, + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + ) + + # Relationships + items = relationship("OrderItem", back_populates="order") + person = relationship("Person", back_populates="orders") + + +class Person(Base): + __tablename__ = "persons" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String, unique=True, index=True) + password = Column(String) + phone_number = Column(String, unique=True, index=True, nullable=True) # Added phone number field + visit_count = Column(Integer, default=1) + last_visit = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + + # Relationship with Order + orders = relationship("Order", back_populates="person") + + +class OrderItem(Base): + __tablename__ = "order_items" + + id = Column(Integer, primary_key=True, index=True) + order_id = Column(Integer, ForeignKey("orders.id")) + dish_id = Column(Integer, ForeignKey("dishes.id")) + quantity = Column(Integer, default=1) + remarks = Column(Text, nullable=True) + created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + + # Relationships + order = relationship("Order", back_populates="items") + dish = relationship("Dish", back_populates="order_items") + + +class Feedback(Base): + __tablename__ = "feedback" + + id = Column(Integer, primary_key=True, index=True) + order_id = Column(Integer, ForeignKey("orders.id")) + person_id = Column(Integer, ForeignKey("persons.id"), nullable=True) + rating = Column(Integer) # 1-5 stars + comment = Column(Text, nullable=True) + created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + + # Relationships + order = relationship("Order") + person = relationship("Person") + + +class LoyaltyProgram(Base): + __tablename__ = "loyalty_program" + + id = Column(Integer, primary_key=True, index=True) + visit_count = Column(Integer, unique=True) # Number of visits required + discount_percentage = Column(Float) # Discount percentage + is_active = Column(Boolean, default=True) # Whether this loyalty tier is active + created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + updated_at = Column( + DateTime, + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + ) + + +class SelectionOffer(Base): + __tablename__ = "selection_offers" + + id = Column(Integer, primary_key=True, index=True) + min_amount = Column(Float) # Minimum order amount to qualify + discount_amount = Column(Float) # Fixed discount amount to apply + is_active = Column(Boolean, default=True) # Whether this offer is active + description = Column(String, nullable=True) # Optional description of the offer + created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + updated_at = Column( + DateTime, + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + ) + + +class Table(Base): + __tablename__ = "tables" + + id = Column(Integer, primary_key=True, index=True) + table_number = Column(Integer, unique=True) # Table number + is_occupied = Column( + Boolean, default=False + ) # Whether the table is currently occupied + current_order_id = Column( + Integer, ForeignKey("orders.id"), nullable=True + ) # Current active order + created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + updated_at = Column( + DateTime, + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + ) + + # Relationship to current order + current_order = relationship("Order", foreign_keys=[current_order_id]) + + +class Settings(Base): + __tablename__ = "settings" + + id = Column(Integer, primary_key=True, index=True) + hotel_name = Column(String, nullable=False, default="Tabble Hotel") + address = Column(String, nullable=True) + contact_number = Column(String, nullable=True) + email = Column(String, nullable=True) + tax_id = Column(String, nullable=True) + logo_path = Column(String, nullable=True) + created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + updated_at = Column( + DateTime, + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + ) + + +# Create tables +def create_tables(): + # Drop the selection_offers table if it exists to force recreation + try: + SelectionOffer.__table__.drop(engine) + print("Dropped selection_offers table to recreate it with the correct schema") + except: + print("Could not drop selection_offers table, it might not exist yet") + + # Create all tables + Base.metadata.create_all(bind=engine) + + +# Get database session +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..868b2d5ad353a9336d6d411782e721c48f5633ad --- /dev/null +++ b/app/main.py @@ -0,0 +1,126 @@ +from fastapi import FastAPI, Request, Depends +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse +from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy.orm import Session +import uvicorn +import os + +from .database import get_db, create_tables +from .routers import chef, customer, admin, feedback, loyalty, selection_offer, table, analytics, settings + +# Create FastAPI app +app = FastAPI(title="Tabble - Hotel Management App") + +# Add CORS middleware to allow cross-origin requests +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Allow all origins + allow_credentials=True, + allow_methods=["*"], # Allow all methods + allow_headers=["*"], # Allow all headers +) + +# Mount static files +app.mount("/static", StaticFiles(directory="app/static"), name="static") + +# Setup templates +templates = Jinja2Templates(directory="templates") + +# Include routers +app.include_router(chef.router) +app.include_router(customer.router) +app.include_router(admin.router) +app.include_router(feedback.router) +app.include_router(loyalty.router) +app.include_router(selection_offer.router) +app.include_router(table.router) +app.include_router(analytics.router) +app.include_router(settings.router) + +# Create database tables +create_tables() + +# Check if we have the React build folder +react_build_dir = "frontend/build" +has_react_build = os.path.isdir(react_build_dir) + +if has_react_build: + # Mount the React build folder + app.mount("/", StaticFiles(directory=react_build_dir, html=True), name="react") + + +# Root route - serve React app in production, otherwise serve index.html template +@app.get("/", response_class=HTMLResponse) +async def root(request: Request): + if has_react_build: + return FileResponse(f"{react_build_dir}/index.html") + return templates.TemplateResponse("index.html", {"request": request}) + + +# Chef page +@app.get("/chef", response_class=HTMLResponse) +async def chef_page(request: Request): + return templates.TemplateResponse("chef/index.html", {"request": request}) + + +# Chef orders page +@app.get("/chef/orders", response_class=HTMLResponse) +async def chef_orders_page(request: Request): + return templates.TemplateResponse("chef/orders.html", {"request": request}) + + +# Customer login page +@app.get("/customer", response_class=HTMLResponse) +async def customer_login_page(request: Request): + return templates.TemplateResponse("customer/login.html", {"request": request}) + + +# Customer menu page +@app.get("/customer/menu", response_class=HTMLResponse) +async def customer_menu_page(request: Request, table_number: int, unique_id: str): + return templates.TemplateResponse( + "customer/menu.html", + {"request": request, "table_number": table_number, "unique_id": unique_id}, + ) + + +# Admin page +@app.get("/admin", response_class=HTMLResponse) +async def admin_page(request: Request): + return templates.TemplateResponse("admin/index.html", {"request": request}) + + +# Admin dishes page +@app.get("/admin/dishes", response_class=HTMLResponse) +async def admin_dishes_page(request: Request): + return templates.TemplateResponse("admin/dishes.html", {"request": request}) + + +# Analysis page +@app.get("/analysis", response_class=HTMLResponse) +async def analysis_page(request: Request): + return templates.TemplateResponse("analysis/index.html", {"request": request}) + + +# Chef analysis page +@app.get("/analysis/chef", response_class=HTMLResponse) +async def chef_analysis_page(request: Request): + return templates.TemplateResponse("analysis/chef.html", {"request": request}) + + +# Customer analysis page +@app.get("/analysis/customer", response_class=HTMLResponse) +async def customer_analysis_page(request: Request): + return templates.TemplateResponse("analysis/customer.html", {"request": request}) + + +# Dish analysis page +@app.get("/analysis/dish", response_class=HTMLResponse) +async def dish_analysis_page(request: Request): + return templates.TemplateResponse("analysis/dish.html", {"request": request}) + + +if __name__ == "__main__": + uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/app/models/__pycache__/dish.cpython-311.pyc b/app/models/__pycache__/dish.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24732514414a11a563d4ad1d8bed4bd3536811b8 Binary files /dev/null and b/app/models/__pycache__/dish.cpython-311.pyc differ diff --git a/app/models/__pycache__/dish.cpython-312.pyc b/app/models/__pycache__/dish.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3a406bc3eb0ba0c37debac150e5c7d07d033997b Binary files /dev/null and b/app/models/__pycache__/dish.cpython-312.pyc differ diff --git a/app/models/__pycache__/feedback.cpython-311.pyc b/app/models/__pycache__/feedback.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3592d41a14d8ed91c3d025397a9716a612733d91 Binary files /dev/null and b/app/models/__pycache__/feedback.cpython-311.pyc differ diff --git a/app/models/__pycache__/feedback.cpython-312.pyc b/app/models/__pycache__/feedback.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d46359fa510ba640ce131179911295ce9898c1e7 Binary files /dev/null and b/app/models/__pycache__/feedback.cpython-312.pyc differ diff --git a/app/models/__pycache__/inventory.cpython-311.pyc b/app/models/__pycache__/inventory.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f536d90f0caa29c558fe24aa6061a8ba77ff27a Binary files /dev/null and b/app/models/__pycache__/inventory.cpython-311.pyc differ diff --git a/app/models/__pycache__/loyalty.cpython-311.pyc b/app/models/__pycache__/loyalty.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cfb1c0da82f18becaccbe6618f9b14afb9e4da8f Binary files /dev/null and b/app/models/__pycache__/loyalty.cpython-311.pyc differ diff --git a/app/models/__pycache__/loyalty.cpython-312.pyc b/app/models/__pycache__/loyalty.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dddeba5e04bf3f1e2848d01d8ea490184ca4330d Binary files /dev/null and b/app/models/__pycache__/loyalty.cpython-312.pyc differ diff --git a/app/models/__pycache__/order.cpython-311.pyc b/app/models/__pycache__/order.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c1fba92791761d134ea32ed0ae8c721093953bb Binary files /dev/null and b/app/models/__pycache__/order.cpython-311.pyc differ diff --git a/app/models/__pycache__/order.cpython-312.pyc b/app/models/__pycache__/order.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86d674e43f66ee76c5eca73690b410ad4e1980b2 Binary files /dev/null and b/app/models/__pycache__/order.cpython-312.pyc differ diff --git a/app/models/__pycache__/selection_offer.cpython-311.pyc b/app/models/__pycache__/selection_offer.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..378512ec01d3de1fff2b483cdf4f0bb4e9dad45d Binary files /dev/null and b/app/models/__pycache__/selection_offer.cpython-311.pyc differ diff --git a/app/models/__pycache__/selection_offer.cpython-312.pyc b/app/models/__pycache__/selection_offer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..618071848492319188fc2379e56a3cb46b154247 Binary files /dev/null and b/app/models/__pycache__/selection_offer.cpython-312.pyc differ diff --git a/app/models/__pycache__/settings.cpython-312.pyc b/app/models/__pycache__/settings.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d6b89ed0285145f33e2af7e9c5e758f97ec29727 Binary files /dev/null and b/app/models/__pycache__/settings.cpython-312.pyc differ diff --git a/app/models/__pycache__/table.cpython-311.pyc b/app/models/__pycache__/table.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76d1f240ca7aa6e861d76a5a8a24a089a676fd02 Binary files /dev/null and b/app/models/__pycache__/table.cpython-311.pyc differ diff --git a/app/models/__pycache__/table.cpython-312.pyc b/app/models/__pycache__/table.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa36cca15a3223bbaba15e81285d7e15973b096b Binary files /dev/null and b/app/models/__pycache__/table.cpython-312.pyc differ diff --git a/app/models/__pycache__/user.cpython-311.pyc b/app/models/__pycache__/user.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..62f30bcdc5a1078556c1c7beaf2221e8bf27961a Binary files /dev/null and b/app/models/__pycache__/user.cpython-311.pyc differ diff --git a/app/models/__pycache__/user.cpython-312.pyc b/app/models/__pycache__/user.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..40f4aa9dc24211250c3129e5038513ca8d378d27 Binary files /dev/null and b/app/models/__pycache__/user.cpython-312.pyc differ diff --git a/app/models/dish.py b/app/models/dish.py new file mode 100644 index 0000000000000000000000000000000000000000..c8be437c662c74c49e5cacb7222578cc0c7e4379 --- /dev/null +++ b/app/models/dish.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + +class DishBase(BaseModel): + name: str + description: Optional[str] = None + category: str + price: float + quantity: int + discount: Optional[float] = 0 + is_offer: Optional[int] = 0 + is_special: Optional[int] = 0 + +class DishCreate(DishBase): + pass + +class DishUpdate(DishBase): + name: Optional[str] = None + description: Optional[str] = None + category: Optional[str] = None + price: Optional[float] = None + quantity: Optional[int] = None + image_path: Optional[str] = None + discount: Optional[float] = None + is_offer: Optional[int] = None + is_special: Optional[int] = None + +class Dish(DishBase): + id: int + image_path: Optional[str] = None + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True # Updated from orm_mode for Pydantic V2 diff --git a/app/models/feedback.py b/app/models/feedback.py new file mode 100644 index 0000000000000000000000000000000000000000..35bf68da3166a059a1dcf708afb4538fb0d7c73c --- /dev/null +++ b/app/models/feedback.py @@ -0,0 +1,22 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + + +class FeedbackBase(BaseModel): + order_id: int + rating: int + comment: Optional[str] = None + person_id: Optional[int] = None + + +class FeedbackCreate(FeedbackBase): + pass + + +class Feedback(FeedbackBase): + id: int + created_at: datetime + + class Config: + from_attributes = True # Updated from orm_mode for Pydantic V2 diff --git a/app/models/loyalty.py b/app/models/loyalty.py new file mode 100644 index 0000000000000000000000000000000000000000..a4abc24d8b199a37fb08c6b94bcef9cdd908ca91 --- /dev/null +++ b/app/models/loyalty.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + + +class LoyaltyProgramBase(BaseModel): + visit_count: int + discount_percentage: float + is_active: bool = True + + +class LoyaltyProgramCreate(LoyaltyProgramBase): + pass + + +class LoyaltyProgramUpdate(BaseModel): + visit_count: Optional[int] = None + discount_percentage: Optional[float] = None + is_active: Optional[bool] = None + + +class LoyaltyProgram(LoyaltyProgramBase): + id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True # Updated from orm_mode for Pydantic V2 diff --git a/app/models/order.py b/app/models/order.py new file mode 100644 index 0000000000000000000000000000000000000000..8059fe0e27702e718700e0b911e0a05028c0ef3f --- /dev/null +++ b/app/models/order.py @@ -0,0 +1,60 @@ +from pydantic import BaseModel +from typing import List, Optional +from datetime import datetime +from .dish import Dish + + +class OrderItemBase(BaseModel): + dish_id: int + quantity: int = 1 + remarks: Optional[str] = None + + +class OrderItemCreate(OrderItemBase): + pass + + +class OrderItem(OrderItemBase): + id: int + order_id: int + created_at: datetime + dish: Optional[Dish] = None + + # Add dish_name property to ensure it's always available + @property + def dish_name(self) -> str: + if self.dish: + return self.dish.name + return "Unknown Dish" + + class Config: + from_attributes = True # Updated from orm_mode for Pydantic V2 + + +class OrderBase(BaseModel): + table_number: int + unique_id: str + + +class OrderCreate(OrderBase): + items: List[OrderItemCreate] + username: Optional[str] = None + password: Optional[str] = None + + +class OrderUpdate(BaseModel): + status: str + + +class Order(OrderBase): + id: int + status: str + created_at: datetime + updated_at: datetime + items: List[OrderItem] = [] + person_id: Optional[int] = None + person_name: Optional[str] = None + visit_count: Optional[int] = None + + class Config: + from_attributes = True # Updated from orm_mode for Pydantic V2 diff --git a/app/models/selection_offer.py b/app/models/selection_offer.py new file mode 100644 index 0000000000000000000000000000000000000000..2e29dd93e937abc2e06c2a5ad1eb0caaee73e5a8 --- /dev/null +++ b/app/models/selection_offer.py @@ -0,0 +1,30 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + + +class SelectionOfferBase(BaseModel): + min_amount: float + discount_amount: float + is_active: bool = True + description: Optional[str] = None + + +class SelectionOfferCreate(SelectionOfferBase): + pass + + +class SelectionOfferUpdate(BaseModel): + min_amount: Optional[float] = None + discount_amount: Optional[float] = None + is_active: Optional[bool] = None + description: Optional[str] = None + + +class SelectionOffer(SelectionOfferBase): + id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True # Updated from orm_mode for Pydantic V2 diff --git a/app/models/settings.py b/app/models/settings.py new file mode 100644 index 0000000000000000000000000000000000000000..45bc272a586c4d6bb929298e887e47f01ce6595f --- /dev/null +++ b/app/models/settings.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + + +class SettingsBase(BaseModel): + hotel_name: str + address: Optional[str] = None + contact_number: Optional[str] = None + email: Optional[str] = None + tax_id: Optional[str] = None + logo_path: Optional[str] = None + + +class SettingsCreate(SettingsBase): + pass + + +class SettingsUpdate(BaseModel): + hotel_name: Optional[str] = None + address: Optional[str] = None + contact_number: Optional[str] = None + email: Optional[str] = None + tax_id: Optional[str] = None + logo_path: Optional[str] = None + + +class Settings(SettingsBase): + id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True # Updated from orm_mode for Pydantic V2 diff --git a/app/models/table.py b/app/models/table.py new file mode 100644 index 0000000000000000000000000000000000000000..4527ce974c805025892e00713227eeb02e87f1e1 --- /dev/null +++ b/app/models/table.py @@ -0,0 +1,33 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + + +class TableBase(BaseModel): + table_number: int + is_occupied: bool = False + current_order_id: Optional[int] = None + + +class TableCreate(TableBase): + pass + + +class TableUpdate(BaseModel): + is_occupied: Optional[bool] = None + current_order_id: Optional[int] = None + + +class Table(TableBase): + id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True # Updated from orm_mode for Pydantic V2 + + +class TableStatus(BaseModel): + total_tables: int + occupied_tables: int + free_tables: int diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000000000000000000000000000000000000..ac03ae35d3e98ed599fa2795a40dd8a6ee830c17 --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,39 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + +class PersonBase(BaseModel): + username: str + +class PersonCreate(PersonBase): + password: str + table_number: int + phone_number: Optional[str] = None + +class PersonLogin(PersonBase): + password: str + table_number: int + +class PhoneAuthRequest(BaseModel): + phone_number: str + table_number: int + +class PhoneVerifyRequest(BaseModel): + phone_number: str + verification_code: str + table_number: int + +class UsernameRequest(BaseModel): + phone_number: str + username: str + table_number: int + +class Person(PersonBase): + id: int + visit_count: int + last_visit: datetime + created_at: datetime + phone_number: Optional[str] = None + + class Config: + from_attributes = True # Updated from orm_mode for Pydantic V2 diff --git a/app/routers/__pycache__/admin.cpython-311.pyc b/app/routers/__pycache__/admin.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a436f5e3e871a32d3ed2886e38610ca5d3ccb9e3 Binary files /dev/null and b/app/routers/__pycache__/admin.cpython-311.pyc differ diff --git a/app/routers/__pycache__/admin.cpython-312.pyc b/app/routers/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8877ab529c218e38ab8e32bd3af0b237bbb318eb Binary files /dev/null and b/app/routers/__pycache__/admin.cpython-312.pyc differ diff --git a/app/routers/__pycache__/analytics.cpython-312.pyc b/app/routers/__pycache__/analytics.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..10b3974306c22ef9d2782076e64edffdb92e2650 Binary files /dev/null and b/app/routers/__pycache__/analytics.cpython-312.pyc differ diff --git a/app/routers/__pycache__/chef.cpython-311.pyc b/app/routers/__pycache__/chef.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e134a38d2f29653da18cdf8535d286bda8228063 Binary files /dev/null and b/app/routers/__pycache__/chef.cpython-311.pyc differ diff --git a/app/routers/__pycache__/chef.cpython-312.pyc b/app/routers/__pycache__/chef.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..34ecbbb92fd061e6cd5de7cfa358e3f0098f7808 Binary files /dev/null and b/app/routers/__pycache__/chef.cpython-312.pyc differ diff --git a/app/routers/__pycache__/customer.cpython-311.pyc b/app/routers/__pycache__/customer.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fba4133c49ae1cda7acbfc84948b108fb6953915 Binary files /dev/null and b/app/routers/__pycache__/customer.cpython-311.pyc differ diff --git a/app/routers/__pycache__/customer.cpython-312.pyc b/app/routers/__pycache__/customer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00734cb94199e0d7b5a393a610f6be4799d74514 Binary files /dev/null and b/app/routers/__pycache__/customer.cpython-312.pyc differ diff --git a/app/routers/__pycache__/feedback.cpython-311.pyc b/app/routers/__pycache__/feedback.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3cd4ca42596d2567369342f27977ae80eff57899 Binary files /dev/null and b/app/routers/__pycache__/feedback.cpython-311.pyc differ diff --git a/app/routers/__pycache__/feedback.cpython-312.pyc b/app/routers/__pycache__/feedback.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..46ab25bdb209ddaad2dcd12cfe42e36379e16079 Binary files /dev/null and b/app/routers/__pycache__/feedback.cpython-312.pyc differ diff --git a/app/routers/__pycache__/loyalty.cpython-311.pyc b/app/routers/__pycache__/loyalty.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..10d9ac7394271a01eadb581413d65d2825ae7c5e Binary files /dev/null and b/app/routers/__pycache__/loyalty.cpython-311.pyc differ diff --git a/app/routers/__pycache__/loyalty.cpython-312.pyc b/app/routers/__pycache__/loyalty.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3fb01b38096d5d488624b653b223d7f3bc6739e2 Binary files /dev/null and b/app/routers/__pycache__/loyalty.cpython-312.pyc differ diff --git a/app/routers/__pycache__/selection_offer.cpython-311.pyc b/app/routers/__pycache__/selection_offer.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b65daffea2d6716930ef5e0ef22a55cead7e36f1 Binary files /dev/null and b/app/routers/__pycache__/selection_offer.cpython-311.pyc differ diff --git a/app/routers/__pycache__/selection_offer.cpython-312.pyc b/app/routers/__pycache__/selection_offer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c90d1d0d042795cd64cbb2ed28118e8c443af24 Binary files /dev/null and b/app/routers/__pycache__/selection_offer.cpython-312.pyc differ diff --git a/app/routers/__pycache__/settings.cpython-312.pyc b/app/routers/__pycache__/settings.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f48380afe1b052e099d584c38cc9fff0812d4e5 Binary files /dev/null and b/app/routers/__pycache__/settings.cpython-312.pyc differ diff --git a/app/routers/__pycache__/table.cpython-311.pyc b/app/routers/__pycache__/table.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c3bec4b0eb8e730af0c4c5e690fe9d702b72089 Binary files /dev/null and b/app/routers/__pycache__/table.cpython-311.pyc differ diff --git a/app/routers/__pycache__/table.cpython-312.pyc b/app/routers/__pycache__/table.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..922752d0cea400a8987cb44cdb50790a4a794616 Binary files /dev/null and b/app/routers/__pycache__/table.cpython-312.pyc differ diff --git a/app/routers/admin.py b/app/routers/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..21f24756fa070cf0e7d75f1a19d1829dc8d31834 --- /dev/null +++ b/app/routers/admin.py @@ -0,0 +1,423 @@ +from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session +from typing import List, Optional +import os +import shutil +from datetime import datetime, timezone +from ..utils.pdf_generator import generate_bill_pdf, generate_multi_order_bill_pdf +from pydantic import BaseModel + +from ..database import get_db, Order, Dish, OrderItem, Person, Settings +from ..models.order import Order as OrderModel +from ..models.dish import Dish as DishModel, DishCreate, DishUpdate + +router = APIRouter( + prefix="/admin", + tags=["admin"], + responses={404: {"description": "Not found"}}, +) + + +# Get all orders with customer information +@router.get("/orders", response_model=List[OrderModel]) +def get_all_orders(status: str = None, db: Session = Depends(get_db)): + query = db.query(Order) + + if status: + query = query.filter(Order.status == status) + + # Order by most recent first + orders = query.order_by(Order.created_at.desc()).all() + + # Load person information for each order + for order in orders: + if order.person_id: + person = db.query(Person).filter(Person.id == order.person_id).first() + if person: + # Add person information to the order + order.person_name = person.username + order.visit_count = person.visit_count + + # Load dish information for each order item + for item in order.items: + if not hasattr(item, "dish") or item.dish is None: + dish = db.query(Dish).filter(Dish.id == item.dish_id).first() + if dish: + item.dish = dish + + return orders + + +# Get all dishes +@router.get("/api/dishes", response_model=List[DishModel]) +def get_all_dishes( + is_offer: Optional[int] = None, + is_special: Optional[int] = None, + db: Session = Depends(get_db), +): + query = db.query(Dish) + + if is_offer is not None: + query = query.filter(Dish.is_offer == is_offer) + + if is_special is not None: + query = query.filter(Dish.is_special == is_special) + + dishes = query.all() + return dishes + + +# Get offer dishes +@router.get("/api/offers", response_model=List[DishModel]) +def get_offer_dishes(db: Session = Depends(get_db)): + dishes = db.query(Dish).filter(Dish.is_offer == 1).all() + return dishes + + +# Get special dishes +@router.get("/api/specials", response_model=List[DishModel]) +def get_special_dishes(db: Session = Depends(get_db)): + dishes = db.query(Dish).filter(Dish.is_special == 1).all() + return dishes + + +# Get dish by ID +@router.get("/api/dishes/{dish_id}", response_model=DishModel) +def get_dish(dish_id: int, db: Session = Depends(get_db)): + dish = db.query(Dish).filter(Dish.id == dish_id).first() + if dish is None: + raise HTTPException(status_code=404, detail="Dish not found") + return dish + + +# Get all categories +@router.get("/api/categories") +def get_all_categories(db: Session = Depends(get_db)): + categories = db.query(Dish.category).distinct().all() + return [category[0] for category in categories] + + +# Create new category +@router.post("/api/categories") +def create_category(category_name: str = Form(...), db: Session = Depends(get_db)): + # Check if category already exists + existing_category = ( + db.query(Dish.category).filter(Dish.category == category_name).first() + ) + if existing_category: + raise HTTPException(status_code=400, detail="Category already exists") + + return {"message": "Category created successfully", "category": category_name} + + +# Create new dish +@router.post("/api/dishes", response_model=DishModel) +async def create_dish( + name: str = Form(...), + description: Optional[str] = Form(None), + category: str = Form(...), + new_category: Optional[str] = Form(None), # New field for custom category + price: float = Form(...), + quantity: int = Form(...), + discount: Optional[float] = Form(0), # Discount amount (percentage) + is_offer: Optional[int] = Form(0), # Whether this dish is part of offers + is_special: Optional[int] = Form(0), # Whether this dish is today's special + image: Optional[UploadFile] = File(None), + db: Session = Depends(get_db), +): + # Use new category if provided, otherwise use selected category + final_category = new_category if new_category else category + + # Create dish object + db_dish = Dish( + name=name, + description=description, + category=final_category, + price=price, + quantity=quantity, + discount=discount, + is_offer=is_offer, + is_special=is_special, + ) + + # Save dish to database + db.add(db_dish) + db.commit() + db.refresh(db_dish) + + # Handle image upload if provided + if image: + # Create directory if it doesn't exist + os.makedirs("app/static/images/dishes", exist_ok=True) + + # Save image + image_path = f"app/static/images/dishes/{db_dish.id}_{image.filename}" + with open(image_path, "wb") as buffer: + shutil.copyfileobj(image.file, buffer) + + # Update dish with image path + db_dish.image_path = f"/static/images/dishes/{db_dish.id}_{image.filename}" + db.commit() + db.refresh(db_dish) + + return db_dish + + +# Update dish +@router.put("/api/dishes/{dish_id}", response_model=DishModel) +async def update_dish( + dish_id: int, + name: Optional[str] = Form(None), + description: Optional[str] = Form(None), + category: Optional[str] = Form(None), + new_category: Optional[str] = Form(None), # New field for custom category + price: Optional[float] = Form(None), + quantity: Optional[int] = Form(None), + discount: Optional[float] = Form(None), # Discount amount (percentage) + is_offer: Optional[int] = Form(None), # Whether this dish is part of offers + is_special: Optional[int] = Form(None), # Whether this dish is today's special + image: Optional[UploadFile] = File(None), + db: Session = Depends(get_db), +): + # Get existing dish + db_dish = db.query(Dish).filter(Dish.id == dish_id).first() + if db_dish is None: + raise HTTPException(status_code=404, detail="Dish not found") + + # Update fields if provided + if name: + db_dish.name = name + if description: + db_dish.description = description + if new_category: # Use new category if provided + db_dish.category = new_category + elif category: + db_dish.category = category + if price: + db_dish.price = price + if quantity: + db_dish.quantity = quantity + if discount is not None: + db_dish.discount = discount + if is_offer is not None: + db_dish.is_offer = is_offer + if is_special is not None: + db_dish.is_special = is_special + + # Handle image upload if provided + if image: + # Create directory if it doesn't exist + os.makedirs("app/static/images/dishes", exist_ok=True) + + # Save image + image_path = f"app/static/images/dishes/{db_dish.id}_{image.filename}" + with open(image_path, "wb") as buffer: + shutil.copyfileobj(image.file, buffer) + + # Update dish with image path + db_dish.image_path = f"/static/images/dishes/{db_dish.id}_{image.filename}" + + # Update timestamp + db_dish.updated_at = datetime.now(timezone.utc) + + # Save changes + db.commit() + db.refresh(db_dish) + + return db_dish + + +# Delete dish +@router.delete("/api/dishes/{dish_id}") +def delete_dish(dish_id: int, db: Session = Depends(get_db)): + db_dish = db.query(Dish).filter(Dish.id == dish_id).first() + if db_dish is None: + raise HTTPException(status_code=404, detail="Dish not found") + + db.delete(db_dish) + db.commit() + + return {"message": "Dish deleted successfully"} + + +# Get order statistics +@router.get("/stats/orders") +def get_order_stats(db: Session = Depends(get_db)): + total_orders = db.query(Order).count() + pending_orders = db.query(Order).filter(Order.status == "pending").count() + completed_orders = db.query(Order).filter(Order.status == "completed").count() + payment_requested = ( + db.query(Order).filter(Order.status == "payment_requested").count() + ) + paid_orders = db.query(Order).filter(Order.status == "paid").count() + + return { + "total_orders": total_orders, + "pending_orders": pending_orders, + "completed_orders": completed_orders, + "payment_requested": payment_requested, + "paid_orders": paid_orders, + } + + +# Mark order as paid +@router.put("/orders/{order_id}/paid") +def mark_order_paid(order_id: int, db: Session = Depends(get_db)): + db_order = db.query(Order).filter(Order.id == order_id).first() + if db_order is None: + raise HTTPException(status_code=404, detail="Order not found") + + # Allow marking as paid from any status + db_order.status = "paid" + db_order.updated_at = datetime.now(timezone.utc) + + db.commit() + + return {"message": "Order marked as paid"} + + +# Generate bill PDF for a single order +@router.get("/orders/{order_id}/bill") +def generate_bill(order_id: int, db: Session = Depends(get_db)): + # Get order with all details + db_order = db.query(Order).filter(Order.id == order_id).first() + if db_order is None: + raise HTTPException(status_code=404, detail="Order not found") + + # Load person information if available + if db_order.person_id: + person = db.query(Person).filter(Person.id == db_order.person_id).first() + if person: + db_order.person_name = person.username + + # Load dish information for each order item + for item in db_order.items: + if not hasattr(item, "dish") or item.dish is None: + dish = db.query(Dish).filter(Dish.id == item.dish_id).first() + if dish: + item.dish = dish + + # Get hotel settings + settings = db.query(Settings).first() + if not settings: + # Create default settings if none exist + settings = Settings( + hotel_name="Tabble Hotel", + address="123 Main Street, City", + contact_number="+1 123-456-7890", + email="info@tabblehotel.com", + ) + db.add(settings) + db.commit() + db.refresh(settings) + + # Generate PDF + pdf_buffer = generate_bill_pdf(db_order, settings) + + # Return PDF as a downloadable file + filename = f"bill_order_{order_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}.pdf" + + return StreamingResponse( + pdf_buffer, + media_type="application/pdf", + headers={"Content-Disposition": f"attachment; filename={filename}"} + ) + + +# Generate bill PDF for multiple orders +@router.post("/orders/multi-bill") +def generate_multi_bill(order_ids: List[int], db: Session = Depends(get_db)): + if not order_ids: + raise HTTPException(status_code=400, detail="No order IDs provided") + + orders = [] + + # Get all orders with details + for order_id in order_ids: + db_order = db.query(Order).filter(Order.id == order_id).first() + if db_order is None: + raise HTTPException(status_code=404, detail=f"Order {order_id} not found") + + # Load person information if available + if db_order.person_id: + person = db.query(Person).filter(Person.id == db_order.person_id).first() + if person: + db_order.person_name = person.username + + # Load dish information for each order item + for item in db_order.items: + if not hasattr(item, "dish") or item.dish is None: + dish = db.query(Dish).filter(Dish.id == item.dish_id).first() + if dish: + item.dish = dish + + orders.append(db_order) + + # Get hotel settings + settings = db.query(Settings).first() + if not settings: + # Create default settings if none exist + settings = Settings( + hotel_name="Tabble Hotel", + address="123 Main Street, City", + contact_number="+1 123-456-7890", + email="info@tabblehotel.com", + ) + db.add(settings) + db.commit() + db.refresh(settings) + + # Generate PDF for multiple orders + pdf_buffer = generate_multi_order_bill_pdf(orders, settings) + + # Create a filename with all order IDs + order_ids_str = "-".join([str(order_id) for order_id in order_ids]) + filename = f"bill_orders_{order_ids_str}_{datetime.now().strftime('%Y%m%d%H%M%S')}.pdf" + + return StreamingResponse( + pdf_buffer, + media_type="application/pdf", + headers={"Content-Disposition": f"attachment; filename={filename}"} + ) + + +# Merge two orders +@router.post("/orders/merge") +def merge_orders(source_order_id: int, target_order_id: int, db: Session = Depends(get_db)): + # Get both orders + source_order = db.query(Order).filter(Order.id == source_order_id).first() + target_order = db.query(Order).filter(Order.id == target_order_id).first() + + if not source_order: + raise HTTPException(status_code=404, detail=f"Source order {source_order_id} not found") + + if not target_order: + raise HTTPException(status_code=404, detail=f"Target order {target_order_id} not found") + + # Check if both orders are completed or paid + valid_statuses = ["completed", "paid"] + if source_order.status not in valid_statuses: + raise HTTPException(status_code=400, detail=f"Source order must be completed or paid, current status: {source_order.status}") + + if target_order.status not in valid_statuses: + raise HTTPException(status_code=400, detail=f"Target order must be completed or paid, current status: {target_order.status}") + + # Move all items from source order to target order + for item in source_order.items: + # Update the order_id to point to the target order + item.order_id = target_order.id + + # Update the target order's updated_at timestamp + target_order.updated_at = datetime.now(timezone.utc) + + # Delete the source order (but keep its items which now belong to the target order) + db.delete(source_order) + + # Commit changes + db.commit() + + # Refresh the target order to include the new items + db.refresh(target_order) + + return {"message": f"Orders merged successfully. Items from order #{source_order_id} have been moved to order #{target_order_id}"} diff --git a/app/routers/analytics.py b/app/routers/analytics.py new file mode 100644 index 0000000000000000000000000000000000000000..acaa9e7813647afc0c4fa838925726d1f2f6d555 --- /dev/null +++ b/app/routers/analytics.py @@ -0,0 +1,563 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from sqlalchemy import func, desc, extract +from typing import List, Dict, Any +from datetime import datetime, timedelta, timezone +import calendar + +from ..database import get_db, Dish, Order, OrderItem, Person, Table, Feedback +from ..models.dish import Dish as DishModel +from ..models.order import Order as OrderModel +from ..models.user import Person as PersonModel +from ..models.feedback import Feedback as FeedbackModel + +router = APIRouter( + prefix="/analytics", + tags=["analytics"], + responses={404: {"description": "Not found"}}, +) + + +# Get overall dashboard statistics +@router.get("/dashboard") +def get_dashboard_stats( + start_date: str = None, + end_date: str = None, + db: Session = Depends(get_db) +): + # Parse date strings to datetime objects if provided + start_datetime = None + end_datetime = None + + if start_date: + try: + start_datetime = datetime.fromisoformat(start_date.replace('Z', '+00:00')) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid start_date format. Use ISO format (YYYY-MM-DDTHH:MM:SS)") + + if end_date: + try: + end_datetime = datetime.fromisoformat(end_date.replace('Z', '+00:00')) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid end_date format. Use ISO format (YYYY-MM-DDTHH:MM:SS)") + + # Base query for orders + orders_query = db.query(Order) + + # Apply date filters if provided + if start_datetime: + orders_query = orders_query.filter(Order.created_at >= start_datetime) + + if end_datetime: + orders_query = orders_query.filter(Order.created_at <= end_datetime) + + # Total sales + total_sales_query = ( + db.query( + func.sum(Dish.price * OrderItem.quantity).label("total_sales") + ) + .join(OrderItem, Dish.id == OrderItem.dish_id) + .join(Order, OrderItem.order_id == Order.id) + .filter(Order.status == "paid") + ) + + # Apply date filters to sales query + if start_datetime: + total_sales_query = total_sales_query.filter(Order.created_at >= start_datetime) + + if end_datetime: + total_sales_query = total_sales_query.filter(Order.created_at <= end_datetime) + + total_sales_result = total_sales_query.first() + total_sales = total_sales_result.total_sales if total_sales_result.total_sales else 0 + + # Total customers (only count those who placed orders in the date range) + if start_datetime or end_datetime: + # Get unique person_ids from filtered orders + person_subquery = orders_query.with_entities(Order.person_id).distinct().subquery() + total_customers = db.query(Person).filter(Person.id.in_(person_subquery)).count() + else: + total_customers = db.query(Person).count() + + # Total orders + total_orders = orders_query.count() + + # Total dishes + total_dishes = db.query(Dish).count() + + # Average order value + avg_order_value_query = ( + db.query( + func.avg( + db.query(func.sum(Dish.price * OrderItem.quantity)) + .join(OrderItem, Dish.id == OrderItem.dish_id) + .filter(OrderItem.order_id == Order.id) + .scalar_subquery() + ).label("avg_order_value") + ) + .filter(Order.status == "paid") + ) + + # Apply date filters to avg order value query + if start_datetime: + avg_order_value_query = avg_order_value_query.filter(Order.created_at >= start_datetime) + + if end_datetime: + avg_order_value_query = avg_order_value_query.filter(Order.created_at <= end_datetime) + + avg_order_value_result = avg_order_value_query.first() + avg_order_value = avg_order_value_result.avg_order_value if avg_order_value_result.avg_order_value else 0 + + # Return all stats + return { + "total_sales": round(total_sales, 2), + "total_customers": total_customers, + "total_orders": total_orders, + "total_dishes": total_dishes, + "avg_order_value": round(avg_order_value, 2), + "date_range": { + "start_date": start_date, + "end_date": end_date + } + } + + +# Get top customers by order count +@router.get("/top-customers") +def get_top_customers(limit: int = 10, db: Session = Depends(get_db)): + # Get customers with most orders + top_customers_by_orders = ( + db.query( + Person.id, + Person.username, + Person.visit_count, + Person.last_visit, + func.count(Order.id).label("order_count"), + func.sum( + db.query(func.sum(Dish.price * OrderItem.quantity)) + .join(OrderItem, Dish.id == OrderItem.dish_id) + .filter(OrderItem.order_id == Order.id) + .scalar_subquery() + ).label("total_spent"), + ) + .join(Order, Person.id == Order.person_id) + .group_by(Person.id) + .order_by(desc("order_count")) + .limit(limit) + .all() + ) + + # Format the results + result = [] + for customer in top_customers_by_orders: + result.append({ + "id": customer.id, + "username": customer.username, + "visit_count": customer.visit_count, + "last_visit": customer.last_visit, + "order_count": customer.order_count, + "total_spent": round(customer.total_spent, 2) if customer.total_spent else 0, + "avg_order_value": round(customer.total_spent / customer.order_count, 2) if customer.total_spent else 0, + }) + + return result + + +# Get top selling dishes +@router.get("/top-dishes") +def get_top_dishes(limit: int = 10, db: Session = Depends(get_db)): + # Get dishes with most orders + top_dishes = ( + db.query( + Dish.id, + Dish.name, + Dish.category, + Dish.price, + func.sum(OrderItem.quantity).label("total_ordered"), + func.sum(Dish.price * OrderItem.quantity).label("total_revenue"), + ) + .join(OrderItem, Dish.id == OrderItem.dish_id) + .join(Order, OrderItem.order_id == Order.id) + .filter(Order.status == "paid") + .group_by(Dish.id) + .order_by(desc("total_ordered")) + .limit(limit) + .all() + ) + + # Format the results + result = [] + for dish in top_dishes: + result.append({ + "id": dish.id, + "name": dish.name, + "category": dish.category, + "price": dish.price, + "total_ordered": dish.total_ordered, + "total_revenue": round(dish.total_revenue, 2), + }) + + return result + + +# Get sales by category +@router.get("/sales-by-category") +def get_sales_by_category(db: Session = Depends(get_db)): + # Get sales by category + sales_by_category = ( + db.query( + Dish.category, + func.sum(OrderItem.quantity).label("total_ordered"), + func.sum(Dish.price * OrderItem.quantity).label("total_revenue"), + ) + .join(OrderItem, Dish.id == OrderItem.dish_id) + .join(Order, OrderItem.order_id == Order.id) + .filter(Order.status == "paid") + .group_by(Dish.category) + .order_by(desc("total_revenue")) + .all() + ) + + # Format the results + result = [] + for category in sales_by_category: + result.append({ + "category": category.category, + "total_ordered": category.total_ordered, + "total_revenue": round(category.total_revenue, 2), + }) + + return result + + +# Get sales over time (daily for the last 30 days) +@router.get("/sales-over-time") +def get_sales_over_time(days: int = 30, db: Session = Depends(get_db)): + # Calculate the date range + end_date = datetime.now(timezone.utc) + start_date = end_date - timedelta(days=days) + + # Get sales by day + sales_by_day = ( + db.query( + func.date(Order.created_at).label("date"), + func.count(Order.id).label("order_count"), + func.sum( + db.query(func.sum(Dish.price * OrderItem.quantity)) + .join(OrderItem, Dish.id == OrderItem.dish_id) + .filter(OrderItem.order_id == Order.id) + .scalar_subquery() + ).label("total_sales"), + ) + .filter(Order.status == "paid") + .filter(Order.created_at >= start_date) + .filter(Order.created_at <= end_date) + .group_by(func.date(Order.created_at)) + .order_by(func.date(Order.created_at)) + .all() + ) + + # Create a dictionary with all dates in the range + date_range = {} + current_date = start_date + while current_date <= end_date: + date_str = current_date.strftime("%Y-%m-%d") + date_range[date_str] = {"order_count": 0, "total_sales": 0} + current_date += timedelta(days=1) + + # Fill in the actual data + for day in sales_by_day: + date_str = day.date.strftime("%Y-%m-%d") if isinstance(day.date, datetime) else day.date + date_range[date_str] = { + "order_count": day.order_count, + "total_sales": round(day.total_sales, 2) if day.total_sales else 0, + } + + # Convert to list format + result = [] + for date_str, data in date_range.items(): + result.append({ + "date": date_str, + "order_count": data["order_count"], + "total_sales": data["total_sales"], + }) + + return result + + +# Get chef performance metrics +@router.get("/chef-performance") +def get_chef_performance(days: int = 30, db: Session = Depends(get_db)): + # Calculate the date range + end_date = datetime.now(timezone.utc) + start_date = end_date - timedelta(days=days) + + # Get completed orders count and average time to complete + completed_orders = ( + db.query(Order) + .filter(Order.status.in_(["completed", "paid"])) + .filter(Order.created_at >= start_date) + .filter(Order.created_at <= end_date) + .all() + ) + + total_completed = len(completed_orders) + + # Calculate average items per order + avg_items_per_order_query = ( + db.query( + func.avg( + db.query(func.count(OrderItem.id)) + .filter(OrderItem.order_id == Order.id) + .scalar_subquery() + ).label("avg_items") + ) + .filter(Order.status.in_(["completed", "paid"])) + .filter(Order.created_at >= start_date) + .filter(Order.created_at <= end_date) + .first() + ) + + avg_items_per_order = avg_items_per_order_query.avg_items if avg_items_per_order_query.avg_items else 0 + + # Get busiest day of week + busiest_day_query = ( + db.query( + extract('dow', Order.created_at).label("day_of_week"), + func.count(Order.id).label("order_count") + ) + .filter(Order.created_at >= start_date) + .filter(Order.created_at <= end_date) + .group_by(extract('dow', Order.created_at)) + .order_by(desc("order_count")) + .first() + ) + + busiest_day = None + if busiest_day_query: + # Convert day number to day name (0 = Sunday, 1 = Monday, etc.) + day_names = list(calendar.day_name) + day_number = int(busiest_day_query.day_of_week) + busiest_day = day_names[day_number] + + return { + "total_completed_orders": total_completed, + "avg_items_per_order": round(avg_items_per_order, 2), + "busiest_day": busiest_day, + } + + +# Get table utilization statistics +@router.get("/table-utilization") +def get_table_utilization(db: Session = Depends(get_db)): + # Get all tables + tables = db.query(Table).all() + + # Get order count by table + table_orders = ( + db.query( + Order.table_number, + func.count(Order.id).label("order_count"), + func.sum( + db.query(func.sum(Dish.price * OrderItem.quantity)) + .join(OrderItem, Dish.id == OrderItem.dish_id) + .filter(OrderItem.order_id == Order.id) + .scalar_subquery() + ).label("total_revenue"), + ) + .group_by(Order.table_number) + .all() + ) + + # Create a dictionary with all tables + table_stats = {} + for table in tables: + table_stats[table.table_number] = { + "table_number": table.table_number, + "is_occupied": table.is_occupied, + "order_count": 0, + "total_revenue": 0, + } + + # Fill in the actual data + for table in table_orders: + if table.table_number in table_stats: + table_stats[table.table_number]["order_count"] = table.order_count + table_stats[table.table_number]["total_revenue"] = round(table.total_revenue, 2) if table.total_revenue else 0 + + # Convert to list format + result = list(table_stats.values()) + + # Sort by order count (descending) + result.sort(key=lambda x: x["order_count"], reverse=True) + + return result + + +# Get customer visit frequency analysis +@router.get("/customer-frequency") +def get_customer_frequency( + start_date: str = None, + end_date: str = None, + db: Session = Depends(get_db) +): + # Parse date strings to datetime objects if provided + start_datetime = None + end_datetime = None + + if start_date: + try: + start_datetime = datetime.fromisoformat(start_date.replace('Z', '+00:00')) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid start_date format. Use ISO format (YYYY-MM-DDTHH:MM:SS)") + + if end_date: + try: + end_datetime = datetime.fromisoformat(end_date.replace('Z', '+00:00')) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid end_date format. Use ISO format (YYYY-MM-DDTHH:MM:SS)") + + # Get visit count distribution + visit_counts_query = db.query(Person.visit_count) + + # Apply date filters if provided + if start_datetime or end_datetime: + # Get person IDs who placed orders in the date range + orders_query = db.query(Order.person_id).distinct() + + if start_datetime: + orders_query = orders_query.filter(Order.created_at >= start_datetime) + + if end_datetime: + orders_query = orders_query.filter(Order.created_at <= end_datetime) + + person_ids = [result[0] for result in orders_query.all() if result[0] is not None] + visit_counts_query = visit_counts_query.filter(Person.id.in_(person_ids)) + + visit_counts = visit_counts_query.all() + + # Create frequency buckets + frequency_buckets = { + "1 visit": 0, + "2-3 visits": 0, + "4-5 visits": 0, + "6-10 visits": 0, + "11+ visits": 0, + } + + # Fill the buckets + for visit in visit_counts: + count = visit.visit_count + if count == 1: + frequency_buckets["1 visit"] += 1 + elif 2 <= count <= 3: + frequency_buckets["2-3 visits"] += 1 + elif 4 <= count <= 5: + frequency_buckets["4-5 visits"] += 1 + elif 6 <= count <= 10: + frequency_buckets["6-10 visits"] += 1 + else: + frequency_buckets["11+ visits"] += 1 + + # Convert to list format + result = [] + for bucket, count in frequency_buckets.items(): + result.append({ + "frequency": bucket, + "customer_count": count, + }) + + return result + + +# Get feedback analysis +@router.get("/feedback-analysis") +def get_feedback_analysis( + start_date: str = None, + end_date: str = None, + db: Session = Depends(get_db) +): + # Parse date strings to datetime objects if provided + start_datetime = None + end_datetime = None + + if start_date: + try: + start_datetime = datetime.fromisoformat(start_date.replace('Z', '+00:00')) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid start_date format. Use ISO format (YYYY-MM-DDTHH:MM:SS)") + + if end_date: + try: + end_datetime = datetime.fromisoformat(end_date.replace('Z', '+00:00')) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid end_date format. Use ISO format (YYYY-MM-DDTHH:MM:SS)") + + # Base query for feedback + feedback_query = db.query(Feedback) + + # Apply date filters if provided + if start_datetime: + feedback_query = feedback_query.filter(Feedback.created_at >= start_datetime) + + if end_datetime: + feedback_query = feedback_query.filter(Feedback.created_at <= end_datetime) + + # Get all feedback + all_feedback = feedback_query.all() + + # Calculate average rating + total_ratings = len(all_feedback) + sum_ratings = sum(feedback.rating for feedback in all_feedback) + avg_rating = round(sum_ratings / total_ratings, 1) if total_ratings > 0 else 0 + + # Count ratings by score + rating_counts = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0} + for feedback in all_feedback: + rating_counts[feedback.rating] = rating_counts.get(feedback.rating, 0) + 1 + + # Calculate rating percentages + rating_percentages = {} + for rating, count in rating_counts.items(): + rating_percentages[rating] = round((count / total_ratings) * 100, 1) if total_ratings > 0 else 0 + + # Get recent feedback with comments + recent_feedback = ( + db.query(Feedback, Person.username) + .outerjoin(Person, Feedback.person_id == Person.id) + .filter(Feedback.comment != None) + .filter(Feedback.comment != "") + ) + + # Apply date filters if provided + if start_datetime: + recent_feedback = recent_feedback.filter(Feedback.created_at >= start_datetime) + + if end_datetime: + recent_feedback = recent_feedback.filter(Feedback.created_at <= end_datetime) + + recent_feedback = recent_feedback.order_by(Feedback.created_at.desc()).limit(10).all() + + # Format recent feedback + formatted_feedback = [] + for feedback, username in recent_feedback: + formatted_feedback.append({ + "id": feedback.id, + "rating": feedback.rating, + "comment": feedback.comment, + "username": username or "Anonymous", + "created_at": feedback.created_at.isoformat(), + }) + + # Return analysis + return { + "total_feedback": total_ratings, + "average_rating": avg_rating, + "rating_counts": rating_counts, + "rating_percentages": rating_percentages, + "recent_comments": formatted_feedback, + "date_range": { + "start_date": start_date, + "end_date": end_date + } + } diff --git a/app/routers/chef.py b/app/routers/chef.py new file mode 100644 index 0000000000000000000000000000000000000000..5ec8d7503ee9fd101f85cee26ba9f3698fd5242f --- /dev/null +++ b/app/routers/chef.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from datetime import datetime + +from ..database import get_db, Dish, Order, OrderItem +from ..models.dish import Dish as DishModel +from ..models.order import Order as OrderModel + +router = APIRouter( + prefix="/chef", + tags=["chef"], + responses={404: {"description": "Not found"}}, +) + +# Add an API endpoint to get completed orders count +@router.get("/api/completed-orders-count") +def get_completed_orders_count(db: Session = Depends(get_db)): + completed_orders = db.query(Order).filter(Order.status == "completed").count() + return {"count": completed_orders} + +# Get pending orders +@router.get("/orders/pending", response_model=List[OrderModel]) +def get_pending_orders(db: Session = Depends(get_db)): + orders = db.query(Order).filter(Order.status == "pending").all() + return orders + +# Mark order as completed +@router.put("/orders/{order_id}/complete") +def complete_order(order_id: int, db: Session = Depends(get_db)): + db_order = db.query(Order).filter(Order.id == order_id).first() + if db_order is None: + raise HTTPException(status_code=404, detail="Order not found") + + db_order.status = "completed" + db_order.updated_at = datetime.utcnow() + + db.commit() + + return {"message": "Order marked as completed"} diff --git a/app/routers/customer.py b/app/routers/customer.py new file mode 100644 index 0000000000000000000000000000000000000000..45765369b151fdd344eafc9d77a5e60162dd8bf6 --- /dev/null +++ b/app/routers/customer.py @@ -0,0 +1,452 @@ +from fastapi import APIRouter, Depends, HTTPException, Request, status +from sqlalchemy.orm import Session +from typing import List, Dict, Any +import uuid +from datetime import datetime, timezone, timedelta + +from ..database import get_db, Dish, Order, OrderItem, Person +from ..models.dish import Dish as DishModel +from ..models.order import OrderCreate, Order as OrderModel +from ..models.user import ( + PersonCreate, + PersonLogin, + Person as PersonModel, + PhoneAuthRequest, + PhoneVerifyRequest, + UsernameRequest +) +from ..services import firebase_auth + +router = APIRouter( + prefix="/customer", + tags=["customer"], + responses={404: {"description": "Not found"}}, +) + + +# Get all dishes for menu +@router.get("/api/menu", response_model=List[DishModel]) +def get_menu(category: str = None, db: Session = Depends(get_db)): + if category: + dishes = db.query(Dish).filter(Dish.category == category).all() + else: + dishes = db.query(Dish).all() + return dishes + + +# Get offer dishes +@router.get("/api/offers", response_model=List[DishModel]) +def get_offers(db: Session = Depends(get_db)): + dishes = db.query(Dish).filter(Dish.is_offer == 1).all() + return dishes + + +# Get special dishes +@router.get("/api/specials", response_model=List[DishModel]) +def get_specials(db: Session = Depends(get_db)): + dishes = db.query(Dish).filter(Dish.is_special == 1).all() + return dishes + + +# Get all dish categories +@router.get("/api/categories") +def get_categories(db: Session = Depends(get_db)): + categories = db.query(Dish.category).distinct().all() + return [category[0] for category in categories] + + +# Register a new user or update existing user +@router.post("/api/register", response_model=PersonModel) +def register_user(user: PersonCreate, db: Session = Depends(get_db)): + # Check if user already exists + db_user = db.query(Person).filter(Person.username == user.username).first() + + if db_user: + # Update existing user's visit count and last visit time + db_user.visit_count += 1 + db_user.last_visit = datetime.now(timezone.utc) + db.commit() + db.refresh(db_user) + return db_user + else: + # Create new user + db_user = Person( + username=user.username, + password=user.password, # In a real app, you should hash this password + visit_count=1, + last_visit=datetime.now(timezone.utc), + ) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + + +# Login user +@router.post("/api/login", response_model=Dict[str, Any]) +def login_user(user_data: PersonLogin, db: Session = Depends(get_db)): + # Find user by username + db_user = db.query(Person).filter(Person.username == user_data.username).first() + + if not db_user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid username" + ) + + # Check password (in a real app, you would verify hashed passwords) + if db_user.password != user_data.password: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid password" + ) + + # Update visit count and last visit time + db_user.visit_count += 1 + db_user.last_visit = datetime.now(timezone.utc) + db.commit() + + # Return user info and a success message + return { + "user": { + "id": db_user.id, + "username": db_user.username, + "visit_count": db_user.visit_count, + }, + "message": "Login successful", + } + + +# Create new order +@router.post("/api/orders", response_model=OrderModel) +def create_order( + order: OrderCreate, person_id: int = None, db: Session = Depends(get_db) +): + # If person_id is not provided but we have a username/password, try to find or create the user + if not person_id and hasattr(order, "username") and hasattr(order, "password"): + # Check if user exists + db_user = db.query(Person).filter(Person.username == order.username).first() + + if db_user: + # Update existing user's visit count + db_user.visit_count += 1 + db_user.last_visit = datetime.now(timezone.utc) + db.commit() + person_id = db_user.id + else: + # Create new user + db_user = Person( + username=order.username, + password=order.password, + visit_count=1, + last_visit=datetime.now(timezone.utc), + ) + db.add(db_user) + db.commit() + db.refresh(db_user) + person_id = db_user.id + + # Create order + db_order = Order( + table_number=order.table_number, + unique_id=order.unique_id, + person_id=person_id, # Link order to person if provided + status="pending", + ) + db.add(db_order) + db.commit() + db.refresh(db_order) + + # Mark the table as occupied + from ..database import Table + + db_table = db.query(Table).filter(Table.table_number == order.table_number).first() + if db_table: + db_table.is_occupied = True + db_table.current_order_id = db_order.id + db.commit() + + # Create order items + for item in order.items: + # Get the dish to include its information + dish = db.query(Dish).filter(Dish.id == item.dish_id).first() + if not dish: + continue # Skip if dish doesn't exist + + db_item = OrderItem( + order_id=db_order.id, + dish_id=item.dish_id, + quantity=item.quantity, + remarks=item.remarks, + ) + db.add(db_item) + + db.commit() + db.refresh(db_order) + + return db_order + + +# Get order status +@router.get("/api/orders/{order_id}", response_model=OrderModel) +def get_order(order_id: int, db: Session = Depends(get_db)): + # Use joinedload to load the dish relationship for each order item + order = db.query(Order).filter(Order.id == order_id).first() + if order is None: + raise HTTPException(status_code=404, detail="Order not found") + + # Explicitly load dish information for each order item + for item in order.items: + if not hasattr(item, "dish") or item.dish is None: + dish = db.query(Dish).filter(Dish.id == item.dish_id).first() + if dish: + item.dish = dish + + return order + + +# Get orders by person_id +@router.get("/api/person/{person_id}/orders", response_model=List[OrderModel]) +def get_person_orders(person_id: int, db: Session = Depends(get_db)): + # Get all orders for a specific person + orders = ( + db.query(Order) + .filter(Order.person_id == person_id) + .order_by(Order.created_at.desc()) + .all() + ) + + # Explicitly load dish information for each order item + for order in orders: + for item in order.items: + if not hasattr(item, "dish") or item.dish is None: + dish = db.query(Dish).filter(Dish.id == item.dish_id).first() + if dish: + item.dish = dish + + return orders + + +# Request payment for order +@router.put("/api/orders/{order_id}/payment") +def request_payment(order_id: int, db: Session = Depends(get_db)): + db_order = db.query(Order).filter(Order.id == order_id).first() + if db_order is None: + raise HTTPException(status_code=404, detail="Order not found") + + # Update order status to paid directly + db_order.status = "paid" + db_order.updated_at = datetime.now(timezone.utc) + + # Mark the table as free + from ..database import Table + + db_table = ( + db.query(Table).filter(Table.table_number == db_order.table_number).first() + ) + if db_table: + db_table.is_occupied = False + db_table.current_order_id = None + db_table.updated_at = datetime.now(timezone.utc) + + db.commit() + + return {"message": "Payment completed successfully"} + + +# Cancel order +@router.put("/api/orders/{order_id}/cancel") +def cancel_order(order_id: int, db: Session = Depends(get_db)): + db_order = db.query(Order).filter(Order.id == order_id).first() + if db_order is None: + raise HTTPException(status_code=404, detail="Order not found") + + # Check if order is in pending status + if db_order.status != "pending": + raise HTTPException( + status_code=400, + detail="Only pending orders can be cancelled" + ) + + # Check if order was created within the last 60 seconds + current_time = datetime.now(timezone.utc) + order_time = db_order.created_at + time_difference = current_time - order_time + + if time_difference > timedelta(seconds=60): + raise HTTPException( + status_code=400, + detail="Orders can only be cancelled within 60 seconds of placing" + ) + + # Update order status to cancelled + db_order.status = "cancelled" + db_order.updated_at = current_time + + # Mark the table as free if this was the current order + from ..database import Table + + db_table = db.query(Table).filter(Table.table_number == db_order.table_number).first() + if db_table and db_table.current_order_id == db_order.id: + db_table.is_occupied = False + db_table.current_order_id = None + db_table.updated_at = current_time + + db.commit() + + return {"message": "Order cancelled successfully"} + + +# Get person details +@router.get("/api/person/{person_id}", response_model=PersonModel) +def get_person(person_id: int, db: Session = Depends(get_db)): + person = db.query(Person).filter(Person.id == person_id).first() + if not person: + raise HTTPException(status_code=404, detail="Person not found") + return person + + +# Phone authentication endpoints +@router.post("/api/phone-auth", response_model=Dict[str, Any]) +def phone_auth(auth_request: PhoneAuthRequest, db: Session = Depends(get_db)): + """ + Initiate phone authentication by sending OTP + """ + try: + # Validate phone number format + if not auth_request.phone_number.startswith("+91"): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Phone number must start with +91" + ) + + # Send OTP via Firebase + result = firebase_auth.verify_phone_number(auth_request.phone_number) + + print(f"Phone auth initiated for: {auth_request.phone_number}, table: {auth_request.table_number}") + + return { + "success": True, + "message": "Verification code sent successfully", + "session_info": result.get("sessionInfo", "firebase-verification-token") + } + except HTTPException as e: + print(f"HTTP Exception in phone_auth: {e.detail}") + raise e + except Exception as e: + print(f"Exception in phone_auth: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to send verification code: {str(e)}" + ) + + +@router.post("/api/verify-otp", response_model=Dict[str, Any]) +def verify_otp(verify_request: PhoneVerifyRequest, db: Session = Depends(get_db)): + """ + Verify OTP and authenticate user + """ + try: + print(f"Verifying OTP for phone: {verify_request.phone_number}") + + # Verify OTP via Firebase + # Note: The actual OTP verification is done on the client side with Firebase + # This is just a validation step + firebase_auth.verify_otp( + verify_request.phone_number, + verify_request.verification_code + ) + + # Check if user exists in database + user = db.query(Person).filter(Person.phone_number == verify_request.phone_number).first() + + if user: + print(f"Existing user found: {user.username}") + # Existing user - update visit count + user.visit_count += 1 + user.last_visit = datetime.now(timezone.utc) + db.commit() + db.refresh(user) + + return { + "success": True, + "message": "Authentication successful", + "user_exists": True, + "user_id": user.id, + "username": user.username + } + else: + print(f"New user with phone: {verify_request.phone_number}") + # New user - return flag to collect username + return { + "success": True, + "message": "Authentication successful, but user not found", + "user_exists": False + } + + except HTTPException as e: + print(f"HTTP Exception in verify_otp: {e.detail}") + raise e + except Exception as e: + print(f"Exception in verify_otp: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to verify OTP: {str(e)}" + ) + + +@router.post("/api/register-phone-user", response_model=Dict[str, Any]) +def register_phone_user(user_request: UsernameRequest, db: Session = Depends(get_db)): + """ + Register a new user after phone authentication + """ + try: + print(f"Registering new user with phone: {user_request.phone_number}, username: {user_request.username}") + + # Check if username already exists + existing_user = db.query(Person).filter(Person.username == user_request.username).first() + if existing_user: + print(f"Username already exists: {user_request.username}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already exists" + ) + + # Check if phone number already exists + phone_user = db.query(Person).filter(Person.phone_number == user_request.phone_number).first() + if phone_user: + print(f"Phone number already registered: {user_request.phone_number}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Phone number already registered" + ) + + # Create new user + new_user = Person( + username=user_request.username, + password="", # No password needed for phone auth + phone_number=user_request.phone_number, + visit_count=1, + last_visit=datetime.now(timezone.utc) + ) + + db.add(new_user) + db.commit() + db.refresh(new_user) + + print(f"User registered successfully: {new_user.id}, {new_user.username}") + + return { + "success": True, + "message": "User registered successfully", + "user_id": new_user.id, + "username": new_user.username + } + + except HTTPException as e: + print(f"HTTP Exception in register_phone_user: {e.detail}") + raise e + except Exception as e: + print(f"Exception in register_phone_user: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to register user: {str(e)}" + ) diff --git a/app/routers/feedback.py b/app/routers/feedback.py new file mode 100644 index 0000000000000000000000000000000000000000..48b60557353133ca27c29c77277f5e212980d938 --- /dev/null +++ b/app/routers/feedback.py @@ -0,0 +1,62 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from datetime import datetime, timezone + +from ..database import get_db, Feedback as FeedbackModel, Order, Person +from ..models.feedback import Feedback, FeedbackCreate + +router = APIRouter( + prefix="/feedback", + tags=["feedback"], + responses={404: {"description": "Not found"}}, +) + + +# Create new feedback +@router.post("/", response_model=Feedback) +def create_feedback(feedback: FeedbackCreate, db: Session = Depends(get_db)): + # Check if order exists + db_order = db.query(Order).filter(Order.id == feedback.order_id).first() + if not db_order: + raise HTTPException(status_code=404, detail="Order not found") + + # Check if person exists if person_id is provided + if feedback.person_id: + db_person = db.query(Person).filter(Person.id == feedback.person_id).first() + if not db_person: + raise HTTPException(status_code=404, detail="Person not found") + + # Create feedback + db_feedback = FeedbackModel( + order_id=feedback.order_id, + person_id=feedback.person_id, + rating=feedback.rating, + comment=feedback.comment, + created_at=datetime.now(timezone.utc), + ) + db.add(db_feedback) + db.commit() + db.refresh(db_feedback) + return db_feedback + + +# Get all feedback +@router.get("/", response_model=List[Feedback]) +def get_all_feedback(db: Session = Depends(get_db)): + return db.query(FeedbackModel).all() + + +# Get feedback by order_id +@router.get("/order/{order_id}", response_model=Feedback) +def get_feedback_by_order(order_id: int, db: Session = Depends(get_db)): + db_feedback = db.query(FeedbackModel).filter(FeedbackModel.order_id == order_id).first() + if not db_feedback: + raise HTTPException(status_code=404, detail="Feedback not found") + return db_feedback + + +# Get feedback by person_id +@router.get("/person/{person_id}", response_model=List[Feedback]) +def get_feedback_by_person(person_id: int, db: Session = Depends(get_db)): + return db.query(FeedbackModel).filter(FeedbackModel.person_id == person_id).all() diff --git a/app/routers/loyalty.py b/app/routers/loyalty.py new file mode 100644 index 0000000000000000000000000000000000000000..4f681dfa3d10f9989b4bb391d859b4b46e2c176a --- /dev/null +++ b/app/routers/loyalty.py @@ -0,0 +1,151 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from datetime import datetime, timezone + +from ..database import get_db, LoyaltyProgram as LoyaltyProgramModel +from ..models.loyalty import LoyaltyProgram, LoyaltyProgramCreate, LoyaltyProgramUpdate + +router = APIRouter( + prefix="/loyalty", + tags=["loyalty"], + responses={404: {"description": "Not found"}}, +) + + +# Get all loyalty program tiers +@router.get("/", response_model=List[LoyaltyProgram]) +def get_all_loyalty_tiers(db: Session = Depends(get_db)): + return db.query(LoyaltyProgramModel).order_by(LoyaltyProgramModel.visit_count).all() + + +# Get active loyalty program tiers +@router.get("/active", response_model=List[LoyaltyProgram]) +def get_active_loyalty_tiers(db: Session = Depends(get_db)): + return ( + db.query(LoyaltyProgramModel) + .filter(LoyaltyProgramModel.is_active == True) + .order_by(LoyaltyProgramModel.visit_count) + .all() + ) + + +# Get loyalty tier by ID +@router.get("/{tier_id}", response_model=LoyaltyProgram) +def get_loyalty_tier(tier_id: int, db: Session = Depends(get_db)): + db_tier = ( + db.query(LoyaltyProgramModel).filter(LoyaltyProgramModel.id == tier_id).first() + ) + if not db_tier: + raise HTTPException(status_code=404, detail="Loyalty tier not found") + return db_tier + + +# Create new loyalty tier +@router.post("/", response_model=LoyaltyProgram) +def create_loyalty_tier(tier: LoyaltyProgramCreate, db: Session = Depends(get_db)): + # Check if a tier with this visit count already exists + existing_tier = ( + db.query(LoyaltyProgramModel) + .filter(LoyaltyProgramModel.visit_count == tier.visit_count) + .first() + ) + if existing_tier: + raise HTTPException( + status_code=400, + detail=f"Loyalty tier with visit count {tier.visit_count} already exists", + ) + + # Create new tier + db_tier = LoyaltyProgramModel( + visit_count=tier.visit_count, + discount_percentage=tier.discount_percentage, + is_active=tier.is_active, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + db.add(db_tier) + db.commit() + db.refresh(db_tier) + return db_tier + + +# Update loyalty tier +@router.put("/{tier_id}", response_model=LoyaltyProgram) +def update_loyalty_tier( + tier_id: int, tier_update: LoyaltyProgramUpdate, db: Session = Depends(get_db) +): + db_tier = ( + db.query(LoyaltyProgramModel).filter(LoyaltyProgramModel.id == tier_id).first() + ) + if not db_tier: + raise HTTPException(status_code=404, detail="Loyalty tier not found") + + # Check if updating visit count and if it already exists + if ( + tier_update.visit_count is not None + and tier_update.visit_count != db_tier.visit_count + ): + existing_tier = ( + db.query(LoyaltyProgramModel) + .filter( + LoyaltyProgramModel.visit_count == tier_update.visit_count, + LoyaltyProgramModel.id != tier_id, + ) + .first() + ) + if existing_tier: + raise HTTPException( + status_code=400, + detail=f"Loyalty tier with visit count {tier_update.visit_count} already exists", + ) + db_tier.visit_count = tier_update.visit_count + + # Update other fields if provided + if tier_update.discount_percentage is not None: + db_tier.discount_percentage = tier_update.discount_percentage + if tier_update.is_active is not None: + db_tier.is_active = tier_update.is_active + + db_tier.updated_at = datetime.now(timezone.utc) + db.commit() + db.refresh(db_tier) + return db_tier + + +# Delete loyalty tier +@router.delete("/{tier_id}") +def delete_loyalty_tier(tier_id: int, db: Session = Depends(get_db)): + db_tier = ( + db.query(LoyaltyProgramModel).filter(LoyaltyProgramModel.id == tier_id).first() + ) + if not db_tier: + raise HTTPException(status_code=404, detail="Loyalty tier not found") + + db.delete(db_tier) + db.commit() + return {"message": "Loyalty tier deleted successfully"} + + +# Get applicable discount for a visit count +@router.get("/discount/{visit_count}") +def get_discount_for_visit_count(visit_count: int, db: Session = Depends(get_db)): + # Find the tier that exactly matches the visit count + applicable_tier = ( + db.query(LoyaltyProgramModel) + .filter( + LoyaltyProgramModel.visit_count == visit_count, + LoyaltyProgramModel.is_active == True, + ) + .first() + ) + + if not applicable_tier: + return {"discount_percentage": 0, "message": "No applicable loyalty discount"} + + return { + "discount_percentage": applicable_tier.discount_percentage, + "tier_id": applicable_tier.id, + "visit_count": applicable_tier.visit_count, + "message": f"Loyalty discount of {applicable_tier.discount_percentage}% applied for {visit_count} visits", + } diff --git a/app/routers/selection_offer.py b/app/routers/selection_offer.py new file mode 100644 index 0000000000000000000000000000000000000000..7a315c14a8866f52dcb5079cd33f981d2bdd5bf2 --- /dev/null +++ b/app/routers/selection_offer.py @@ -0,0 +1,162 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List +from datetime import datetime, timezone + +from ..database import get_db, SelectionOffer as SelectionOfferModel +from ..models.selection_offer import ( + SelectionOffer, + SelectionOfferCreate, + SelectionOfferUpdate, +) + +router = APIRouter( + prefix="/selection-offers", + tags=["selection-offers"], + responses={404: {"description": "Not found"}}, +) + + +# Get all selection offers +@router.get("/", response_model=List[SelectionOffer]) +def get_all_selection_offers(db: Session = Depends(get_db)): + return db.query(SelectionOfferModel).order_by(SelectionOfferModel.min_amount).all() + + +# Get active selection offers +@router.get("/active", response_model=List[SelectionOffer]) +def get_active_selection_offers(db: Session = Depends(get_db)): + return ( + db.query(SelectionOfferModel) + .filter(SelectionOfferModel.is_active == True) + .order_by(SelectionOfferModel.min_amount) + .all() + ) + + +# Get selection offer by ID +@router.get("/{offer_id}", response_model=SelectionOffer) +def get_selection_offer(offer_id: int, db: Session = Depends(get_db)): + db_offer = ( + db.query(SelectionOfferModel).filter(SelectionOfferModel.id == offer_id).first() + ) + if not db_offer: + raise HTTPException(status_code=404, detail="Selection offer not found") + return db_offer + + +# Create new selection offer +@router.post("/", response_model=SelectionOffer) +def create_selection_offer(offer: SelectionOfferCreate, db: Session = Depends(get_db)): + # Check if an offer with this min_amount already exists + existing_offer = ( + db.query(SelectionOfferModel) + .filter(SelectionOfferModel.min_amount == offer.min_amount) + .first() + ) + if existing_offer: + raise HTTPException( + status_code=400, + detail=f"Selection offer with minimum amount {offer.min_amount} already exists", + ) + + # Create new offer + db_offer = SelectionOfferModel( + min_amount=offer.min_amount, + discount_amount=offer.discount_amount, + is_active=offer.is_active, + description=offer.description, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + db.add(db_offer) + db.commit() + db.refresh(db_offer) + return db_offer + + +# Update selection offer +@router.put("/{offer_id}", response_model=SelectionOffer) +def update_selection_offer( + offer_id: int, offer_update: SelectionOfferUpdate, db: Session = Depends(get_db) +): + db_offer = ( + db.query(SelectionOfferModel).filter(SelectionOfferModel.id == offer_id).first() + ) + if not db_offer: + raise HTTPException(status_code=404, detail="Selection offer not found") + + # Check if updating min_amount and if it already exists + if ( + offer_update.min_amount is not None + and offer_update.min_amount != db_offer.min_amount + ): + existing_offer = ( + db.query(SelectionOfferModel) + .filter( + SelectionOfferModel.min_amount == offer_update.min_amount, + SelectionOfferModel.id != offer_id, + ) + .first() + ) + if existing_offer: + raise HTTPException( + status_code=400, + detail=f"Selection offer with minimum amount {offer_update.min_amount} already exists", + ) + db_offer.min_amount = offer_update.min_amount + + # Update other fields if provided + if offer_update.discount_amount is not None: + db_offer.discount_amount = offer_update.discount_amount + if offer_update.is_active is not None: + db_offer.is_active = offer_update.is_active + if offer_update.description is not None: + db_offer.description = offer_update.description + + db_offer.updated_at = datetime.now(timezone.utc) + db.commit() + db.refresh(db_offer) + return db_offer + + +# Delete selection offer +@router.delete("/{offer_id}") +def delete_selection_offer(offer_id: int, db: Session = Depends(get_db)): + db_offer = ( + db.query(SelectionOfferModel).filter(SelectionOfferModel.id == offer_id).first() + ) + if not db_offer: + raise HTTPException(status_code=404, detail="Selection offer not found") + + db.delete(db_offer) + db.commit() + return {"message": "Selection offer deleted successfully"} + + +# Get applicable discount for an order amount +@router.get("/discount/{order_amount}") +def get_discount_for_order_amount(order_amount: float, db: Session = Depends(get_db)): + # Find the highest tier that the order amount qualifies for + applicable_offer = ( + db.query(SelectionOfferModel) + .filter( + SelectionOfferModel.min_amount <= order_amount, + SelectionOfferModel.is_active == True, + ) + .order_by(SelectionOfferModel.min_amount.desc()) + .first() + ) + + if not applicable_offer: + return { + "discount_amount": 0, + "message": "No applicable selection offer discount", + } + + return { + "discount_amount": applicable_offer.discount_amount, + "offer_id": applicable_offer.id, + "min_amount": applicable_offer.min_amount, + "message": f"Selection offer discount of ${applicable_offer.discount_amount} applied", + } diff --git a/app/routers/settings.py b/app/routers/settings.py new file mode 100644 index 0000000000000000000000000000000000000000..1aaffaf80d8ec65f906f5bfa5603690a4be4cec8 --- /dev/null +++ b/app/routers/settings.py @@ -0,0 +1,89 @@ +from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form +from sqlalchemy.orm import Session +from typing import Optional +import os +import shutil +from datetime import datetime, timezone + +from ..database import get_db, Settings +from ..models.settings import Settings as SettingsModel, SettingsUpdate + +router = APIRouter( + prefix="/settings", + tags=["settings"], + responses={404: {"description": "Not found"}}, +) + + +# Get hotel settings +@router.get("/", response_model=SettingsModel) +def get_settings(db: Session = Depends(get_db)): + # Get the first settings record or create one if it doesn't exist + settings = db.query(Settings).first() + + if not settings: + # Create default settings + settings = Settings( + hotel_name="Tabble Hotel", + address="123 Main Street, City", + contact_number="+1 123-456-7890", + email="info@tabblehotel.com", + ) + db.add(settings) + db.commit() + db.refresh(settings) + + return settings + + +# Update hotel settings +@router.put("/", response_model=SettingsModel) +async def update_settings( + hotel_name: str = Form(...), + address: Optional[str] = Form(None), + contact_number: Optional[str] = Form(None), + email: Optional[str] = Form(None), + tax_id: Optional[str] = Form(None), + logo: Optional[UploadFile] = File(None), + db: Session = Depends(get_db) +): + # Get existing settings or create new + settings = db.query(Settings).first() + + if not settings: + settings = Settings( + hotel_name=hotel_name, + address=address, + contact_number=contact_number, + email=email, + tax_id=tax_id, + ) + db.add(settings) + else: + # Update fields + settings.hotel_name = hotel_name + settings.address = address + settings.contact_number = contact_number + settings.email = email + settings.tax_id = tax_id + + # Handle logo upload if provided + if logo: + # Create directory if it doesn't exist + os.makedirs("app/static/images/logo", exist_ok=True) + + # Save logo + logo_path = f"app/static/images/logo/hotel_logo_{logo.filename}" + with open(logo_path, "wb") as buffer: + shutil.copyfileobj(logo.file, buffer) + + # Update settings with logo path + settings.logo_path = f"/static/images/logo/hotel_logo_{logo.filename}" + + # Update timestamp + settings.updated_at = datetime.now(timezone.utc) + + db.commit() + db.refresh(settings) + + return settings diff --git a/app/routers/table.py b/app/routers/table.py new file mode 100644 index 0000000000000000000000000000000000000000..1a4658cade8888c2f2d7c9feca938ef72e24de6c --- /dev/null +++ b/app/routers/table.py @@ -0,0 +1,229 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from datetime import datetime, timezone + +from ..database import get_db, Table as TableModel, Order +from ..models.table import Table, TableCreate, TableUpdate, TableStatus + +router = APIRouter( + prefix="/tables", + tags=["tables"], + responses={404: {"description": "Not found"}}, +) + + +# Get all tables +@router.get("/", response_model=List[Table]) +def get_all_tables(db: Session = Depends(get_db)): + return db.query(TableModel).order_by(TableModel.table_number).all() + + +# Get table by ID +@router.get("/{table_id}", response_model=Table) +def get_table(table_id: int, db: Session = Depends(get_db)): + db_table = db.query(TableModel).filter(TableModel.id == table_id).first() + if not db_table: + raise HTTPException(status_code=404, detail="Table not found") + return db_table + + +# Get table by table number +@router.get("/number/{table_number}", response_model=Table) +def get_table_by_number(table_number: int, db: Session = Depends(get_db)): + db_table = ( + db.query(TableModel).filter(TableModel.table_number == table_number).first() + ) + if not db_table: + raise HTTPException(status_code=404, detail="Table not found") + return db_table + + +# Create new table +@router.post("/", response_model=Table) +def create_table(table: TableCreate, db: Session = Depends(get_db)): + # Check if a table with this number already exists + existing_table = ( + db.query(TableModel) + .filter(TableModel.table_number == table.table_number) + .first() + ) + if existing_table: + raise HTTPException( + status_code=400, + detail=f"Table with number {table.table_number} already exists", + ) + + # Create new table + db_table = TableModel( + table_number=table.table_number, + is_occupied=table.is_occupied, + current_order_id=table.current_order_id, + last_occupied_at=table.last_occupied_at, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + db.add(db_table) + db.commit() + db.refresh(db_table) + return db_table + + +# Update table +@router.put("/{table_id}", response_model=Table) +def update_table( + table_id: int, table_update: TableUpdate, db: Session = Depends(get_db) +): + db_table = db.query(TableModel).filter(TableModel.id == table_id).first() + if not db_table: + raise HTTPException(status_code=404, detail="Table not found") + + # Update fields if provided + if table_update.is_occupied is not None: + db_table.is_occupied = table_update.is_occupied + if table_update.current_order_id is not None: + db_table.current_order_id = table_update.current_order_id + + db_table.updated_at = datetime.now(timezone.utc) + db.commit() + db.refresh(db_table) + return db_table + + +# Delete table +@router.delete("/{table_id}") +def delete_table(table_id: int, db: Session = Depends(get_db)): + db_table = db.query(TableModel).filter(TableModel.id == table_id).first() + if not db_table: + raise HTTPException(status_code=404, detail="Table not found") + + # Check if table is currently occupied + if db_table.is_occupied: + raise HTTPException( + status_code=400, detail="Cannot delete a table that is currently occupied" + ) + + db.delete(db_table) + db.commit() + return {"message": "Table deleted successfully"} + + +# Get table status (total, occupied, free) +@router.get("/status/summary", response_model=TableStatus) +def get_table_status(db: Session = Depends(get_db)): + total_tables = db.query(TableModel).count() + occupied_tables = ( + db.query(TableModel).filter(TableModel.is_occupied == True).count() + ) + free_tables = total_tables - occupied_tables + + return { + "total_tables": total_tables, + "occupied_tables": occupied_tables, + "free_tables": free_tables, + } + + +# Set table as occupied +@router.put("/{table_id}/occupy", response_model=Table) +def set_table_occupied( + table_id: int, order_id: int = None, db: Session = Depends(get_db) +): + db_table = db.query(TableModel).filter(TableModel.id == table_id).first() + if not db_table: + raise HTTPException(status_code=404, detail="Table not found") + + # Check if table is already occupied + if db_table.is_occupied: + raise HTTPException(status_code=400, detail="Table is already occupied") + + # Update table status + db_table.is_occupied = True + + # Link to order if provided + if order_id: + # Verify order exists + order = db.query(Order).filter(Order.id == order_id).first() + if not order: + raise HTTPException(status_code=404, detail="Order not found") + db_table.current_order_id = order_id + + db_table.updated_at = datetime.now(timezone.utc) + db.commit() + db.refresh(db_table) + return db_table + + +# Set table as free +@router.put("/{table_id}/free", response_model=Table) +def set_table_free(table_id: int, db: Session = Depends(get_db)): + db_table = db.query(TableModel).filter(TableModel.id == table_id).first() + if not db_table: + raise HTTPException(status_code=404, detail="Table not found") + + # Check if table is already free + if not db_table.is_occupied: + raise HTTPException(status_code=400, detail="Table is already free") + + # Update table status + db_table.is_occupied = False + db_table.current_order_id = None + db_table.updated_at = datetime.now(timezone.utc) + db.commit() + db.refresh(db_table) + return db_table + + +# Set table as occupied by table number +@router.put("/number/{table_number}/occupy", response_model=Table) +def set_table_occupied_by_number(table_number: int, db: Session = Depends(get_db)): + db_table = ( + db.query(TableModel).filter(TableModel.table_number == table_number).first() + ) + if not db_table: + raise HTTPException(status_code=404, detail="Table not found") + + # Update table status (even if already occupied, just update the timestamp) + db_table.is_occupied = True + db_table.last_occupied_at = datetime.now(timezone.utc) + db_table.updated_at = datetime.now(timezone.utc) + db.commit() + db.refresh(db_table) + return db_table + + +# Create multiple tables at once +@router.post("/batch", response_model=List[Table]) +def create_tables_batch(num_tables: int, db: Session = Depends(get_db)): + if num_tables <= 0: + raise HTTPException( + status_code=400, detail="Number of tables must be greater than 0" + ) + + # Get the highest existing table number + highest_table = ( + db.query(TableModel).order_by(TableModel.table_number.desc()).first() + ) + start_number = 1 + if highest_table: + start_number = highest_table.table_number + 1 + + # Create tables + new_tables = [] + for i in range(start_number, start_number + num_tables): + db_table = TableModel( + table_number=i, + is_occupied=False, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + db.add(db_table) + new_tables.append(db_table) + + db.commit() + + # Refresh all tables + for table in new_tables: + db.refresh(table) + + return new_tables diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a70b3029a5ce0e22988567ea083fca68daa2141b --- /dev/null +++ b/app/services/__init__.py @@ -0,0 +1 @@ +# Services package diff --git a/app/services/__pycache__/__init__.cpython-312.pyc b/app/services/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..75e503379f37dc269d6ef22af24c522bd58514e3 Binary files /dev/null and b/app/services/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/services/__pycache__/firebase_auth.cpython-312.pyc b/app/services/__pycache__/firebase_auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cb7b7ecfd09d1d37f2dab8eadf95ef040e13ee61 Binary files /dev/null and b/app/services/__pycache__/firebase_auth.cpython-312.pyc differ diff --git a/app/services/firebase_auth.py b/app/services/firebase_auth.py new file mode 100644 index 0000000000000000000000000000000000000000..9eac4b7cf9f66cd541dfd559b4127066d43e3688 --- /dev/null +++ b/app/services/firebase_auth.py @@ -0,0 +1,100 @@ +import firebase_admin +from firebase_admin import credentials, auth +import os +import json +from fastapi import HTTPException, status + +# Initialize Firebase Admin SDK +cred_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), + "app", "tabble-v1-firebase-adminsdk-fbsvc-8024adcbdf.json") + +# Global variable to track initialization +firebase_initialized = False + +try: + # Check if Firebase is already initialized + try: + firebase_app = firebase_admin.get_app() + firebase_initialized = True + print("Firebase already initialized") + except ValueError: + # Initialize Firebase if not already initialized + cred = credentials.Certificate(cred_path) + firebase_app = firebase_admin.initialize_app(cred) + firebase_initialized = True + print("Firebase initialized successfully") +except Exception as e: + print(f"Firebase initialization error: {e}") + # Continue without crashing, but authentication will fail + +# Firebase Authentication functions +def verify_phone_number(phone_number): + """ + Verify a phone number and send OTP + Returns a session info token that will be used to verify the OTP + """ + try: + # Check if Firebase is initialized + if not firebase_initialized: + print("Firebase is not initialized, using mock verification") + + # Validate phone number format (should start with +91) + if not phone_number.startswith("+91"): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Phone number must start with +91" + ) + + # In a real implementation with Firebase Admin SDK, we would use: + # session_info = auth.create_session_cookie(...) + # But for this implementation, we'll let the client-side Firebase handle the actual SMS sending + + print(f"Phone verification requested for: {phone_number}") + return {"sessionInfo": "firebase-verification-token", "success": True} + + except HTTPException as e: + # Re-raise HTTP exceptions + raise e + except Exception as e: + print(f"Error in verify_phone_number: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to send verification code: {str(e)}" + ) + +def verify_otp(phone_number, otp, session_info=None): + """ + Verify the OTP sent to the phone number + Returns a Firebase ID token if verification is successful + + Note: In this implementation, the actual OTP verification is done on the client side + using Firebase Authentication. This function is just for validating the format and + returning a success response. + """ + try: + # Check if Firebase is initialized + if not firebase_initialized: + print("Firebase is not initialized, using mock verification") + + # Validate OTP format + if not otp.isdigit() or len(otp) != 6: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid OTP format. Must be 6 digits." + ) + + # In a real implementation with Firebase Admin SDK, we would verify the OTP + # But for this implementation, we trust that the client-side Firebase has already verified it + + print(f"OTP verification successful for: {phone_number}") + return {"idToken": "firebase-id-token", "phone_number": phone_number, "success": True} + + except HTTPException as e: + # Re-raise HTTP exceptions + raise e + except Exception as e: + print(f"Error in verify_otp: {str(e)}") + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=f"Failed to verify OTP: {str(e)}" + ) diff --git a/app/static/images/default-dish.jpg b/app/static/images/default-dish.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e8ee874b71b656377f6e4b33a3ff492c8510ad7f --- /dev/null +++ b/app/static/images/default-dish.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:deda0f3567ebcd8b694618b0c1cd01a498f4dc737fe5b8dd42692858564b6bc8 +size 103 diff --git a/app/static/images/dishes/13_download.jpeg b/app/static/images/dishes/13_download.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..dace6199aae2571ccb73fc580c14a4c2f19bc9a6 --- /dev/null +++ b/app/static/images/dishes/13_download.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:142f57bd1734e04f8a756656ad8d6a3dd13926a1768b4cea5a18a4766a98a839 +size 13573 diff --git a/app/static/images/dishes/1_OSK.jpg b/app/static/images/dishes/1_OSK.jpg new file mode 100644 index 0000000000000000000000000000000000000000..efbf82f603dade88245757f6251cf8a3333f026f --- /dev/null +++ b/app/static/images/dishes/1_OSK.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b290198db6813499ca3fdfeee7d14ba0a97b9f106076dad664248fe5984ca86b +size 5065 diff --git a/app/static/images/dishes/1_download (5).jpeg b/app/static/images/dishes/1_download (5).jpeg new file mode 100644 index 0000000000000000000000000000000000000000..bbe3651faa250c2dd3750000a77c1d0b010c320b --- /dev/null +++ b/app/static/images/dishes/1_download (5).jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3714deb1f54c0f27cd0f2f6a933c9fb3cef1b457862499c3abcbdd16a0bf544c +size 11431 diff --git a/app/static/images/dishes/1_download.jpeg b/app/static/images/dishes/1_download.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..729b34ee3039dab5aa78946854818b90bcf2de1d --- /dev/null +++ b/app/static/images/dishes/1_download.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:118fd976a7435452bf9ae72e4f317fda01be166837f41037840f9acb85614d5d +size 11680 diff --git a/app/static/images/dishes/2_Ice_cream_with_whipped_cream,_chocolate_syrup,_and_a_wafer_(cropped).jpg b/app/static/images/dishes/2_Ice_cream_with_whipped_cream,_chocolate_syrup,_and_a_wafer_(cropped).jpg new file mode 100644 index 0000000000000000000000000000000000000000..9ed3d8df8ac6e6a90eaf01c9c9dd50353becb0fb --- /dev/null +++ b/app/static/images/dishes/2_Ice_cream_with_whipped_cream,_chocolate_syrup,_and_a_wafer_(cropped).jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6b9157d5830c2d48d771ad0ae2c0f042a9430bd29423d109232630b99dee336 +size 210922 diff --git a/app/static/images/dishes/2_download (4).jpeg b/app/static/images/dishes/2_download (4).jpeg new file mode 100644 index 0000000000000000000000000000000000000000..1d2bb0054eb75ce91c5bfe784093849c37fe02d7 --- /dev/null +++ b/app/static/images/dishes/2_download (4).jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b96098a606a75ead99ff23c7395e15b3f80cb5ab5b34f554c4e80f8cf3f23363 +size 11869 diff --git a/app/static/images/dishes/2_th.jpg b/app/static/images/dishes/2_th.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a6e0b0aa1e24351210fca6c0fddf4a1bc3d4b31c --- /dev/null +++ b/app/static/images/dishes/2_th.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab044f17dbb78714a19e14394175a5e1d44a304b7c2f19e91b1910d937ceaa97 +size 46065 diff --git a/app/static/images/dishes/3_download (3).jpeg b/app/static/images/dishes/3_download (3).jpeg new file mode 100644 index 0000000000000000000000000000000000000000..83e81951ae9c0b28708d73334d477cf30e081aa1 --- /dev/null +++ b/app/static/images/dishes/3_download (3).jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a17e347381eaa223faa6e8416245ea892f1e2564164eb48e12815ecab55c0479 +size 13657 diff --git a/app/static/images/dishes/4_download (2).jpeg b/app/static/images/dishes/4_download (2).jpeg new file mode 100644 index 0000000000000000000000000000000000000000..bc34b43a4ad77475af9f89e7d9932f6548dbb127 --- /dev/null +++ b/app/static/images/dishes/4_download (2).jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2345f2c19dfc0cc58c6bf8ac5d908621efba86150a2411a8fa9a843c281f3d80 +size 14680 diff --git a/app/static/images/dishes/5_download (1).jpeg b/app/static/images/dishes/5_download (1).jpeg new file mode 100644 index 0000000000000000000000000000000000000000..1801dcef0659e75d4158e8d07de8fbdd2eea7e13 --- /dev/null +++ b/app/static/images/dishes/5_download (1).jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebea96a8d7f8059014b1ecbdedfa5e083be9aa7b58cf1f97a7283ac28b836d28 +size 11587 diff --git a/app/static/images/dishes/6_download.jpeg b/app/static/images/dishes/6_download.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..bb3a06c68a52c196b0e8f4ae5017397c2e894b54 --- /dev/null +++ b/app/static/images/dishes/6_download.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e787bf2a996dc418bf8ac01172794046c42445032a4a69d8f377bfbd63e7cba +size 12717 diff --git a/app/static/images/dishes/7_50050-five-minute-ice-cream-DDMFS-4x3-076-fbf49ca6248e4dceb3f43a4f02823dd9.webp b/app/static/images/dishes/7_50050-five-minute-ice-cream-DDMFS-4x3-076-fbf49ca6248e4dceb3f43a4f02823dd9.webp new file mode 100644 index 0000000000000000000000000000000000000000..4697d2599f34a4ab546f177c8ba1ba85259e35c9 --- /dev/null +++ b/app/static/images/dishes/7_50050-five-minute-ice-cream-DDMFS-4x3-076-fbf49ca6248e4dceb3f43a4f02823dd9.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fb6fb9e982f0b12001910afbb54e8b024a4a38eec7afa06cf777f2caa3d2f0d +size 36854 diff --git a/app/static/images/dishes/7_download.webp b/app/static/images/dishes/7_download.webp new file mode 100644 index 0000000000000000000000000000000000000000..5592250aad3020a418397aecb3ee376e95ae9bf3 --- /dev/null +++ b/app/static/images/dishes/7_download.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbb71e1f370f237a8a75ac95cb27ca218d8168861d1c9f14729de7f6a6758ead +size 12812 diff --git a/app/tabble-v1-firebase-adminsdk-fbsvc-8024adcbdf.json b/app/tabble-v1-firebase-adminsdk-fbsvc-8024adcbdf.json new file mode 100644 index 0000000000000000000000000000000000000000..f586c1462875755058033df33c3fbd8536cd2754 --- /dev/null +++ b/app/tabble-v1-firebase-adminsdk-fbsvc-8024adcbdf.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "tabble-v1", + "private_key_id": "8024adcbdf26bf1cac14997f39331ee88fb00a86", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDk0yMPULQZ95J0\n8Ksg5l8mg9VcpKs+tAGSGvVrmfLDHVaVIanYGmd6yJ+mZd8ZTXf5G38IlmBzK1nV\nwGsv7WPBQPSS5wDyJgj2iULuKNO09YxFXwEwzut5iQNlw81YB0K7UkIE3Psk6DY+\naaxRgToid0T3Ppbkav0FvrbU9/3/PEoIeL/i7bBo1lDnuzsAy1cycmueWA1ZdM3v\naKU6EjlXYfbo2kZPNfHplPg32JWknQy/SPeY26aBX9O07Qh/dT/DxET/OXpWaEJz\nQhuTuPHtIdFDXjIoYDQoBAKsvzxk0LnHKZ60MhkV2qoXQjPAAwah+Ki4bPEqmGRR\nsiZgoOXxAgMBAAECggEAKlJfuWkuil/3E1BhAlKBdjzrZTlin9QIt1ZrnmVomExQ\nk4AYqwrFKVk6Z/sO/p3MqwL6JaM0fxCdSrLOUFy6dseWBKabju3YegqsmaJs95rz\nwO/fp0CaHk4oVfXXQKkFH2LJKZ+ahrq5L6V5VNDPKQlAtO77Vw9vsVSS+cRNUtug\n3a8VgrBM8hDcD3Ej78XeEHI33kOgJQ4mL9Dp8zAJFrR0X3oTAI4NtYOPcgH5l2Ng\nYXZKmigQR1jIWXb5AiKaHnFdBaatyrAvWtvT1/Zsp0NYvaRoefdcfG5KIoZoRLpb\nA7sfOp1c/JPRlye1CdwggJzORLjwSbfiBbGaDvVb7QKBgQD+JfpGP95KoEO7v7hH\nPSwb9db/dzB5/3CRbq7/G9eq0J7Zl+/UeKMfJmIHQP3p9/GvPnQHn7pih9AcoMsb\nyhURGeJ5FnLVytWwPQDat4IlyTUhqaACcElogBHcgFbgJxQLfaQiQcyR6vOjAE8L\nlFEDTMFXf8LXrlkZ0H9XtXPwRQKBgQDmfe1giRIehboRqD9eS4BQtC1L5zbQx8lW\nmj8a/QXySWBMSHFKZjWChWrg1nfsGTSWGlE4uMznP+p2nuOS6jHAATf2GJZVBVm/\nPnYfcpfHGA94a3CJVWjaTp5cE1D4FRFnUS4HgY2w0T3sD9zzU8L8XQ+X2zsTviCT\n252VmJYnvQKBgQDXWICvg7CsVQ3viSzxGBFHA9EQGAM4bEwKvtmDCil88FaZE1fB\nFhNJ8rD/an97/36HOgkA6MP6dw/NIiXXvyyImAFBDtdw9fSI57fQm8uojsv5YQxW\n5KQe6t23k/uI5TPj5Krt6AkZ3xZgGIPh0OOwQxpUNMp5DJ8s83DjdbnubQKBgQC2\nyL5qg8j+s4XvYsF+AdnsJjaVrvJld0pPh1rsCCOjFFVtqTVOjud4bl1nmCzZ6tMt\nBgnLNaIo8SL6lt5aL6bsYQsD+lOdcPTPGLWMEtASbx41nN5NypGwLhCfbCIV2n9G\ns7YQ9chrpEO65ImP3akPgK1Q++ZJrckf+FVrwOmy8QKBgBv1rjNyXm3FplH0hq44\nfRcTJorvOk2ac3Hew5Czzk6ODJovIr+GPRQ2/9wF/NMBD9bfbwcvmp7vFqkTEl5f\nD/xJOwxZ06c3LBYi9tp5yrpuiqvfNZ0dUrCEY20KUktvNxfTigEGCgTmJ8+AEPlx\nFIJyQJtUD6yk6TGx7G9wYOPT\n-----END PRIVATE KEY-----\n", + "client_email": "firebase-adminsdk-fbsvc@tabble-v1.iam.gserviceaccount.com", + "client_id": "116848039876810333298", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40tabble-v1.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..dd7ee44cc225898f78b85cc4725f87b743bfab91 --- /dev/null +++ b/app/utils/__init__.py @@ -0,0 +1 @@ +# Utils package diff --git a/app/utils/__pycache__/__init__.cpython-312.pyc b/app/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3bcdad57411a1e284402d48fb2c3bced71e3c185 Binary files /dev/null and b/app/utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/utils/__pycache__/pdf_generator.cpython-312.pyc b/app/utils/__pycache__/pdf_generator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e05afb7d10f1eb85ad491536b0093b33e247992f Binary files /dev/null and b/app/utils/__pycache__/pdf_generator.cpython-312.pyc differ diff --git a/app/utils/pdf_generator.py b/app/utils/pdf_generator.py new file mode 100644 index 0000000000000000000000000000000000000000..9f48ee4bd822fad811895fa9dbaad2c38d9958e4 --- /dev/null +++ b/app/utils/pdf_generator.py @@ -0,0 +1,252 @@ +from reportlab.lib import colors +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.units import inch +from io import BytesIO +from datetime import datetime +from typing import List + +def generate_bill_pdf(order, settings): + """ + Generate a PDF bill for a single order + + Args: + order: The order object with all details + settings: The hotel settings object + + Returns: + BytesIO: A buffer containing the PDF data + """ + # Convert single order to list and use the multi-order function + return generate_multi_order_bill_pdf([order], settings) + +def generate_multi_order_bill_pdf(orders: List, settings): + """ + Generate a PDF bill for multiple orders in a receipt-like format + + Args: + orders: List of order objects with all details + settings: The hotel settings object + + Returns: + BytesIO: A buffer containing the PDF data + """ + buffer = BytesIO() + # Use a narrower page size to mimic a receipt + doc = SimpleDocTemplate( + buffer, + pagesize=(4*inch, 11*inch), # Typical receipt width + rightMargin=10, + leftMargin=10, + topMargin=10, + bottomMargin=10 + ) + + # Create styles + styles = getSampleStyleSheet() + styles.add(ParagraphStyle( + name='HotelName', + fontName='Helvetica-Bold', + fontSize=14, + alignment=1, # Center alignment + spaceAfter=2 + )) + styles.add(ParagraphStyle( + name='HotelTagline', + fontName='Helvetica', + fontSize=9, + alignment=1, # Center alignment + spaceAfter=2 + )) + styles.add(ParagraphStyle( + name='HotelAddress', + fontName='Helvetica', + fontSize=8, + alignment=1, # Center alignment + spaceAfter=1 + )) + styles.add(ParagraphStyle( + name='BillInfo', + fontName='Helvetica', + fontSize=8, + alignment=0, # Left alignment + spaceAfter=1 + )) + styles.add(ParagraphStyle( + name='BillInfoRight', + fontName='Helvetica', + fontSize=8, + alignment=2, # Right alignment + spaceAfter=1 + )) + styles.add(ParagraphStyle( + name='TableHeader', + fontName='Helvetica-Bold', + fontSize=8, + alignment=0 + )) + styles.add(ParagraphStyle( + name='ItemName', + fontName='Helvetica', + fontSize=8, + alignment=0 + )) + styles.add(ParagraphStyle( + name='ItemValue', + fontName='Helvetica', + fontSize=8, + alignment=2 # Right alignment + )) + styles.add(ParagraphStyle( + name='Total', + fontName='Helvetica-Bold', + fontSize=9, + alignment=1 # Center alignment + )) + styles.add(ParagraphStyle( + name='Footer', + fontName='Helvetica', + fontSize=7, + alignment=1, # Center alignment + textColor=colors.black + )) + + # Create content elements + elements = [] + + # We're not using the logo in this receipt-style bill + # Add hotel name and info + elements.append(Paragraph(settings.hotel_name.upper(), styles['HotelName'])) + + # Add tagline (if any, otherwise use a default) + tagline = "AN AUTHENTIC CUISINE SINCE 2000" + elements.append(Paragraph(tagline, styles['HotelTagline'])) + + # Add address with formatting similar to the image + if settings.address: + elements.append(Paragraph(settings.address, styles['HotelAddress'])) + + # Add contact info + if settings.contact_number: + elements.append(Paragraph(f"Contact: {settings.contact_number}", styles['HotelAddress'])) + + # Add tax ID (GSTIN) + if settings.tax_id: + elements.append(Paragraph(f"GSTIN: {settings.tax_id}", styles['HotelAddress'])) + + # Add a separator line + elements.append(Paragraph("_" * 50, styles['HotelAddress'])) + + # Add bill details in a more receipt-like format + # Use the first order for common details + first_order = orders[0] + + # Create a table for the bill header info + # Get customer name if available + customer_name = "" + if hasattr(first_order, 'person_name') and first_order.person_name: + customer_name = first_order.person_name + + bill_info_data = [ + ["Name:", customer_name], + [f"Date: {datetime.now().strftime('%d/%m/%y')}", f"Dine In: {first_order.table_number}"], + [f"{datetime.now().strftime('%H:%M')}", f"Bill No.: {first_order.id}"] + ] + + bill_info_table = Table(bill_info_data, colWidths=[doc.width/2-20, doc.width/2-20]) + bill_info_table.setStyle(TableStyle([ + ('FONT', (0, 0), (-1, -1), 'Helvetica', 8), + ('ALIGN', (0, 0), (0, -1), 'LEFT'), + ('ALIGN', (1, 0), (1, -1), 'RIGHT'), + ('LINEBELOW', (0, 0), (1, 0), 0.5, colors.black), + ])) + + elements.append(bill_info_table) + elements.append(Paragraph("_" * 50, styles['HotelAddress'])) + + # Create header for items table + items_header = [["Item", "Qty.", "Price", "Amount"]] + items_header_table = Table(items_header, colWidths=[doc.width*0.4, doc.width*0.15, doc.width*0.2, doc.width*0.25]) + items_header_table.setStyle(TableStyle([ + ('FONT', (0, 0), (-1, -1), 'Helvetica-Bold', 8), + ('ALIGN', (0, 0), (0, -1), 'LEFT'), + ('ALIGN', (1, 0), (-1, -1), 'RIGHT'), + ('LINEBELOW', (0, 0), (-1, 0), 0.5, colors.black), + ])) + + elements.append(items_header_table) + + # Add all order items + total_items = 0 + grand_total = 0 + + for order in orders: + order_data = [] + + for item in order.items: + dish_name = item.dish.name if item.dish else "Unknown Dish" + price = item.dish.price if item.dish else 0 + quantity = item.quantity + total = price * quantity + grand_total += total + total_items += quantity + + order_data.append([ + dish_name, + str(quantity), + f"{price:.2f}", + f"{total:.2f}" + ]) + + # Create the table for this order's items + if order_data: + items_table = Table(order_data, colWidths=[doc.width*0.4, doc.width*0.15, doc.width*0.2, doc.width*0.25]) + items_table.setStyle(TableStyle([ + ('FONT', (0, 0), (-1, -1), 'Helvetica', 8), + ('ALIGN', (0, 0), (0, -1), 'LEFT'), + ('ALIGN', (1, 0), (-1, -1), 'RIGHT'), + ])) + + elements.append(items_table) + + # Add a separator line + elements.append(Paragraph("_" * 50, styles['HotelAddress'])) + + # Add totals section + # Calculate tax (assuming 5% CGST and 5% SGST like in the image) + tax_rate = 0.05 # 5% + cgst = grand_total * tax_rate + sgst = grand_total * tax_rate + subtotal = grand_total - cgst - sgst + + totals_data = [ + [f"Total Qty: {total_items}", f"Sub Total", f"{subtotal:.2f}"], + ["", f"CGST", f"{cgst:.2f}"], + ["", f"SGST", f"{sgst:.2f}"], + ] + + totals_table = Table(totals_data, colWidths=[doc.width*0.4, doc.width*0.35, doc.width*0.25]) + totals_table.setStyle(TableStyle([ + ('FONT', (0, 0), (-1, -1), 'Helvetica', 8), + ('ALIGN', (0, 0), (0, -1), 'LEFT'), + ('ALIGN', (1, 0), (1, -1), 'RIGHT'), + ('ALIGN', (2, 0), (2, -1), 'RIGHT'), + ])) + + elements.append(totals_table) + + # Add grand total with emphasis + elements.append(Paragraph("_" * 50, styles['HotelAddress'])) + elements.append(Paragraph(f"Grand Total ₹{grand_total:.2f}", styles['Total'])) + elements.append(Paragraph("_" * 50, styles['HotelAddress'])) + + # Add license info and thank you message + elements.append(Spacer(1, 5)) + elements.append(Paragraph("FSSAI Lic No: 12018033000205", styles['Footer'])) + elements.append(Paragraph("!!! Thank You !!! Visit Again !!!", styles['Footer'])) + + # Build the PDF + doc.build(elements) + buffer.seek(0) + + return buffer diff --git a/check_firewall.py b/check_firewall.py new file mode 100644 index 0000000000000000000000000000000000000000..1bc10380ce250613b75b3085c9f80f301f1db684 --- /dev/null +++ b/check_firewall.py @@ -0,0 +1,84 @@ +import subprocess +import sys +import os + +def is_admin(): + """Check if the script is running with administrator privileges.""" + try: + return os.getuid() == 0 + except AttributeError: + # Windows + import ctypes + return ctypes.windll.shell32.IsUserAnAdmin() != 0 + +def check_firewall_rule(): + """Check if there's a firewall rule for port 8000.""" + try: + if sys.platform == 'win32': + # Windows + result = subprocess.run( + ['netsh', 'advfirewall', 'firewall', 'show', 'rule', 'name=TabbleApp'], + capture_output=True, + text=True + ) + return 'TabbleApp' in result.stdout + else: + # Linux/Mac + result = subprocess.run( + ['sudo', 'iptables', '-L', '-n'], + capture_output=True, + text=True + ) + return '8000' in result.stdout + except Exception as e: + print(f"Error checking firewall rule: {e}") + return False + +def add_firewall_rule(): + """Add a firewall rule to allow incoming connections on port 8000.""" + try: + if sys.platform == 'win32': + # Windows + subprocess.run([ + 'netsh', 'advfirewall', 'firewall', 'add', 'rule', + 'name=TabbleApp', + 'dir=in', + 'action=allow', + 'protocol=TCP', + 'localport=8000' + ], check=True) + print("Firewall rule added successfully.") + else: + # Linux/Mac + subprocess.run([ + 'sudo', 'iptables', '-A', 'INPUT', '-p', 'tcp', '--dport', '8000', + '-j', 'ACCEPT' + ], check=True) + print("Firewall rule added successfully.") + except Exception as e: + print(f"Error adding firewall rule: {e}") + print("Please manually add a firewall rule to allow incoming connections on port 8000.") + +if __name__ == "__main__": + print("\nChecking firewall settings for Tabble app...\n") + + if not is_admin(): + print("This script needs to be run with administrator privileges to check and modify firewall settings.") + print("Please run it again as administrator/root.") + sys.exit(1) + + if check_firewall_rule(): + print("Firewall rule for port 8000 already exists.") + else: + print("No firewall rule found for port 8000.") + response = input("Would you like to add a firewall rule to allow incoming connections on port 8000? (y/n): ") + + if response.lower() == 'y': + add_firewall_rule() + else: + print("\nNo changes made to firewall settings.") + print("Note: Other devices on your network may not be able to access the Tabble app.") + + print("\nFirewall check complete.") + print("If you're still having issues connecting from other devices, please check your network settings.") + print("You may need to manually configure your firewall to allow incoming connections on port 8000.") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..bd28266e2bdbfe176a7dc8151c74d5c1dbb682ed --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.8' + +services: + backend: + build: . + ports: + - "7860:7860" + environment: + - PORT=7860 + - HOST=0.0.0.0 + volumes: + - ./app:/code/app + - ./templates:/code/templates + - ./tabble_new.db:/code/tabble_new.db + restart: unless-stopped \ No newline at end of file diff --git a/fix_selection_offers.py b/fix_selection_offers.py new file mode 100644 index 0000000000000000000000000000000000000000..799e52fb0a5578ee162d7989518f38a53fbe24b7 --- /dev/null +++ b/fix_selection_offers.py @@ -0,0 +1,119 @@ +import sqlite3 +import os + +def fix_selection_offers(): + db_path = "tabble_new.db" + + if not os.path.exists(db_path): + print(f"Database file {db_path} not found!") + return False + + print(f"Fixing selection_offers table in {db_path}...") + + # Connect to the database + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + try: + # Check if the selection_offers table exists + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='selection_offers'") + if not cursor.fetchone(): + print("selection_offers table does not exist! Creating it...") + + # Create the selection_offers table with all required columns + cursor.execute(""" + CREATE TABLE selection_offers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + min_amount FLOAT NOT NULL, + discount_amount FLOAT NOT NULL, + is_active BOOLEAN DEFAULT 1, + description TEXT, + created_at TIMESTAMP, + updated_at TIMESTAMP + ) + """) + + # Add sample data + cursor.execute(""" + INSERT INTO selection_offers (min_amount, discount_amount, is_active, description, created_at, updated_at) + VALUES + (50.0, 5.0, 1, 'Spend $50, get $5 off', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + (100.0, 15.0, 1, 'Spend $100, get $15 off', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + (150.0, 25.0, 1, 'Spend $150, get $25 off', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + """) + + conn.commit() + print("Created selection_offers table with sample data.") + else: + # Check if the discount_amount column exists + cursor.execute("PRAGMA table_info(selection_offers)") + columns = cursor.fetchall() + column_names = [column[1] for column in columns] + + if "discount_amount" not in column_names: + print("Adding discount_amount column to selection_offers table...") + + # Create a new table with the correct schema + cursor.execute(""" + CREATE TABLE selection_offers_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + min_amount FLOAT NOT NULL, + discount_amount FLOAT NOT NULL DEFAULT 0, + is_active BOOLEAN DEFAULT 1, + description TEXT, + created_at TIMESTAMP, + updated_at TIMESTAMP + ) + """) + + # Copy data from the old table to the new one + cursor.execute(""" + INSERT INTO selection_offers_new (id, min_amount, is_active, description, created_at, updated_at) + SELECT id, min_amount, is_active, description, created_at, updated_at FROM selection_offers + """) + + # Update the discount_amount based on min_amount (as a simple rule) + cursor.execute(""" + UPDATE selection_offers_new + SET discount_amount = CASE + WHEN min_amount <= 50 THEN 5.0 + WHEN min_amount <= 100 THEN 15.0 + ELSE 25.0 + END + """) + + # Drop the old table and rename the new one + cursor.execute("DROP TABLE selection_offers") + cursor.execute("ALTER TABLE selection_offers_new RENAME TO selection_offers") + + conn.commit() + print("Added discount_amount column and populated with values.") + else: + print("discount_amount column already exists. No changes needed.") + + # Verify the table structure + cursor.execute("PRAGMA table_info(selection_offers)") + columns = cursor.fetchall() + print("\nCurrent selection_offers table structure:") + for column in columns: + print(f" {column[1]} ({column[2]})") + + # Show the data + cursor.execute("SELECT * FROM selection_offers") + rows = cursor.fetchall() + print("\nCurrent selection_offers data:") + for row in rows: + print(f" {row}") + + print("\nFix completed successfully!") + return True + + except sqlite3.Error as e: + print(f"SQLite error: {e}") + conn.rollback() + return False + finally: + conn.close() + +if __name__ == "__main__": + fix_selection_offers() diff --git a/get_ip.py b/get_ip.py new file mode 100644 index 0000000000000000000000000000000000000000..b8e57d121a7cf7b2212f83bae15aaabf05e9396c --- /dev/null +++ b/get_ip.py @@ -0,0 +1,28 @@ +import socket + +def get_ip_address(): + """Get the local IP address of the machine.""" + print("Starting IP address detection...") + try: + # Create a socket connection to an external server + print("Creating socket connection...") + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # Doesn't need to be reachable + print("Connecting to Google DNS (8.8.8.8)...") + s.connect(("8.8.8.8", 80)) + print("Getting local socket name...") + ip_address = s.getsockname()[0] + s.close() + print(f"Successfully detected IP: {ip_address}") + return ip_address + except Exception as e: + print(f"Error getting IP address: {e}") + return "127.0.0.1" # Return localhost if there's an error + +if __name__ == "__main__": + print("\nStarting Tabble IP detection...") + ip = get_ip_address() + print("\nYour IP Address:", ip) + print(f"\nYou can access the Tabble app at: http://{ip}:8000\n") + print("Share this URL with other devices on your network to access the application.") + print("Note: Make sure your firewall allows incoming connections on port 8000.") diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000000000000000000000000000000000000..9b8d495807880f5a77ffeee680af1065587f9885 --- /dev/null +++ b/init_db.py @@ -0,0 +1,354 @@ +from app.database import ( + create_tables, + SessionLocal, + Dish, + Person, + Base, + LoyaltyProgram, + SelectionOffer, + Table, +) +from sqlalchemy import create_engine +from datetime import datetime, timezone +import os +import sys + + +def init_db(force_reset=False): + # Check if force_reset is enabled + if force_reset: + # Drop all tables and recreate them + print("Forcing database reset...") + Base.metadata.drop_all( + bind=create_engine( + "sqlite:///./tabble_new.db", connect_args={"check_same_thread": False} + ) + ) + + # Create tables + create_tables() + + # Create a database session + db = SessionLocal() + + # Check if dishes already exist + existing_dishes = db.query(Dish).count() + if existing_dishes > 0: + print("Database already contains data. Skipping initialization.") + return + + # Add sample dishes + sample_dishes = [ + # Regular dishes + Dish( + name="Margherita Pizza", + description="Classic pizza with tomato sauce, mozzarella, and basil", + category="Main Course", + price=12.99, + quantity=20, + image_path="/static/images/default-dish.jpg", + discount=0, + is_offer=0, + is_special=0, + ), + Dish( + name="Caesar Salad", + description="Fresh romaine lettuce with Caesar dressing, croutons, and parmesan", + category="Appetizer", + price=8.99, + quantity=15, + image_path="/static/images/default-dish.jpg", + discount=0, + is_offer=0, + is_special=0, + ), + Dish( + name="Chocolate Cake", + description="Rich chocolate cake with ganache frosting", + category="Dessert", + price=6.99, + quantity=10, + image_path="/static/images/default-dish.jpg", + discount=0, + is_offer=0, + is_special=0, + ), + Dish( + name="Iced Tea", + description="Refreshing iced tea with lemon", + category="Beverage", + price=3.99, + quantity=30, + image_path="/static/images/default-dish.jpg", + discount=0, + is_offer=0, + is_special=0, + ), + Dish( + name="Chicken Alfredo", + description="Fettuccine pasta with creamy Alfredo sauce and grilled chicken", + category="Main Course", + price=15.99, + quantity=12, + image_path="/static/images/default-dish.jpg", + discount=0, + is_offer=0, + ), + Dish( + name="Garlic Bread", + description="Toasted bread with garlic butter and herbs", + category="Appetizer", + price=4.99, + quantity=25, + image_path="/static/images/default-dish.jpg", + discount=0, + is_offer=0, + is_special=0, + ), + # Special offer dishes + Dish( + name="Weekend Special Pizza", + description="Deluxe pizza with premium toppings and extra cheese", + category="Main Course", + price=18.99, + quantity=15, + image_path="/static/images/default-dish.jpg", + discount=20, + is_offer=1, + is_special=0, + ), + Dish( + name="Seafood Pasta", + description="Fresh pasta with mixed seafood in a creamy sauce", + category="Main Course", + price=22.99, + quantity=10, + image_path="/static/images/default-dish.jpg", + discount=15, + is_offer=1, + is_special=0, + ), + Dish( + name="Tiramisu", + description="Classic Italian dessert with coffee-soaked ladyfingers and mascarpone cream", + category="Dessert", + price=9.99, + quantity=8, + image_path="/static/images/default-dish.jpg", + discount=25, + is_offer=1, + is_special=0, + ), + # Today's special dishes + Dish( + name="Chef's Special Steak", + description="Prime cut steak cooked to perfection with special house seasoning", + category="Main Course", + price=24.99, + quantity=12, + image_path="/static/images/default-dish.jpg", + discount=0, + is_offer=0, + is_special=1, + ), + Dish( + name="Truffle Mushroom Risotto", + description="Creamy risotto with wild mushrooms and truffle oil", + category="Main Course", + price=16.99, + quantity=10, + image_path="/static/images/default-dish.jpg", + discount=0, + is_offer=0, + is_special=1, + ), + Dish( + name="Chocolate Lava Cake", + description="Warm chocolate cake with a molten center, served with vanilla ice cream", + category="Dessert", + price=8.99, + quantity=15, + image_path="/static/images/default-dish.jpg", + discount=0, + is_offer=0, + is_special=1, + ), + ] + + # Add dishes to database + for dish in sample_dishes: + db.add(dish) + + # Add sample users + sample_users = [ + Person( + username="john_doe", + password="password123", + visit_count=1, + last_visit=datetime.now(timezone.utc), + ), + Person( + username="jane_smith", + password="password456", + visit_count=3, + last_visit=datetime.now(timezone.utc), + ), + Person( + username="guest", + password="guest", + visit_count=5, + last_visit=datetime.now(timezone.utc), + ), + ] + + # Add users to database + for user in sample_users: + db.add(user) + + # Add sample loyalty program tiers + sample_loyalty_tiers = [ + LoyaltyProgram( + visit_count=3, + discount_percentage=5.0, + is_active=True, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ), + LoyaltyProgram( + visit_count=5, + discount_percentage=10.0, + is_active=True, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ), + LoyaltyProgram( + visit_count=10, + discount_percentage=15.0, + is_active=True, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ), + LoyaltyProgram( + visit_count=20, + discount_percentage=20.0, + is_active=True, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ), + ] + + # Add loyalty tiers to database + for tier in sample_loyalty_tiers: + db.add(tier) + + # Add sample selection offers + sample_selection_offers = [ + SelectionOffer( + min_amount=50.0, + discount_amount=5.0, + is_active=True, + description="Spend $50, get $5 off", + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ), + SelectionOffer( + min_amount=100.0, + discount_amount=15.0, + is_active=True, + description="Spend $100, get $15 off", + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ), + SelectionOffer( + min_amount=150.0, + discount_amount=25.0, + is_active=True, + description="Spend $150, get $25 off", + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ), + ] + + # Add selection offers to database + for offer in sample_selection_offers: + db.add(offer) + + # Add sample tables + sample_tables = [ + Table( + table_number=1, + is_occupied=False, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ), + Table( + table_number=2, + is_occupied=False, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ), + Table( + table_number=3, + is_occupied=False, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ), + Table( + table_number=4, + is_occupied=False, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ), + Table( + table_number=5, + is_occupied=False, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ), + Table( + table_number=6, + is_occupied=False, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ), + Table( + table_number=7, + is_occupied=False, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ), + Table( + table_number=8, + is_occupied=False, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ), + ] + + # Add tables to database + for table in sample_tables: + db.add(table) + + # Commit changes + db.commit() + + print("Database initialized with sample data:") + print("- Added", len(sample_dishes), "sample dishes") + print("- Added", len(sample_users), "sample users") + print("- Added", len(sample_loyalty_tiers), "loyalty program tiers") + print("- Added", len(sample_selection_offers), "selection offers") + print("- Added", len(sample_tables), "tables") + + # Close session + db.close() + + +if __name__ == "__main__": + # Create static/images directory if it doesn't exist + os.makedirs("app/static/images", exist_ok=True) + + # Check for force reset flag + force_reset = "--force-reset" in sys.argv + + # Initialize database + init_db(force_reset) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..1b07890d0f5fca1ab557a0970af3580c5554d715 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +fastapi==0.104.1 +uvicorn==0.23.2 +sqlalchemy==2.0.27 +python-multipart==0.0.6 +jinja2==3.1.2 +python-dotenv==1.0.0 +reportlab==4.0.7 +pillow==10.1.0 diff --git a/run.py b/run.py new file mode 100644 index 0000000000000000000000000000000000000000..988c63c1bf5b9d569fd1c4f78c6f073bfebfb58b --- /dev/null +++ b/run.py @@ -0,0 +1,43 @@ +import uvicorn +import os +import socket + + +def get_ip_address(): + """Get the local IP address of the machine.""" + try: + # Create a socket connection to an external server + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + # Doesn't need to be reachable + s.connect(("8.8.8.8", 80)) + ip_address = s.getsockname()[0] + s.close() + return ip_address + except Exception as e: + print(f"Error getting IP address: {e}") + return "127.0.0.1" # Return localhost if there's an error + + +if __name__ == "__main__": + # Create static/images directory if it doesn't exist + os.makedirs("app/static/images", exist_ok=True) + + # Get host and port from environment variables (for Hugging Face Spaces) + host = os.getenv("HOST", "0.0.0.0") + port = int(os.getenv("PORT", "8000")) + + # Get the IP address for local development + ip_address = get_ip_address() + + # Display access information + print("\n" + "=" * 50) + print(f"Access from other devices at: http://{ip_address}:{port}") + print("=" * 50 + "\n") + + # Run the application + uvicorn.run( + "app.main:app", + host=host, + port=port, + reload=os.getenv("ENVIRONMENT") == "development" + ) diff --git a/tabble_new.db b/tabble_new.db new file mode 100644 index 0000000000000000000000000000000000000000..3213912b552a3b4e7e573486986a1a30771727e5 --- /dev/null +++ b/tabble_new.db @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ccf69911e369918d3ee1fa170d69eaf51487da931601d861e7491f07530924d6 +size 106496