2026-01-27 18:07:00 -06:00
from fastapi import FastAPI , APIRouter , HTTPException , Depends , status , Query , Response , UploadFile , File , Form
from fastapi . security import HTTPBearer , HTTPAuthorizationCredentials
from fastapi . responses import StreamingResponse
from fastapi . staticfiles import StaticFiles
from dotenv import load_dotenv
from starlette . middleware . cors import CORSMiddleware
from sqlalchemy . ext . asyncio import AsyncSession
from sqlalchemy import select , func , and_ , or_ , desc , asc , distinct , delete
from sqlalchemy . orm import selectinload
import os
import logging
2026-02-01 22:31:00 -06:00
import aiofiles
2026-01-27 18:07:00 -06:00
from pathlib import Path
from pydantic import BaseModel , Field , EmailStr , ConfigDict
from typing import List , Optional , Dict , Any
import uuid
from datetime import datetime , timezone , timedelta
import bcrypt
import jwt
import io
import csv
import base64
import shutil
2026-02-01 22:31:00 -06:00
import smtplib
import httpx
from email . mime . text import MIMEText
from email . mime . multipart import MIMEMultipart
2026-01-27 18:07:00 -06:00
from contextlib import asynccontextmanager
from reportlab . lib import colors
from reportlab . lib . pagesizes import letter , A4
from reportlab . platypus import SimpleDocTemplate , Table , TableStyle , Paragraph , Spacer
from reportlab . lib . styles import getSampleStyleSheet , ParagraphStyle
from reportlab . lib . units import inch
from database import get_db , init_db , AsyncSessionLocal
from models import (
User , Product , Service , CartItem , Order , OrderItem , OrderStatusHistory ,
Review , Booking , Contact , InventoryLog , Category , SalesReport ,
OrderStatus , UserRole , Base , ProductImage , ServiceImage ,
2026-02-01 22:31:00 -06:00
AboutContent , TeamMember , CompanyValue , Media , MediaType
2026-01-27 18:07:00 -06:00
)
ROOT_DIR = Path ( __file__ ) . parent
load_dotenv ( ROOT_DIR / ' .env ' )
# Create uploads directory for images
UPLOAD_DIR = ROOT_DIR / ' uploads ' / ' products '
UPLOAD_DIR . mkdir ( parents = True , exist_ok = True )
2026-02-01 22:31:00 -06:00
# Notification settings (from environment variables)
SMTP_HOST = os . environ . get ( ' SMTP_HOST ' , ' smtp.gmail.com ' )
SMTP_PORT = int ( os . environ . get ( ' SMTP_PORT ' , 587 ) )
SMTP_USER = os . environ . get ( ' SMTP_USER ' , ' ' )
SMTP_PASSWORD = os . environ . get ( ' SMTP_PASSWORD ' , ' ' )
ADMIN_EMAIL = os . environ . get ( ' ADMIN_EMAIL ' , ' admin@prompttech.com ' )
ADMIN_PHONE = os . environ . get ( ' ADMIN_PHONE ' , ' +5016261234 ' ) # WhatsApp number
WHATSAPP_API_URL = os . environ . get ( ' WHATSAPP_API_URL ' , ' ' ) # e.g., CallMeBot or Twilio
WHATSAPP_API_KEY = os . environ . get ( ' WHATSAPP_API_KEY ' , ' ' )
# Create media uploads directory
MEDIA_UPLOAD_DIR = ROOT_DIR / ' uploads ' / ' media '
MEDIA_UPLOAD_DIR . mkdir ( parents = True , exist_ok = True )
# Allowed file extensions for media
ALLOWED_IMAGE_EXTENSIONS = { ' .jpg ' , ' .jpeg ' , ' .png ' , ' .gif ' , ' .webp ' , ' .svg ' , ' .bmp ' , ' .ico ' }
ALLOWED_DOC_EXTENSIONS = { ' .pdf ' , ' .doc ' , ' .docx ' , ' .xls ' , ' .xlsx ' , ' .ppt ' , ' .pptx ' , ' .txt ' }
ALLOWED_VIDEO_EXTENSIONS = { ' .mp4 ' , ' .webm ' , ' .mov ' , ' .avi ' }
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50MB
2026-01-27 18:07:00 -06:00
# JWT Configuration
SECRET_KEY = os . environ . get ( ' JWT_SECRET ' , ' techzone-super-secret-key-2024-production ' )
ALGORITHM = " HS256 "
ACCESS_TOKEN_EXPIRE_HOURS = 24
# Configure logging
logging . basicConfig ( level = logging . INFO , format = ' %(asctime)s - %(name)s - %(levelname)s - %(message)s ' )
logger = logging . getLogger ( __name__ )
# Lifespan event handler for FastAPI
@asynccontextmanager
async def lifespan ( app : FastAPI ) :
""" Initialize database with error handling and verification """
try :
await init_db ( )
logger . info ( " Database initialized successfully " )
# Verify database connection
try :
async with AsyncSessionLocal ( ) as session :
result = await session . execute ( select ( func . count ( User . id ) ) )
user_count = result . scalar ( )
logger . info ( f " Database connection verified - { user_count } users found " )
except Exception as e :
logger . error ( f " Database verification failed: { e } " )
except Exception as e :
logger . critical ( f " Database initialization failed: { e } " , exc_info = True )
yield
# Cleanup on shutdown (if needed)
pass
# Create the main app with lifespan
app = FastAPI ( title = " TechZone API " , version = " 2.0.0 " , lifespan = lifespan )
# Create a router with the /api prefix
api_router = APIRouter ( prefix = " /api " )
security = HTTPBearer ( )
# ================== PYDANTIC MODELS ==================
class UserCreate ( BaseModel ) :
email : EmailStr
name : str
password : str
class UserLogin ( BaseModel ) :
email : EmailStr
password : str
class TokenResponse ( BaseModel ) :
access_token : str
token_type : str = " bearer "
user : dict
class ProductCreate ( BaseModel ) :
name : str
description : str # Now supports HTML from rich text editor
price : float
category : str
image_url : str = " " # Optional - for backwards compatibility
stock : int = 10
low_stock_threshold : int = 5
brand : str = " "
specs : dict = { }
images : List [ str ] = [ ] # List of image URLs
class ProductUpdate ( BaseModel ) :
name : Optional [ str ] = None
description : Optional [ str ] = None # Supports HTML
price : Optional [ float ] = None
category : Optional [ str ] = None
image_url : Optional [ str ] = None
stock : Optional [ int ] = None
low_stock_threshold : Optional [ int ] = None
brand : Optional [ str ] = None
specs : Optional [ dict ] = None
is_active : Optional [ bool ] = None
images : Optional [ List [ str ] ] = None # List of image URLs
class CategoryCreate ( BaseModel ) :
name : str
slug : Optional [ str ] = None
description : Optional [ str ] = " "
class CategoryUpdate ( BaseModel ) :
name : Optional [ str ] = None
slug : Optional [ str ] = None
description : Optional [ str ] = None
class ServiceCreate ( BaseModel ) :
name : str
description : str
price : float
duration : str
image_url : Optional [ str ] = " " # Deprecated
category : str
images : List [ str ] = [ ]
class ServiceUpdate ( BaseModel ) :
name : Optional [ str ] = None
description : Optional [ str ] = None
price : Optional [ float ] = None
duration : Optional [ str ] = None
image_url : Optional [ str ] = None # Deprecated
category : Optional [ str ] = None
images : Optional [ List [ str ] ] = None
category : Optional [ str ] = None
images : Optional [ List [ str ] ] = None
is_active : Optional [ bool ] = None
class CartItemCreate ( BaseModel ) :
product_id : str
quantity : int = 1
class OrderCreate ( BaseModel ) :
shipping_address : dict = { }
notes : str = " "
class OrderStatusUpdate ( BaseModel ) :
status : str
notes : str = " "
tracking_number : Optional [ str ] = None
class ReviewCreate ( BaseModel ) :
product_id : Optional [ str ] = None
service_id : Optional [ str ] = None
rating : int
title : str = " "
comment : str = " "
class BookingCreate ( BaseModel ) :
service_id : str
name : str
email : EmailStr
phone : str
preferred_date : str
notes : str = " "
class ContactCreate ( BaseModel ) :
name : str
email : EmailStr
subject : str
message : str
class InventoryAdjust ( BaseModel ) :
quantity_change : int
notes : str = " "
# ================== HELPERS ==================
def hash_password ( password : str ) - > str :
return bcrypt . hashpw ( password . encode ( ' utf-8 ' ) , bcrypt . gensalt ( ) ) . decode ( ' utf-8 ' )
def verify_password ( password : str , hashed : str ) - > bool :
return bcrypt . checkpw ( password . encode ( ' utf-8 ' ) , hashed . encode ( ' utf-8 ' ) )
def create_access_token ( data : dict ) - > str :
to_encode = data . copy ( )
expire = datetime . now ( timezone . utc ) + timedelta ( hours = ACCESS_TOKEN_EXPIRE_HOURS )
to_encode . update ( { " exp " : expire } )
return jwt . encode ( to_encode , SECRET_KEY , algorithm = ALGORITHM )
async def get_current_user ( credentials : HTTPAuthorizationCredentials = Depends ( security ) , db : AsyncSession = Depends ( get_db ) ) :
try :
payload = jwt . decode ( credentials . credentials , SECRET_KEY , algorithms = [ ALGORITHM ] )
user_id = payload . get ( " sub " )
if user_id is None :
raise HTTPException ( status_code = 401 , detail = " Invalid token " )
result = await db . execute ( select ( User ) . where ( User . id == user_id ) )
user = result . scalar_one_or_none ( )
if user is None :
raise HTTPException ( status_code = 401 , detail = " User not found " )
return user
except jwt . ExpiredSignatureError :
raise HTTPException ( status_code = 401 , detail = " Token expired " )
except jwt . InvalidTokenError :
raise HTTPException ( status_code = 401 , detail = " Invalid token " )
async def get_admin_user ( user : User = Depends ( get_current_user ) ) :
""" Verify user has admin role with logging """
if not user :
logger . warning ( " Admin access attempted with no user " )
raise HTTPException ( status_code = 401 , detail = " Authentication required " )
if user . role != UserRole . ADMIN :
logger . warning ( f " Non-admin user { user . id } attempted admin access " )
raise HTTPException ( status_code = 403 , detail = " Admin access required " )
logger . debug ( f " Admin access granted to user { user . id } " )
return user
async def get_optional_user ( credentials : Optional [ HTTPAuthorizationCredentials ] = Depends ( HTTPBearer ( auto_error = False ) ) , db : AsyncSession = Depends ( get_db ) ) :
if credentials is None :
return None
try :
payload = jwt . decode ( credentials . credentials , SECRET_KEY , algorithms = [ ALGORITHM ] )
user_id = payload . get ( " sub " )
if user_id :
result = await db . execute ( select ( User ) . where ( User . id == user_id ) )
return result . scalar_one_or_none ( )
except :
pass
return None
# ================== CRUD HELPERS ==================
async def _get_or_404 ( db : AsyncSession , model , record_id : str , error_message : str = " Record not found " ) :
""" Generic helper to fetch a record by ID or raise 404 """
result = await db . execute ( select ( model ) . where ( model . id == record_id ) )
record = result . scalar_one_or_none ( )
if not record :
raise HTTPException ( status_code = 404 , detail = error_message )
return record
async def _soft_delete ( db : AsyncSession , record , commit : bool = True ) :
""" Generic helper for soft delete (set is_active=False) """
record . is_active = False
if commit :
await db . commit ( )
return { " message " : f " { record . __class__ . __name__ } deleted " }
def _build_response ( message : str , * * kwargs ) :
""" Build standardized API response """
response = { " message " : message }
response . update ( kwargs )
return response
# ================== SERIALIZATION HELPERS ==================
def _safe_isoformat ( dt ) - > Optional [ str ] :
""" Safely convert datetime to ISO format """
return dt . isoformat ( ) if dt else None
def _safe_enum_value ( enum_val , default = " pending " ) - > str :
""" Safely extract enum value """
return enum_val . value if enum_val else default
def _calculate_reviews_stats ( reviews : list ) - > dict :
""" Calculate review statistics """
if not reviews :
return { " average_rating " : 0 , " review_count " : 0 }
return {
" average_rating " : sum ( r . rating for r in reviews ) / len ( reviews ) ,
" review_count " : len ( reviews )
}
def user_to_dict ( user : User ) - > dict :
return {
" id " : user . id ,
" email " : user . email ,
" name " : user . name ,
" role " : _safe_enum_value ( user . role , " user " ) ,
" is_active " : getattr ( user , ' is_active ' , True ) ,
" created_at " : _safe_isoformat ( user . created_at )
}
def product_to_dict ( product : Product , include_reviews : bool = False ) - > dict :
# Get images if available
images = [ ]
if hasattr ( product , ' images ' ) and product . images :
images = [
{
" id " : img . id ,
" url " : img . image_url ,
" display_order " : img . display_order ,
" is_primary " : img . is_primary
}
for img in sorted ( product . images , key = lambda x : x . display_order )
]
data = {
" id " : product . id ,
" name " : product . name ,
" description " : product . description , # Contains HTML from rich text editor
" price " : product . price ,
" category " : product . category ,
" image_url " : product . image_url or ( images [ 0 ] [ " url " ] if images else " " ) , # Fallback to first image
" images " : images , # New: array of all images
" stock " : product . stock ,
" low_stock_threshold " : product . low_stock_threshold ,
" brand " : product . brand ,
" specs " : product . specs or { } ,
" is_active " : product . is_active ,
" created_at " : _safe_isoformat ( product . created_at )
}
if include_reviews and product . reviews :
data [ " reviews " ] = [ review_to_dict ( r ) for r in product . reviews ]
data . update ( _calculate_reviews_stats ( product . reviews ) )
return data
def service_to_dict ( service : Service , include_reviews : bool = False ) - > dict :
# Get images if available
images = [ ]
if hasattr ( service , ' images ' ) and service . images :
images = [
{
" id " : img . id ,
" url " : img . image_url ,
" display_order " : img . display_order ,
" is_primary " : img . is_primary
}
for img in sorted ( service . images , key = lambda x : x . display_order )
]
# Set primary image_url for backwards compatibility
primary_image = images [ 0 ] [ " url " ] if images else service . image_url
data = {
" id " : service . id ,
" name " : service . name ,
" description " : service . description ,
" price " : service . price ,
" duration " : service . duration ,
" image_url " : primary_image ,
" images " : images ,
" category " : service . category ,
" is_active " : service . is_active ,
" created_at " : _safe_isoformat ( service . created_at )
}
if include_reviews and service . reviews :
data [ " reviews " ] = [ review_to_dict ( r ) for r in service . reviews ]
data . update ( _calculate_reviews_stats ( service . reviews ) )
return data
def order_to_dict ( order : Order ) - > dict :
return {
" id " : order . id ,
" user_id " : order . user_id ,
" status " : _safe_enum_value ( order . status ) ,
" subtotal " : order . subtotal ,
" tax " : order . tax ,
" shipping " : order . shipping ,
" total " : order . total ,
" shipping_address " : order . shipping_address or { } ,
" notes " : order . notes ,
" tracking_number " : order . tracking_number ,
" created_at " : _safe_isoformat ( order . created_at ) ,
" updated_at " : _safe_isoformat ( order . updated_at ) ,
" items " : [ order_item_to_dict ( item ) for item in order . items ] if order . items else [ ] ,
" status_history " : [ status_history_to_dict ( h ) for h in order . status_history ] if order . status_history else [ ]
}
def order_item_to_dict ( item : OrderItem ) - > dict :
return {
" id " : item . id ,
" product_id " : item . product_id ,
" product_name " : item . product_name ,
" product_image " : item . product_image ,
" quantity " : item . quantity ,
" price " : item . price
}
def status_history_to_dict ( history : OrderStatusHistory ) - > dict :
return {
" id " : history . id ,
" status " : _safe_enum_value ( history . status , None ) ,
" notes " : history . notes ,
" created_at " : _safe_isoformat ( history . created_at )
}
def review_to_dict ( review : Review ) - > dict :
return {
" id " : review . id ,
" user_id " : review . user_id ,
" user_name " : review . user . name if review . user else " Anonymous " ,
" product_id " : review . product_id ,
" service_id " : review . service_id ,
" rating " : review . rating ,
" title " : review . title ,
" comment " : review . comment ,
" is_verified_purchase " : review . is_verified_purchase ,
" created_at " : _safe_isoformat ( review . created_at )
}
def booking_to_dict ( booking : Booking ) - > dict :
return {
" id " : booking . id ,
" service_id " : booking . service_id ,
" service_name " : booking . service_name ,
" name " : booking . name ,
" email " : booking . email ,
" phone " : booking . phone ,
" preferred_date " : booking . preferred_date ,
" notes " : booking . notes ,
" status " : booking . status ,
2026-02-01 22:31:00 -06:00
" created_at " : booking . created_at . isoformat ( ) if booking . created_at else None ,
" completed_at " : booking . completed_at . isoformat ( ) if booking . completed_at else None ,
" diagnosis " : booking . diagnosis ,
" work_performed " : booking . work_performed ,
" technician_notes " : booking . technician_notes ,
" service_cost " : booking . service_cost ,
" paid " : booking . paid ,
" paid_at " : booking . paid_at . isoformat ( ) if booking . paid_at else None ,
" device_model " : booking . device_model ,
" serial_number " : booking . serial_number ,
" product_number " : booking . product_number ,
" screen_size " : booking . screen_size
2026-01-27 18:07:00 -06:00
}
2026-02-01 22:31:00 -06:00
# Booking completion model
class BookingComplete ( BaseModel ) :
diagnosis : Optional [ str ] = None
work_performed : Optional [ str ] = None
technician_notes : Optional [ str ] = None
service_cost : Optional [ float ] = None
paid : Optional [ bool ] = None
device_model : Optional [ str ] = None
serial_number : Optional [ str ] = None
product_number : Optional [ str ] = None
screen_size : Optional [ str ] = None
# ================== NOTIFICATION FUNCTIONS ==================
async def send_email_notification ( subject : str , body : str , to_email : str = None ) :
""" Send email notification to admin """
if not SMTP_USER or not SMTP_PASSWORD :
logger . warning ( " Email notification skipped - SMTP not configured " )
return False
try :
to_email = to_email or ADMIN_EMAIL
msg = MIMEMultipart ( )
msg [ ' From ' ] = SMTP_USER
msg [ ' To ' ] = to_email
msg [ ' Subject ' ] = subject
msg . attach ( MIMEText ( body , ' html ' ) )
with smtplib . SMTP ( SMTP_HOST , SMTP_PORT ) as server :
server . starttls ( )
server . login ( SMTP_USER , SMTP_PASSWORD )
server . send_message ( msg )
logger . info ( f " Email notification sent to { to_email } " )
return True
except Exception as e :
logger . error ( f " Failed to send email: { e } " )
return False
async def send_whatsapp_notification ( message : str , phone : str = None ) :
""" Send WhatsApp notification using CallMeBot API or similar service """
if not WHATSAPP_API_URL or not WHATSAPP_API_KEY :
logger . warning ( " WhatsApp notification skipped - API not configured " )
return False
try :
phone = phone or ADMIN_PHONE
# CallMeBot API format: https://api.callmebot.com/whatsapp.php?phone=PHONE&text=MESSAGE&apikey=KEY
async with httpx . AsyncClient ( ) as client :
params = {
' phone ' : phone . replace ( ' + ' , ' ' ) ,
' text ' : message ,
' apikey ' : WHATSAPP_API_KEY
}
response = await client . get ( WHATSAPP_API_URL , params = params , timeout = 10 )
if response . status_code == 200 :
logger . info ( f " WhatsApp notification sent to { phone } " )
return True
else :
logger . error ( f " WhatsApp API error: { response . status_code } " )
return False
except Exception as e :
logger . error ( f " Failed to send WhatsApp: { e } " )
return False
async def notify_booking ( booking_data : dict , service_name : str ) :
""" Send booking notifications via email and WhatsApp """
# Format the booking details
booking_date = booking_data . get ( ' preferred_date ' , ' Not specified ' )
customer_name = booking_data . get ( ' name ' , ' Unknown ' )
customer_email = booking_data . get ( ' email ' , ' Not provided ' )
customer_phone = booking_data . get ( ' phone ' , ' Not provided ' )
notes = booking_data . get ( ' notes ' , ' None ' )
# Email body (HTML)
email_body = f """
< html >
< body style = " font-family: Arial, sans-serif; padding: 20px; " >
< h2 style = " color: #2563eb; " > New Service Booking ! < / h2 >
< p > A customer has booked a service on your website . < / p >
< table style = " border-collapse: collapse; width: 100 % ; max-width: 500px; " >
< tr > < td style = " padding: 8px; font-weight: bold; border-bottom: 1px solid #eee; " > Service : < / td >
< td style = " padding: 8px; border-bottom: 1px solid #eee; " > { service_name } < / td > < / tr >
< tr > < td style = " padding: 8px; font-weight: bold; border-bottom: 1px solid #eee; " > Customer Name : < / td >
< td style = " padding: 8px; border-bottom: 1px solid #eee; " > { customer_name } < / td > < / tr >
< tr > < td style = " padding: 8px; font-weight: bold; border-bottom: 1px solid #eee; " > Email : < / td >
< td style = " padding: 8px; border-bottom: 1px solid #eee; " > { customer_email } < / td > < / tr >
< tr > < td style = " padding: 8px; font-weight: bold; border-bottom: 1px solid #eee; " > Phone : < / td >
< td style = " padding: 8px; border-bottom: 1px solid #eee; " > { customer_phone } < / td > < / tr >
< tr > < td style = " padding: 8px; font-weight: bold; border-bottom: 1px solid #eee; " > Preferred Date : < / td >
< td style = " padding: 8px; border-bottom: 1px solid #eee; " > { booking_date } < / td > < / tr >
< tr > < td style = " padding: 8px; font-weight: bold; border-bottom: 1px solid #eee; " > Notes : < / td >
< td style = " padding: 8px; border-bottom: 1px solid #eee; " > { notes } < / td > < / tr >
< / table >
< p style = " margin-top: 20px; " > Please reach out to the customer as soon as possible . < / p >
< p style = " color: #666; " > - PromptTech Solutions < / p >
< / body >
< / html >
"""
# WhatsApp message (plain text)
whatsapp_msg = f """ 🔔 NEW BOOKING!
Service : { service_name }
Customer : { customer_name }
Phone : { customer_phone }
Email : { customer_email }
Date : { booking_date }
Notes : { notes }
Please reach out to the customer . """
# Send both notifications (don't block on failures)
await send_email_notification (
subject = f " New Booking: { service_name } - { customer_name } " ,
body = email_body
)
await send_whatsapp_notification ( whatsapp_msg )
2026-01-27 18:07:00 -06:00
# ================== AUTH ROUTES ==================
@api_router.post ( " /auth/register " , response_model = TokenResponse )
async def register ( user_data : UserCreate , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute ( select ( User ) . where ( User . email == user_data . email ) )
if result . scalar_one_or_none ( ) :
raise HTTPException ( status_code = 400 , detail = " Email already registered " )
user = User (
email = user_data . email ,
name = user_data . name ,
password = hash_password ( user_data . password ) ,
role = UserRole . USER
)
db . add ( user )
await db . commit ( )
await db . refresh ( user )
token = create_access_token ( { " sub " : user . id } )
return TokenResponse ( access_token = token , user = user_to_dict ( user ) )
@api_router.post ( " /auth/login " , response_model = TokenResponse )
async def login ( credentials : UserLogin , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute ( select ( User ) . where ( User . email == credentials . email ) )
user = result . scalar_one_or_none ( )
if not user or not verify_password ( credentials . password , user . password ) :
raise HTTPException ( status_code = 401 , detail = " Invalid credentials " )
token = create_access_token ( { " sub " : user . id } )
return TokenResponse ( access_token = token , user = user_to_dict ( user ) )
@api_router.get ( " /auth/me " )
async def get_me ( user : User = Depends ( get_current_user ) ) :
return user_to_dict ( user )
# ================== PRODUCTS ROUTES ==================
@api_router.get ( " /products " )
async def get_products (
response : Response ,
category : Optional [ str ] = None ,
search : Optional [ str ] = None ,
min_price : Optional [ float ] = None ,
max_price : Optional [ float ] = None ,
in_stock : Optional [ bool ] = None ,
db : AsyncSession = Depends ( get_db )
) :
# Add cache headers for better performance
response . headers [ " Cache-Control " ] = " public, max-age=60 " # Cache for 60 seconds
query = select ( Product ) . where ( Product . is_active == True ) . options ( selectinload ( Product . images ) )
if category and category != " all " :
query = query . where ( Product . category == category )
if search :
query = query . where (
or_ (
Product . name . ilike ( f " % { search } % " ) ,
Product . description . ilike ( f " % { search } % " ) ,
Product . brand . ilike ( f " % { search } % " )
)
)
if min_price is not None :
query = query . where ( Product . price > = min_price )
if max_price is not None :
query = query . where ( Product . price < = max_price )
if in_stock :
query = query . where ( Product . stock > 0 )
query = query . options ( selectinload ( Product . reviews ) . selectinload ( Review . user ) )
result = await db . execute ( query )
products = result . scalars ( ) . all ( )
return [ product_to_dict ( p , include_reviews = True ) for p in products ]
@api_router.get ( " /products/ {product_id} " )
async def get_product ( product_id : str , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute (
select ( Product )
. where ( Product . id == product_id )
. options (
selectinload ( Product . images ) ,
selectinload ( Product . reviews ) . selectinload ( Review . user )
)
)
product = result . scalar_one_or_none ( )
if not product :
raise HTTPException ( status_code = 404 , detail = " Product not found " )
return product_to_dict ( product , include_reviews = True )
@api_router.get ( " /products/categories/list " )
async def get_product_categories ( db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute ( select ( Product . category ) . distinct ( ) )
categories = [ row [ 0 ] for row in result . fetchall ( ) ]
return categories
# ================== SERVICES ROUTES ==================
@api_router.get ( " /services " )
async def get_services (
response : Response ,
category : Optional [ str ] = None ,
db : AsyncSession = Depends ( get_db )
) :
# Add cache headers for better performance
response . headers [ " Cache-Control " ] = " public, max-age=60 " # Cache for 60 seconds
query = select ( Service ) . where ( Service . is_active == True ) . options ( selectinload ( Service . images ) )
if category and category != " all " :
query = query . where ( Service . category == category )
query = query . options ( selectinload ( Service . reviews ) . selectinload ( Review . user ) )
result = await db . execute ( query )
services = result . scalars ( ) . all ( )
return [ service_to_dict ( s , include_reviews = True ) for s in services ]
@api_router.get ( " /services/ {service_id} " )
async def get_service ( service_id : str , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute (
select ( Service )
. where ( Service . id == service_id )
. options (
selectinload ( Service . images ) ,
selectinload ( Service . reviews ) . selectinload ( Review . user )
)
)
service = result . scalar_one_or_none ( )
if not service :
raise HTTPException ( status_code = 404 , detail = " Service not found " )
return service_to_dict ( service , include_reviews = True )
@api_router.post ( " /services/book " )
async def book_service (
booking_data : BookingCreate ,
user : Optional [ User ] = Depends ( get_optional_user ) ,
db : AsyncSession = Depends ( get_db )
) :
result = await db . execute ( select ( Service ) . where ( Service . id == booking_data . service_id ) )
service = result . scalar_one_or_none ( )
if not service :
raise HTTPException ( status_code = 404 , detail = " Service not found " )
booking = Booking (
service_id = booking_data . service_id ,
user_id = user . id if user else None ,
name = booking_data . name ,
email = booking_data . email ,
phone = booking_data . phone ,
preferred_date = booking_data . preferred_date ,
notes = booking_data . notes ,
service_name = service . name
)
db . add ( booking )
await db . commit ( )
2026-02-01 22:31:00 -06:00
# Send notifications to admin (email + WhatsApp)
try :
await notify_booking ( {
' name ' : booking_data . name ,
' email ' : booking_data . email ,
' phone ' : booking_data . phone ,
' preferred_date ' : booking_data . preferred_date ,
' notes ' : booking_data . notes
} , service . name )
except Exception as e :
logger . error ( f " Failed to send booking notifications: { e } " )
# Don't fail the booking if notifications fail
2026-01-27 18:07:00 -06:00
return { " message " : " Booking created successfully " , " booking_id " : booking . id }
# ================== CART ROUTES ==================
@api_router.get ( " /cart " )
async def get_cart ( user : User = Depends ( get_current_user ) , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute (
select ( CartItem )
. where ( CartItem . user_id == user . id )
. options (
selectinload ( CartItem . product ) . selectinload ( Product . images )
)
)
cart_items = result . scalars ( ) . all ( )
return [ {
" id " : item . id ,
" product_id " : item . product_id ,
" quantity " : item . quantity ,
" product " : product_to_dict ( item . product ) if item . product else None
} for item in cart_items ]
@api_router.post ( " /cart/add " )
async def add_to_cart ( item : CartItemCreate , user : User = Depends ( get_current_user ) , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute ( select ( Product ) . where ( Product . id == item . product_id ) )
product = result . scalar_one_or_none ( )
if not product :
raise HTTPException ( status_code = 404 , detail = " Product not found " )
result = await db . execute (
select ( CartItem ) . where (
and_ ( CartItem . user_id == user . id , CartItem . product_id == item . product_id )
)
)
existing = result . scalar_one_or_none ( )
if existing :
existing . quantity + = item . quantity
else :
cart_item = CartItem ( user_id = user . id , product_id = item . product_id , quantity = item . quantity )
db . add ( cart_item )
await db . commit ( )
return { " message " : " Item added to cart " }
@api_router.put ( " /cart/ {item_id} " )
async def update_cart_item ( item_id : str , quantity : int = Query ( . . . ) , user : User = Depends ( get_current_user ) , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute (
select ( CartItem ) . where ( and_ ( CartItem . id == item_id , CartItem . user_id == user . id ) )
)
item = result . scalar_one_or_none ( )
if not item :
raise HTTPException ( status_code = 404 , detail = " Cart item not found " )
if quantity < = 0 :
await db . delete ( item )
else :
item . quantity = quantity
await db . commit ( )
return { " message " : " Cart updated " }
@api_router.delete ( " /cart/ {item_id} " )
async def remove_from_cart ( item_id : str , user : User = Depends ( get_current_user ) , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute (
select ( CartItem ) . where ( and_ ( CartItem . id == item_id , CartItem . user_id == user . id ) )
)
item = result . scalar_one_or_none ( )
if item :
await db . delete ( item )
await db . commit ( )
return { " message " : " Item removed from cart " }
@api_router.delete ( " /cart " )
async def clear_cart ( user : User = Depends ( get_current_user ) , db : AsyncSession = Depends ( get_db ) ) :
await db . execute (
CartItem . __table__ . delete ( ) . where ( CartItem . user_id == user . id )
)
await db . commit ( )
return { " message " : " Cart cleared " }
# ================== ORDERS ROUTES ==================
@api_router.post ( " /orders " )
async def create_order ( order_data : OrderCreate , user : User = Depends ( get_current_user ) , db : AsyncSession = Depends ( get_db ) ) :
# Get cart items
result = await db . execute (
select ( CartItem )
. where ( CartItem . user_id == user . id )
. options ( selectinload ( CartItem . product ) )
)
cart_items = result . scalars ( ) . all ( )
if not cart_items :
raise HTTPException ( status_code = 400 , detail = " Cart is empty " )
# Calculate totals
subtotal = sum ( item . product . price * item . quantity for item in cart_items )
tax = subtotal * 0.08
shipping = 0 if subtotal > 100 else 9.99
total = subtotal + tax + shipping
# Create order
order = Order (
user_id = user . id ,
status = OrderStatus . PENDING ,
subtotal = subtotal ,
tax = tax ,
shipping = shipping ,
total = total ,
shipping_address = order_data . shipping_address ,
notes = order_data . notes
)
db . add ( order )
await db . flush ( )
# Create order items and update inventory
for cart_item in cart_items :
product = cart_item . product
order_item = OrderItem (
order_id = order . id ,
product_id = product . id ,
quantity = cart_item . quantity ,
price = product . price ,
product_name = product . name ,
product_image = product . image_url
)
db . add ( order_item )
# Update stock
previous_stock = product . stock
product . stock = max ( 0 , product . stock - cart_item . quantity )
# Log inventory change
inv_log = InventoryLog (
product_id = product . id ,
action = " sale " ,
quantity_change = - cart_item . quantity ,
previous_stock = previous_stock ,
new_stock = product . stock ,
notes = f " Order { order . id } " ,
created_by = user . id
)
db . add ( inv_log )
# Add status history
status_history = OrderStatusHistory (
order_id = order . id ,
status = OrderStatus . PENDING ,
notes = " Order placed " ,
created_by = user . id
)
db . add ( status_history )
# Clear cart
await db . execute ( CartItem . __table__ . delete ( ) . where ( CartItem . user_id == user . id ) )
await db . commit ( )
await db . refresh ( order )
return { " message " : " Order created successfully " , " order_id " : order . id }
@api_router.get ( " /orders " )
async def get_orders ( user : User = Depends ( get_current_user ) , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute (
select ( Order )
. where ( Order . user_id == user . id )
. options ( selectinload ( Order . items ) , selectinload ( Order . status_history ) )
. order_by ( desc ( Order . created_at ) )
)
orders = result . scalars ( ) . all ( )
return [ order_to_dict ( o ) for o in orders ]
@api_router.get ( " /orders/ {order_id} " )
async def get_order ( order_id : str , user : User = Depends ( get_current_user ) , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute (
select ( Order )
. where ( and_ ( Order . id == order_id , Order . user_id == user . id ) )
. options ( selectinload ( Order . items ) , selectinload ( Order . status_history ) )
)
order = result . scalar_one_or_none ( )
if not order :
raise HTTPException ( status_code = 404 , detail = " Order not found " )
return order_to_dict ( order )
# ================== REVIEWS ROUTES ==================
@api_router.post ( " /reviews " )
async def create_review ( review_data : ReviewCreate , user : User = Depends ( get_current_user ) , db : AsyncSession = Depends ( get_db ) ) :
if not review_data . product_id and not review_data . service_id :
raise HTTPException ( status_code = 400 , detail = " Product or service ID required " )
if review_data . rating < 1 or review_data . rating > 5 :
raise HTTPException ( status_code = 400 , detail = " Rating must be between 1 and 5 " )
# Check for verified purchase
is_verified = False
if review_data . product_id :
result = await db . execute (
select ( OrderItem )
. join ( Order )
. where (
and_ (
Order . user_id == user . id ,
OrderItem . product_id == review_data . product_id ,
Order . status . in_ ( [ OrderStatus . DELIVERED , OrderStatus . SHIPPED ] )
)
)
)
if result . scalar_one_or_none ( ) :
is_verified = True
review = Review (
user_id = user . id ,
product_id = review_data . product_id ,
service_id = review_data . service_id ,
rating = review_data . rating ,
title = review_data . title ,
comment = review_data . comment ,
is_verified_purchase = is_verified
)
db . add ( review )
await db . commit ( )
await db . refresh ( review )
return { " message " : " Review submitted successfully " , " review_id " : review . id }
@api_router.get ( " /reviews/product/ {product_id} " )
async def get_product_reviews ( product_id : str , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute (
select ( Review )
. where ( and_ ( Review . product_id == product_id , Review . is_approved == True ) )
. options ( selectinload ( Review . user ) )
. order_by ( desc ( Review . created_at ) )
)
reviews = result . scalars ( ) . all ( )
return [ review_to_dict ( r ) for r in reviews ]
@api_router.get ( " /reviews/service/ {service_id} " )
async def get_service_reviews ( service_id : str , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute (
select ( Review )
. where ( and_ ( Review . service_id == service_id , Review . is_approved == True ) )
. options ( selectinload ( Review . user ) )
. order_by ( desc ( Review . created_at ) )
)
reviews = result . scalars ( ) . all ( )
return [ review_to_dict ( r ) for r in reviews ]
# ================== CONTACT ROUTES ==================
@api_router.post ( " /contact " )
async def submit_contact ( contact_data : ContactCreate , db : AsyncSession = Depends ( get_db ) ) :
contact = Contact (
name = contact_data . name ,
email = contact_data . email ,
subject = contact_data . subject ,
message = contact_data . message
)
db . add ( contact )
await db . commit ( )
return { " message " : " Message sent successfully " , " id " : contact . id }
# ================== ADMIN ROUTES ==================
# Admin - Dashboard Stats
@api_router.get ( " /admin/dashboard " )
async def get_admin_dashboard ( user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
""" Admin dashboard with comprehensive stats and error handling """
async def safe_scalar ( query , default = 0 , error_msg = " " ) :
""" Execute query and return scalar with error handling """
try :
result = await db . execute ( query )
value = result . scalar ( )
return float ( value ) if value is not None and isinstance ( value , ( int , float ) ) else default
except Exception as e :
if error_msg :
logger . error ( f " { error_msg } : { e } " )
return default
try :
today = datetime . now ( timezone . utc ) . date ( )
month_ago = today - timedelta ( days = 30 )
month_start = datetime . combine ( month_ago , datetime . min . time ( ) )
# Batch count queries for better performance
counts_queries = {
" products " : select ( func . count ( Product . id ) ) ,
" services " : select ( func . count ( Service . id ) ) ,
" users " : select ( func . count ( User . id ) ) ,
" orders " : select ( func . count ( Order . id ) )
}
counts = { }
for key , query in counts_queries . items ( ) :
counts [ key ] = await safe_scalar ( query , 0 , f " Error fetching { key } count " )
# Revenue queries
total_revenue = await safe_scalar (
select ( func . sum ( Order . total ) ) ,
0.0 , " Error fetching total revenue "
)
monthly_revenue = await safe_scalar (
select ( func . sum ( Order . total ) ) . where ( Order . created_at > = month_start ) ,
0.0 , " Error fetching monthly revenue "
)
# Today's stats - batch queries
today_orders = await safe_scalar (
select ( func . count ( Order . id ) ) . where ( func . date ( Order . created_at ) == today ) ,
0 , " Error fetching today ' s orders "
)
today_revenue = await safe_scalar (
select ( func . sum ( Order . total ) ) . where ( func . date ( Order . created_at ) == today ) ,
0.0 , " Error fetching today ' s revenue "
)
pending_bookings = await safe_scalar (
select ( func . count ( Booking . id ) ) . where ( Booking . status == " pending " ) ,
0 , " Error fetching pending bookings "
)
# Low stock products
low_stock_products = [ ]
try :
low_stock_result = await db . execute (
select ( Product )
. where ( Product . stock < = Product . low_stock_threshold , Product . is_active == True )
)
low_stock_products = [ {
" id " : p . id , " name " : p . name , " stock " : p . stock ,
" low_stock_threshold " : p . low_stock_threshold , " category " : p . category
} for p in low_stock_result . scalars ( ) . all ( ) ]
except Exception as e :
logger . error ( f " Error fetching low stock products: { e } " )
# Recent orders
recent_orders_data = [ ]
try :
recent_orders_result = await db . execute (
select ( Order )
. options ( selectinload ( Order . items ) )
. order_by ( desc ( Order . created_at ) )
. limit ( 10 )
)
for order in recent_orders_result . scalars ( ) . all ( ) :
try :
recent_orders_data . append ( {
" id " : order . id ,
" status " : _safe_enum_value ( order . status ) ,
" total " : float ( order . total ) if order . total is not None else 0.0 ,
" created_at " : _safe_isoformat ( order . created_at ) ,
" items " : [ { " id " : i . id , " product_name " : i . product_name , " quantity " : i . quantity } for i in order . items ] if order . items else [ ]
} )
except Exception as e :
logger . warning ( f " Error processing order { order . id } : { e } " )
except Exception as e :
logger . error ( f " Error fetching recent orders: { e } " )
# Build response
response = {
" stats " : {
" total_products " : int ( counts [ " products " ] ) ,
" total_services " : int ( counts [ " services " ] ) ,
" total_users " : int ( counts [ " users " ] ) ,
" total_orders " : int ( counts [ " orders " ] ) ,
" total_revenue " : total_revenue ,
" monthly_revenue " : monthly_revenue ,
" today_orders " : int ( today_orders ) ,
" today_revenue " : today_revenue ,
" pending_bookings " : int ( pending_bookings )
} ,
" low_stock_products " : low_stock_products ,
" recent_orders " : recent_orders_data
}
logger . info ( f " Dashboard data fetched successfully for user { user . id } " )
return response
except Exception as e :
logger . error ( f " Critical error in get_admin_dashboard: { e } " , exc_info = True )
# Return safe defaults instead of crashing
return {
" stats " : {
" total_products " : 0 ,
" total_services " : 0 ,
" total_users " : 0 ,
" total_orders " : 0 ,
" total_revenue " : 0.0 ,
" monthly_revenue " : 0.0 ,
" today_orders " : 0 ,
" today_revenue " : 0.0 ,
" pending_bookings " : 0
} ,
" low_stock_products " : [ ] ,
" recent_orders " : [ ] ,
" error " : " Failed to load some dashboard data "
}
# ================== IMAGE UPLOAD ROUTES ==================
@api_router.post ( " /upload/image " )
async def upload_image (
file : UploadFile = File ( . . . ) ,
user : User = Depends ( get_admin_user )
) :
""" Upload a product image and return the URL """
try :
logger . info ( f " === Image Upload Request === " )
logger . info ( f " Filename: ' { file . filename } ' " )
logger . info ( f " Content-Type: { file . content_type } " )
logger . info ( f " File size: { file . size if hasattr ( file , ' size ' ) else ' unknown ' } " )
# Handle cases where filename might be None or empty
if not file . filename :
logger . error ( " No filename provided " )
raise HTTPException ( status_code = 400 , detail = " No filename provided " )
# Validate file extension (more reliable than content_type)
file_ext = Path ( file . filename ) . suffix . lower ( )
logger . info ( f " Extracted extension: ' { file_ext } ' " )
# Handle cases where file has no extension - try to infer from content_type
if not file_ext and file . content_type :
content_type_map = {
' image/jpeg ' : ' .jpg ' ,
' image/jpg ' : ' .jpg ' ,
' image/png ' : ' .png ' ,
' image/gif ' : ' .gif ' ,
' image/webp ' : ' .webp ' ,
' image/bmp ' : ' .bmp ' ,
' image/svg+xml ' : ' .svg ' ,
' image/heic ' : ' .heic ' ,
' image/heif ' : ' .heif '
}
file_ext = content_type_map . get ( file . content_type , ' ' )
logger . info ( f " Inferred extension from content-type: ' { file_ext } ' " )
# Support common image formats including iPhone formats
allowed_extensions = { ' .jpg ' , ' .jpeg ' , ' .png ' , ' .gif ' , ' .webp ' , ' .bmp ' , ' .svg ' , ' .heic ' , ' .heif ' }
# Special handling for HEIC/HEIF - convert to JPG for better compatibility
convert_to_jpg = file_ext in { ' .heic ' , ' .heif ' }
if convert_to_jpg :
logger . info ( f " HEIC/HEIF file detected, will convert to JPG for compatibility " )
if not file_ext or file_ext not in allowed_extensions :
logger . warning ( f " Invalid or missing file extension: ' { file_ext } ' " )
logger . warning ( f " Full filename: ' { file . filename } ' " )
raise HTTPException (
status_code = 400 ,
detail = f " File must be an image with a valid extension. Allowed: { ' , ' . join ( sorted ( allowed_extensions ) ) } "
)
# Generate unique filename - convert HEIC to JPG extension
output_ext = ' .jpg ' if convert_to_jpg else file_ext
unique_filename = f " { uuid . uuid4 ( ) } { output_ext } "
file_path = UPLOAD_DIR / unique_filename
logger . info ( f " Saving file to: { file_path } " )
# Save file asynchronously
content = await file . read ( )
if len ( content ) == 0 :
raise HTTPException ( status_code = 400 , detail = " File is empty " )
with open ( file_path , " wb " ) as buffer :
buffer . write ( content )
# Return relative URL
# Convert HEIC/HEIF to JPG for better browser compatibility
if convert_to_jpg :
try :
from PIL import Image
from io import BytesIO
# Try to use pillow_heif if available
try :
import pillow_heif
pillow_heif . register_heif_opener ( )
except ImportError :
logger . warning ( " pillow_heif not installed, HEIC conversion may not work " )
# Open and convert image
img = Image . open ( BytesIO ( content ) )
img = img . convert ( ' RGB ' ) # Ensure RGB mode for JPEG
img . save ( file_path , ' JPEG ' , quality = 90 )
logger . info ( f " Converted HEIC/HEIF to JPG successfully " )
except Exception as e :
logger . error ( f " Failed to convert HEIC/HEIF: { e } " )
# Fallback: save as-is and let browser handle it
with open ( file_path , " wb " ) as buffer :
buffer . write ( content )
logger . info ( f " Saved HEIC file as-is without conversion " )
else :
with open ( file_path , " wb " ) as buffer :
buffer . write ( content )
# Return relative URL
image_url = f " /uploads/products/ { unique_filename } "
logger . info ( f " Image uploaded successfully: { image_url } , size: { len ( content ) } bytes " )
return { " url " : image_url , " filename " : unique_filename }
except HTTPException :
raise
except Exception as e :
logger . error ( f " Failed to upload image: { str ( e ) } " , exc_info = True )
raise HTTPException ( status_code = 500 , detail = f " Failed to upload image: { str ( e ) } " )
@api_router.post ( " /admin/products/ {product_id} /images " )
async def add_product_images (
product_id : str ,
image_urls : List [ str ] ,
user : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Add multiple images to a product """
product = await _get_or_404 ( db , Product , product_id , " Product not found " )
# Get current max display order
result = await db . execute (
select ( func . max ( ProductImage . display_order ) )
. where ( ProductImage . product_id == product_id )
)
max_order = result . scalar ( ) or - 1
# Add new images
for idx , url in enumerate ( image_urls ) :
product_image = ProductImage (
product_id = product_id ,
image_url = url ,
display_order = max_order + idx + 1 ,
is_primary = ( max_order == - 1 and idx == 0 ) # First image is primary if no images exist
)
db . add ( product_image )
await db . commit ( )
return { " message " : f " Added { len ( image_urls ) } images " , " product_id " : product_id }
@api_router.delete ( " /admin/products/ {product_id} /images/ {image_id} " )
async def delete_product_image (
product_id : str ,
image_id : str ,
user : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Delete a product image """
result = await db . execute (
select ( ProductImage )
. where ( and_ ( ProductImage . id == image_id , ProductImage . product_id == product_id ) )
)
image = result . scalar_one_or_none ( )
if not image :
raise HTTPException ( status_code = 404 , detail = " Image not found " )
# Delete file from filesystem
try :
file_path = UPLOAD_DIR / Path ( image . image_url ) . name
if file_path . exists ( ) :
file_path . unlink ( )
except Exception as e :
logger . warning ( f " Failed to delete image file: { e } " )
await db . delete ( image )
await db . commit ( )
return { " message " : " Image deleted " }
@api_router.put ( " /admin/products/ {product_id} /images/reorder " )
async def reorder_product_images (
product_id : str ,
image_orders : Dict [ str , int ] , # {image_id: display_order}
user : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Reorder product images """
for image_id , order in image_orders . items ( ) :
result = await db . execute (
select ( ProductImage )
. where ( and_ ( ProductImage . id == image_id , ProductImage . product_id == product_id ) )
)
image = result . scalar_one_or_none ( )
if image :
image . display_order = order
await db . commit ( )
return { " message " : " Images reordered " }
# Admin - Products CRUD
@api_router.get ( " /admin/products " )
async def admin_get_products (
include_inactive : bool = False ,
user : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
query = select ( Product ) . options ( selectinload ( Product . images ) )
if not include_inactive :
query = query . where ( Product . is_active == True )
query = query . order_by ( desc ( Product . created_at ) )
result = await db . execute ( query )
products = result . scalars ( ) . all ( )
return [ product_to_dict ( p ) for p in products ]
@api_router.post ( " /admin/products " )
async def admin_create_product ( product_data : ProductCreate , user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
data_dict = product_data . model_dump ( )
image_urls = data_dict . pop ( ' images ' , [ ] )
product = Product ( * * data_dict )
db . add ( product )
await db . commit ( )
await db . refresh ( product )
# Add images if provided
if image_urls :
for idx , url in enumerate ( image_urls ) :
product_image = ProductImage (
product_id = product . id ,
image_url = url ,
display_order = idx ,
is_primary = ( idx == 0 )
)
db . add ( product_image )
await db . commit ( )
# Log inventory
inv_log = InventoryLog (
product_id = product . id ,
action = " add " ,
quantity_change = product . stock ,
previous_stock = 0 ,
new_stock = product . stock ,
notes = " Initial stock " ,
created_by = user . id
)
db . add ( inv_log )
await db . commit ( )
# Reload product with images relationship
result = await db . execute (
select ( Product )
. where ( Product . id == product . id )
. options ( selectinload ( Product . images ) )
)
product = result . scalar_one ( )
return product_to_dict ( product )
@api_router.put ( " /admin/products/ {product_id} " )
async def admin_update_product ( product_id : str , product_data : ProductUpdate , user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
product = await _get_or_404 ( db , Product , product_id , " Product not found " )
update_data = product_data . model_dump ( exclude_unset = True )
image_urls = update_data . pop ( ' images ' , None )
# Track stock changes
if " stock " in update_data and update_data [ " stock " ] != product . stock :
inv_log = InventoryLog (
product_id = product . id ,
action = " adjust " ,
quantity_change = update_data [ " stock " ] - product . stock ,
previous_stock = product . stock ,
new_stock = update_data [ " stock " ] ,
notes = " Manual adjustment " ,
created_by = user . id
)
db . add ( inv_log )
for key , value in update_data . items ( ) :
setattr ( product , key , value )
# Update images if provided
if image_urls is not None :
# Delete existing images
await db . execute (
delete ( ProductImage ) . where ( ProductImage . product_id == product_id )
)
# Add new images
for idx , url in enumerate ( image_urls ) :
product_image = ProductImage (
product_id = product_id ,
image_url = url ,
display_order = idx ,
is_primary = ( idx == 0 )
)
db . add ( product_image )
await db . commit ( )
# Reload product with images relationship
result = await db . execute (
select ( Product )
. where ( Product . id == product_id )
. options ( selectinload ( Product . images ) )
)
product = result . scalar_one ( )
return product_to_dict ( product )
@api_router.delete ( " /admin/products/ {product_id} " )
async def admin_delete_product ( product_id : str , user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
product = await _get_or_404 ( db , Product , product_id , " Product not found " )
# Hard delete - remove from database
await db . delete ( product )
await db . commit ( )
return { " message " : " Product deleted " }
# Admin - Categories CRUD
@api_router.get ( " /admin/categories " )
async def admin_get_categories ( user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute ( select ( Category ) . order_by ( Category . name ) )
categories = result . scalars ( ) . all ( )
return [ {
" id " : str ( c . id ) ,
" name " : c . name ,
" description " : c . description ,
" created_at " : c . created_at . isoformat ( ) if c . created_at else None
} for c in categories ]
@api_router.post ( " /admin/categories " )
async def admin_create_category ( category_data : CategoryCreate , user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
data = category_data . model_dump ( )
# Auto-generate slug if not provided
if not data . get ( ' slug ' ) :
data [ ' slug ' ] = data [ ' name ' ] . lower ( ) . replace ( ' ' , ' - ' ) . replace ( ' & ' , ' and ' )
category = Category ( * * data )
db . add ( category )
await db . commit ( )
await db . refresh ( category )
return {
" id " : str ( category . id ) ,
" name " : category . name ,
" slug " : category . slug ,
" description " : category . description ,
" created_at " : category . created_at . isoformat ( ) if category . created_at else None
}
@api_router.put ( " /admin/categories/ {category_id} " )
async def admin_update_category ( category_id : str , category_data : CategoryUpdate , user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
category = await _get_or_404 ( db , Category , category_id , " Category not found " )
update_data = category_data . model_dump ( exclude_unset = True )
for key , value in update_data . items ( ) :
setattr ( category , key , value )
await db . commit ( )
await db . refresh ( category )
return {
" id " : str ( category . id ) ,
" name " : category . name ,
" description " : category . description ,
" created_at " : category . created_at . isoformat ( ) if category . created_at else None
}
@api_router.delete ( " /admin/categories/ {category_id} " )
async def admin_delete_category ( category_id : str , user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
category = await _get_or_404 ( db , Category , category_id , " Category not found " )
await db . delete ( category )
await db . commit ( )
return { " message " : " Category deleted successfully " }
@api_router.get ( " /categories " )
async def get_categories ( db : AsyncSession = Depends ( get_db ) ) :
""" Public endpoint to get all categories """
result = await db . execute ( select ( Category ) . order_by ( Category . name ) )
categories = result . scalars ( ) . all ( )
return [ {
" id " : str ( c . id ) ,
" name " : c . name ,
" description " : c . description
} for c in categories ]
# Admin - Services CRUD
@api_router.get ( " /admin/services " )
async def admin_get_services ( include_inactive : bool = False , user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
query = select ( Service ) . options ( selectinload ( Service . images ) )
if not include_inactive :
query = query . where ( Service . is_active == True )
query = query . order_by ( desc ( Service . created_at ) )
result = await db . execute ( query )
services = result . scalars ( ) . all ( )
return [ service_to_dict ( s ) for s in services ]
@api_router.post ( " /admin/services " )
async def admin_create_service ( service_data : ServiceCreate , user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
data_dict = service_data . model_dump ( )
image_urls = data_dict . pop ( ' images ' , [ ] )
service = Service ( * * data_dict )
db . add ( service )
await db . commit ( )
await db . refresh ( service )
# Add images if provided
if image_urls :
for idx , url in enumerate ( image_urls ) :
service_image = ServiceImage (
service_id = service . id ,
image_url = url ,
display_order = idx ,
is_primary = ( idx == 0 )
)
db . add ( service_image )
await db . commit ( )
# Reload service with images relationship
result = await db . execute (
select ( Service )
. where ( Service . id == service . id )
. options ( selectinload ( Service . images ) )
)
service = result . scalar_one ( )
return service_to_dict ( service )
@api_router.put ( " /admin/services/ {service_id} " )
async def admin_update_service ( service_id : str , service_data : ServiceUpdate , user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
service = await _get_or_404 ( db , Service , service_id , " Service not found " )
update_data = service_data . model_dump ( exclude_unset = True )
image_urls = update_data . pop ( ' images ' , None )
for key , value in update_data . items ( ) :
setattr ( service , key , value )
# Update images if provided
if image_urls is not None :
# Delete existing images
await db . execute (
delete ( ServiceImage ) . where ( ServiceImage . service_id == service_id )
)
# Add new images
for idx , url in enumerate ( image_urls ) :
service_image = ServiceImage (
service_id = service_id ,
image_url = url ,
display_order = idx ,
is_primary = ( idx == 0 )
)
db . add ( service_image )
await db . commit ( )
# Reload service with images relationship
result = await db . execute (
select ( Service )
. where ( Service . id == service_id )
. options ( selectinload ( Service . images ) )
)
service = result . scalar_one ( )
return service_to_dict ( service )
@api_router.delete ( " /admin/services/ {service_id} " )
async def admin_delete_service ( service_id : str , user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
service = await _get_or_404 ( db , Service , service_id , " Service not found " )
# Hard delete - remove from database
await db . delete ( service )
await db . commit ( )
return { " message " : " Service deleted " }
# Admin - Orders Management
@api_router.get ( " /admin/orders " )
async def admin_get_orders (
status : Optional [ str ] = None ,
limit : int = 50 ,
user : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
query = select ( Order ) . options ( selectinload ( Order . items ) , selectinload ( Order . status_history ) , selectinload ( Order . user ) )
if status :
query = query . where ( Order . status == OrderStatus ( status ) )
query = query . order_by ( desc ( Order . created_at ) ) . limit ( limit )
result = await db . execute ( query )
orders = result . scalars ( ) . all ( )
return [ {
* * order_to_dict ( o ) ,
" user_name " : o . user . name if o . user else " Unknown " ,
" user_email " : o . user . email if o . user else " Unknown "
} for o in orders ]
@api_router.put ( " /admin/orders/ {order_id} /status " )
async def admin_update_order_status ( order_id : str , status_data : OrderStatusUpdate , user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute (
select ( Order ) . where ( Order . id == order_id ) . options ( selectinload ( Order . items ) )
)
order = result . scalar_one_or_none ( )
if not order :
raise HTTPException ( status_code = 404 , detail = " Order not found " )
new_status = OrderStatus ( status_data . status )
order . status = new_status
if status_data . tracking_number :
order . tracking_number = status_data . tracking_number
# Handle refunds - restore stock
if new_status == OrderStatus . REFUNDED :
for item in order . items :
result = await db . execute ( select ( Product ) . where ( Product . id == item . product_id ) )
product = result . scalar_one_or_none ( )
if product :
previous_stock = product . stock
product . stock + = item . quantity
inv_log = InventoryLog (
product_id = product . id ,
action = " refund " ,
quantity_change = item . quantity ,
previous_stock = previous_stock ,
new_stock = product . stock ,
notes = f " Refund for order { order_id } " ,
created_by = user . id
)
db . add ( inv_log )
# Add status history
status_history = OrderStatusHistory (
order_id = order . id ,
status = new_status ,
notes = status_data . notes ,
created_by = user . id
)
db . add ( status_history )
await db . commit ( )
return { " message " : " Order status updated " }
# Admin - Inventory Management
@api_router.get ( " /admin/inventory " )
async def admin_get_inventory ( user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
try :
result = await db . execute (
select ( Product )
. options ( selectinload ( Product . images ) )
. where ( Product . is_active == True )
. order_by ( Product . stock )
)
products = result . scalars ( ) . all ( )
inventory_data = [ ]
for p in products :
product_dict = product_to_dict ( p )
product_dict [ " is_low_stock " ] = p . stock < = p . low_stock_threshold
inventory_data . append ( product_dict )
return inventory_data
except Exception as e :
logger . error ( f " Error fetching inventory: { str ( e ) } " )
raise HTTPException ( status_code = 500 , detail = f " Failed to fetch inventory: { str ( e ) } " )
@api_router.post ( " /admin/inventory/ {product_id} /adjust " )
async def admin_adjust_inventory ( product_id : str , adjustment : InventoryAdjust , user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
product = await _get_or_404 ( db , Product , product_id , " Product not found " )
previous_stock = product . stock
product . stock = max ( 0 , product . stock + adjustment . quantity_change )
inv_log = InventoryLog (
product_id = product . id ,
action = " adjust " if adjustment . quantity_change > = 0 else " remove " ,
quantity_change = adjustment . quantity_change ,
previous_stock = previous_stock ,
new_stock = product . stock ,
notes = adjustment . notes ,
created_by = user . id
)
db . add ( inv_log )
await db . commit ( )
return _build_response ( " Inventory adjusted " , new_stock = product . stock )
@api_router.get ( " /admin/inventory/ {product_id} /logs " )
async def admin_get_inventory_logs ( product_id : str , user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute (
select ( InventoryLog )
. where ( InventoryLog . product_id == product_id )
. order_by ( desc ( InventoryLog . created_at ) )
. limit ( 50 )
)
logs = result . scalars ( ) . all ( )
return [ {
" id " : log . id ,
" action " : log . action ,
" quantity_change " : log . quantity_change ,
" previous_stock " : log . previous_stock ,
" new_stock " : log . new_stock ,
" notes " : log . notes ,
" created_at " : log . created_at . isoformat ( ) if log . created_at else None
} for log in logs ]
# Admin - Bookings Management
@api_router.get ( " /admin/bookings " )
async def admin_get_bookings ( status : Optional [ str ] = None , user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
query = select ( Booking ) . options ( selectinload ( Booking . service ) )
if status :
query = query . where ( Booking . status == status )
query = query . order_by ( desc ( Booking . created_at ) )
result = await db . execute ( query )
bookings = result . scalars ( ) . all ( )
return [ booking_to_dict ( b ) for b in bookings ]
@api_router.put ( " /admin/bookings/ {booking_id} /status " )
async def admin_update_booking_status ( booking_id : str , status : str , user : User = Depends ( get_admin_user ) , db : AsyncSession = Depends ( get_db ) ) :
result = await db . execute ( select ( Booking ) . where ( Booking . id == booking_id ) )
booking = result . scalar_one_or_none ( )
if not booking :
raise HTTPException ( status_code = 404 , detail = " Booking not found " )
booking . status = status
await db . commit ( )
return { " message " : " Booking status updated " }
2026-02-01 22:31:00 -06:00
@api_router.put ( " /admin/bookings/ {booking_id} /complete " )
async def admin_complete_booking (
booking_id : str ,
completion_data : BookingComplete ,
user : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Mark a booking as completed with diagnosis and work performed details """
result = await db . execute ( select ( Booking ) . where ( Booking . id == booking_id ) )
booking = result . scalar_one_or_none ( )
if not booking :
raise HTTPException ( status_code = 404 , detail = " Booking not found " )
booking . status = " completed "
booking . completed_at = datetime . now ( timezone . utc )
if completion_data . diagnosis :
booking . diagnosis = completion_data . diagnosis
if completion_data . work_performed :
booking . work_performed = completion_data . work_performed
if completion_data . technician_notes :
booking . technician_notes = completion_data . technician_notes
if completion_data . service_cost is not None :
booking . service_cost = completion_data . service_cost
if completion_data . paid is not None :
booking . paid = completion_data . paid
if completion_data . paid :
booking . paid_at = datetime . now ( timezone . utc )
if completion_data . device_model :
booking . device_model = completion_data . device_model
if completion_data . serial_number :
booking . serial_number = completion_data . serial_number
if completion_data . product_number :
booking . product_number = completion_data . product_number
if completion_data . screen_size :
booking . screen_size = completion_data . screen_size
await db . commit ( )
await db . refresh ( booking )
logger . info ( f " Booking { booking_id } marked as completed by { user . email } " )
return booking_to_dict ( booking )
@api_router.get ( " /admin/bookings/ {booking_id} /receipt " )
async def admin_get_booking_receipt (
booking_id : str ,
user : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Get booking receipt data for printing """
result = await db . execute (
select ( Booking )
. where ( Booking . id == booking_id )
. options ( selectinload ( Booking . service ) )
)
booking = result . scalar_one_or_none ( )
if not booking :
raise HTTPException ( status_code = 404 , detail = " Booking not found " )
# Get service details
service_price = booking . service . price if booking . service else 0
return {
* * booking_to_dict ( booking ) ,
" service_base_price " : service_price ,
" final_cost " : booking . service_cost if booking . service_cost else service_price ,
" company " : {
" name " : " PromptTech Solutions " ,
" address " : " Belmopan City, Cayo District, Belize " ,
" phone " : " +501 638-6318 " ,
" email " : " prompttechbz@gmail.com "
}
}
@api_router.put ( " /admin/bookings/ {booking_id} /update " )
async def admin_update_booking (
booking_id : str ,
update_data : BookingComplete ,
user : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Update booking details without marking as complete """
result = await db . execute ( select ( Booking ) . where ( Booking . id == booking_id ) )
booking = result . scalar_one_or_none ( )
if not booking :
raise HTTPException ( status_code = 404 , detail = " Booking not found " )
if update_data . diagnosis is not None :
booking . diagnosis = update_data . diagnosis
if update_data . work_performed is not None :
booking . work_performed = update_data . work_performed
if update_data . technician_notes is not None :
booking . technician_notes = update_data . technician_notes
if update_data . service_cost is not None :
booking . service_cost = update_data . service_cost
await db . commit ( )
await db . refresh ( booking )
logger . info ( f " Booking { booking_id } updated by { user . email } " )
return booking_to_dict ( booking )
@api_router.delete ( " /admin/bookings/ {booking_id} " )
async def admin_delete_booking (
booking_id : str ,
user : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Delete a booking """
result = await db . execute ( select ( Booking ) . where ( Booking . id == booking_id ) )
booking = result . scalar_one_or_none ( )
if not booking :
raise HTTPException ( status_code = 404 , detail = " Booking not found " )
await db . delete ( booking )
await db . commit ( )
logger . info ( f " Booking { booking_id } deleted by { user . email } " )
return { " message " : " Booking deleted successfully " }
2026-01-27 18:07:00 -06:00
# Admin - Users Management
# Admin - Reports
@api_router.get ( " /admin/reports/sales " )
async def admin_get_sales_report (
period : str = " daily " , # daily, weekly, monthly
start_date : Optional [ str ] = None ,
end_date : Optional [ str ] = None ,
user : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
now = datetime . now ( timezone . utc )
if start_date :
start = datetime . fromisoformat ( start_date . replace ( ' Z ' , ' +00:00 ' ) )
else :
if period == " daily " :
start = now - timedelta ( days = 30 )
elif period == " weekly " :
start = now - timedelta ( weeks = 12 )
else :
start = now - timedelta ( days = 365 )
if end_date :
end = datetime . fromisoformat ( end_date . replace ( ' Z ' , ' +00:00 ' ) )
else :
end = now
# Get orders in date range
result = await db . execute (
select ( Order )
. where ( and_ ( Order . created_at > = start , Order . created_at < = end ) )
. options ( selectinload ( Order . items ) )
. order_by ( Order . created_at )
)
orders = result . scalars ( ) . all ( )
# Get bookings in date range
bookings_result = await db . execute (
select ( Booking )
. where ( and_ ( Booking . created_at > = start , Booking . created_at < = end ) )
2026-02-01 22:31:00 -06:00
. options ( selectinload ( Booking . service ) )
2026-01-27 18:07:00 -06:00
)
bookings = bookings_result . scalars ( ) . all ( )
# Aggregate by period
report_data = { }
for order in orders :
if period == " daily " :
key = order . created_at . strftime ( " % Y- % m- %d " )
elif period == " weekly " :
key = order . created_at . strftime ( " % Y-W % W " )
else :
key = order . created_at . strftime ( " % Y- % m " )
if key not in report_data :
report_data [ key ] = {
" period " : key ,
" orders " : 0 ,
" revenue " : 0 ,
" products_sold " : 0 ,
2026-02-01 22:31:00 -06:00
" order_statuses " : { } ,
" services_booked " : 0 ,
" services_completed " : 0 ,
" services_paid " : 0 ,
" service_revenue " : 0
2026-01-27 18:07:00 -06:00
}
report_data [ key ] [ " orders " ] + = 1
report_data [ key ] [ " revenue " ] + = order . total
report_data [ key ] [ " products_sold " ] + = sum ( item . quantity for item in order . items )
status = order . status . value if order . status else " unknown "
report_data [ key ] [ " order_statuses " ] [ status ] = report_data [ key ] [ " order_statuses " ] . get ( status , 0 ) + 1
2026-02-01 22:31:00 -06:00
# Add booking counts and service revenue
total_service_revenue = 0
total_services_completed = 0
total_services_paid = 0
2026-01-27 18:07:00 -06:00
for booking in bookings :
if period == " daily " :
key = booking . created_at . strftime ( " % Y- % m- %d " )
elif period == " weekly " :
key = booking . created_at . strftime ( " % Y-W % W " )
else :
key = booking . created_at . strftime ( " % Y- % m " )
if key not in report_data :
report_data [ key ] = {
" period " : key ,
" orders " : 0 ,
" revenue " : 0 ,
" products_sold " : 0 ,
2026-02-01 22:31:00 -06:00
" order_statuses " : { } ,
" services_booked " : 0 ,
" services_completed " : 0 ,
" services_paid " : 0 ,
" service_revenue " : 0
2026-01-27 18:07:00 -06:00
}
report_data [ key ] [ " services_booked " ] = report_data [ key ] . get ( " services_booked " , 0 ) + 1
2026-02-01 22:31:00 -06:00
if booking . status == " completed " :
report_data [ key ] [ " services_completed " ] = report_data [ key ] . get ( " services_completed " , 0 ) + 1
total_services_completed + = 1
if booking . paid :
report_data [ key ] [ " services_paid " ] = report_data [ key ] . get ( " services_paid " , 0 ) + 1
total_services_paid + = 1
# Use service_cost if set, otherwise use service base price
cost = booking . service_cost if booking . service_cost else ( booking . service . price if booking . service else 0 )
report_data [ key ] [ " service_revenue " ] = report_data [ key ] . get ( " service_revenue " , 0 ) + cost
total_service_revenue + = cost
2026-01-27 18:07:00 -06:00
# Calculate totals
total_orders = len ( orders )
total_revenue = sum ( o . total for o in orders )
total_products = sum ( sum ( item . quantity for item in o . items ) for o in orders )
total_bookings = len ( bookings )
return {
" period " : period ,
" start_date " : start . isoformat ( ) ,
" end_date " : end . isoformat ( ) ,
" summary " : {
" total_orders " : total_orders ,
" total_revenue " : total_revenue ,
" total_products_sold " : total_products ,
" total_services_booked " : total_bookings ,
2026-02-01 22:31:00 -06:00
" total_services_completed " : total_services_completed ,
" total_services_paid " : total_services_paid ,
" total_service_revenue " : total_service_revenue ,
" combined_revenue " : total_revenue + total_service_revenue ,
2026-01-27 18:07:00 -06:00
" average_order_value " : total_revenue / total_orders if total_orders > 0 else 0
} ,
" data " : list ( report_data . values ( ) )
}
# Admin - Export Reports
@api_router.get ( " /admin/reports/export/csv " )
async def admin_export_csv (
report_type : str = " sales " , # sales, inventory, orders
period : str = " monthly " ,
user : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
output = io . StringIO ( )
writer = csv . writer ( output )
now = datetime . now ( timezone . utc )
if report_type == " sales " :
if period == " daily " :
start = now - timedelta ( days = 30 )
elif period == " weekly " :
start = now - timedelta ( weeks = 12 )
else :
start = now - timedelta ( days = 365 )
result = await db . execute (
select ( Order )
. where ( Order . created_at > = start )
. options ( selectinload ( Order . items ) , selectinload ( Order . user ) )
. order_by ( Order . created_at )
)
orders = result . scalars ( ) . all ( )
writer . writerow ( [ " Date " , " Order ID " , " Customer " , " Items " , " Subtotal " , " Tax " , " Shipping " , " Total " , " Status " ] )
for order in orders :
writer . writerow ( [
order . created_at . strftime ( " % Y- % m- %d % H: % M " ) ,
order . id ,
order . user . name if order . user else " Guest " ,
sum ( item . quantity for item in order . items ) ,
f " $ { order . subtotal : .2f } " ,
f " $ { order . tax : .2f } " ,
f " $ { order . shipping : .2f } " ,
f " $ { order . total : .2f } " ,
order . status . value if order . status else " unknown "
] )
elif report_type == " inventory " :
result = await db . execute ( select ( Product ) . where ( Product . is_active == True ) )
products = result . scalars ( ) . all ( )
writer . writerow ( [ " Product ID " , " Name " , " Category " , " Brand " , " Price " , " Stock " , " Low Stock Threshold " , " Status " ] )
for product in products :
writer . writerow ( [
product . id ,
product . name ,
product . category ,
product . brand ,
f " $ { product . price : .2f } " ,
product . stock ,
product . low_stock_threshold ,
" Low Stock " if product . stock < = product . low_stock_threshold else " In Stock "
] )
elif report_type == " orders " :
result = await db . execute (
select ( Order )
. options ( selectinload ( Order . items ) , selectinload ( Order . user ) )
. order_by ( desc ( Order . created_at ) )
. limit ( 500 )
)
orders = result . scalars ( ) . all ( )
writer . writerow ( [ " Order ID " , " Date " , " Customer " , " Email " , " Items " , " Total " , " Status " , " Tracking " ] )
for order in orders :
writer . writerow ( [
order . id ,
order . created_at . strftime ( " % Y- % m- %d % H: % M " ) ,
order . user . name if order . user else " Guest " ,
order . user . email if order . user else " " ,
sum ( item . quantity for item in order . items ) ,
f " $ { order . total : .2f } " ,
order . status . value if order . status else " unknown " ,
order . tracking_number or " "
] )
output . seek ( 0 )
return StreamingResponse (
iter ( [ output . getvalue ( ) ] ) ,
media_type = " text/csv " ,
headers = { " Content-Disposition " : f " attachment; filename= { report_type } _report_ { now . strftime ( ' % Y % m %d ' ) } .csv " }
)
@api_router.get ( " /admin/reports/export/pdf " )
async def admin_export_pdf (
report_type : str = " sales " ,
period : str = " monthly " ,
user : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
buffer = io . BytesIO ( )
doc = SimpleDocTemplate ( buffer , pagesize = A4 )
styles = getSampleStyleSheet ( )
elements = [ ]
now = datetime . now ( timezone . utc )
# Title
title_style = ParagraphStyle (
' CustomTitle ' ,
parent = styles [ ' Heading1 ' ] ,
fontSize = 24 ,
spaceAfter = 30
)
elements . append ( Paragraph ( f " TechZone { report_type . title ( ) } Report " , title_style ) )
elements . append ( Paragraph ( f " Generated: { now . strftime ( ' % Y- % m- %d % H: % M ' ) } " , styles [ ' Normal ' ] ) )
elements . append ( Spacer ( 1 , 20 ) )
if report_type == " sales " :
if period == " daily " :
start = now - timedelta ( days = 30 )
elif period == " weekly " :
start = now - timedelta ( weeks = 12 )
else :
start = now - timedelta ( days = 365 )
result = await db . execute (
select ( Order )
. where ( Order . created_at > = start )
. options ( selectinload ( Order . items ) )
)
orders = result . scalars ( ) . all ( )
# Summary
total_orders = len ( orders )
total_revenue = sum ( o . total for o in orders )
total_products = sum ( sum ( item . quantity for item in o . items ) for o in orders )
elements . append ( Paragraph ( " Summary " , styles [ ' Heading2 ' ] ) )
summary_data = [
[ " Metric " , " Value " ] ,
[ " Total Orders " , str ( total_orders ) ] ,
[ " Total Revenue " , f " $ { total_revenue : .2f } " ] ,
[ " Products Sold " , str ( total_products ) ] ,
[ " Average Order Value " , f " $ { total_revenue / total_orders : .2f } " if total_orders > 0 else " $0.00 " ]
]
summary_table = Table ( summary_data , colWidths = [ 3 * inch , 2 * inch ] )
summary_table . setStyle ( TableStyle ( [
( ' BACKGROUND ' , ( 0 , 0 ) , ( - 1 , 0 ) , colors . grey ) ,
( ' TEXTCOLOR ' , ( 0 , 0 ) , ( - 1 , 0 ) , colors . whitesmoke ) ,
( ' ALIGN ' , ( 0 , 0 ) , ( - 1 , - 1 ) , ' CENTER ' ) ,
( ' FONTNAME ' , ( 0 , 0 ) , ( - 1 , 0 ) , ' Helvetica-Bold ' ) ,
( ' FONTSIZE ' , ( 0 , 0 ) , ( - 1 , 0 ) , 12 ) ,
( ' BOTTOMPADDING ' , ( 0 , 0 ) , ( - 1 , 0 ) , 12 ) ,
( ' BACKGROUND ' , ( 0 , 1 ) , ( - 1 , - 1 ) , colors . beige ) ,
( ' GRID ' , ( 0 , 0 ) , ( - 1 , - 1 ) , 1 , colors . black )
] ) )
elements . append ( summary_table )
elements . append ( Spacer ( 1 , 20 ) )
# Orders table
elements . append ( Paragraph ( " Recent Orders " , styles [ ' Heading2 ' ] ) )
orders_data = [ [ " Date " , " Order ID " , " Items " , " Total " , " Status " ] ]
for order in orders [ : 50 ] :
orders_data . append ( [
order . created_at . strftime ( " % Y- % m- %d " ) ,
order . id [ : 8 ] + " ... " ,
str ( sum ( item . quantity for item in order . items ) ) ,
f " $ { order . total : .2f } " ,
order . status . value if order . status else " unknown "
] )
orders_table = Table ( orders_data , colWidths = [ 1.2 * inch , 1.2 * inch , 0.8 * inch , 1 * inch , 1 * inch ] )
orders_table . setStyle ( TableStyle ( [
( ' BACKGROUND ' , ( 0 , 0 ) , ( - 1 , 0 ) , colors . grey ) ,
( ' TEXTCOLOR ' , ( 0 , 0 ) , ( - 1 , 0 ) , colors . whitesmoke ) ,
( ' ALIGN ' , ( 0 , 0 ) , ( - 1 , - 1 ) , ' CENTER ' ) ,
( ' FONTNAME ' , ( 0 , 0 ) , ( - 1 , 0 ) , ' Helvetica-Bold ' ) ,
( ' FONTSIZE ' , ( 0 , 0 ) , ( - 1 , - 1 ) , 9 ) ,
( ' BOTTOMPADDING ' , ( 0 , 0 ) , ( - 1 , 0 ) , 12 ) ,
( ' GRID ' , ( 0 , 0 ) , ( - 1 , - 1 ) , 0.5 , colors . black )
] ) )
elements . append ( orders_table )
elif report_type == " inventory " :
result = await db . execute ( select ( Product ) . where ( Product . is_active == True ) . order_by ( Product . stock ) )
products = result . scalars ( ) . all ( )
elements . append ( Paragraph ( " Inventory Status " , styles [ ' Heading2 ' ] ) )
inv_data = [ [ " Product " , " Category " , " Price " , " Stock " , " Status " ] ]
for product in products :
status = " LOW STOCK " if product . stock < = product . low_stock_threshold else " In Stock "
inv_data . append ( [
product . name [ : 30 ] + " ... " if len ( product . name ) > 30 else product . name ,
product . category ,
f " $ { product . price : .2f } " ,
str ( product . stock ) ,
status
] )
inv_table = Table ( inv_data , colWidths = [ 2 * inch , 1 * inch , 0.8 * inch , 0.6 * inch , 0.8 * inch ] )
inv_table . setStyle ( TableStyle ( [
( ' BACKGROUND ' , ( 0 , 0 ) , ( - 1 , 0 ) , colors . grey ) ,
( ' TEXTCOLOR ' , ( 0 , 0 ) , ( - 1 , 0 ) , colors . whitesmoke ) ,
( ' ALIGN ' , ( 0 , 0 ) , ( - 1 , - 1 ) , ' CENTER ' ) ,
( ' FONTNAME ' , ( 0 , 0 ) , ( - 1 , 0 ) , ' Helvetica-Bold ' ) ,
( ' FONTSIZE ' , ( 0 , 0 ) , ( - 1 , - 1 ) , 8 ) ,
( ' BOTTOMPADDING ' , ( 0 , 0 ) , ( - 1 , 0 ) , 12 ) ,
( ' GRID ' , ( 0 , 0 ) , ( - 1 , - 1 ) , 0.5 , colors . black )
] ) )
elements . append ( inv_table )
doc . build ( elements )
buffer . seek ( 0 )
return StreamingResponse (
buffer ,
media_type = " application/pdf " ,
headers = { " Content-Disposition " : f " attachment; filename= { report_type } _report_ { now . strftime ( ' % Y % m %d ' ) } .pdf " }
)
# ================== USER MANAGEMENT ==================
class UserCreateAdmin ( BaseModel ) :
email : EmailStr
name : str
password : str
role : str
is_active : bool = True
password_never_change : bool = False
class UserUpdateAdmin ( BaseModel ) :
email : Optional [ EmailStr ] = None
name : Optional [ str ] = None
password : Optional [ str ] = None # Allow password updates
role : Optional [ str ] = None
is_active : Optional [ bool ] = None
password_never_change : Optional [ bool ] = None
@api_router.get ( " /admin/users " )
async def get_all_users (
skip : int = 0 ,
limit : int = 20 ,
search : str = " " ,
role : str = " " ,
status : str = " " ,
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Get all users with filters """
query = select ( User )
# Apply search filter
if search :
query = query . where (
or_ (
User . name . ilike ( f " % { search } % " ) ,
User . email . ilike ( f " % { search } % " )
)
)
# Apply role filter
if role :
try :
role_enum = UserRole [ role . upper ( ) ]
query = query . where ( User . role == role_enum )
except KeyError :
pass
# Apply status filter
if status == " active " :
query = query . where ( User . is_active == True )
elif status == " inactive " :
query = query . where ( User . is_active == False )
# Get total count
count_result = await db . execute ( select ( func . count ( ) ) . select_from ( query . subquery ( ) ) )
total = count_result . scalar ( )
# Apply pagination
query = query . offset ( skip ) . limit ( limit ) . order_by ( User . created_at . desc ( ) )
result = await db . execute ( query )
users = result . scalars ( ) . all ( )
return {
" users " : [ user_to_dict ( user ) for user in users ] ,
" total " : total ,
" skip " : skip ,
" limit " : limit
}
@api_router.post ( " /admin/users " )
async def create_user (
user_data : UserCreateAdmin ,
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Create a new user (admin only) """
# Check if email already exists
result = await db . execute ( select ( User ) . where ( User . email == user_data . email ) )
if result . scalar_one_or_none ( ) :
raise HTTPException ( status_code = 400 , detail = " Email already registered " )
# Validate role
try :
role_enum = UserRole [ user_data . role . upper ( ) ]
except KeyError :
raise HTTPException ( status_code = 400 , detail = f " Invalid role: { user_data . role } " )
# Create new user
new_user = User (
email = user_data . email ,
name = user_data . name ,
password = hash_password ( user_data . password ) ,
role = role_enum ,
is_active = user_data . is_active
)
db . add ( new_user )
await db . commit ( )
await db . refresh ( new_user )
logger . info ( f " Admin { admin . email } created user { new_user . email } with role { role_enum . value } " )
return {
" message " : " User created successfully " ,
" user " : user_to_dict ( new_user )
}
@api_router.put ( " /admin/users/ {user_id} " )
async def update_user (
user_id : str ,
user_data : UserUpdateAdmin ,
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Update user details (admin only) """
user = await _get_or_404 ( db , User , user_id , " User not found " )
# Update fields if provided
if user_data . email is not None :
# Check if new email already exists
result = await db . execute (
select ( User ) . where ( User . email == user_data . email , User . id != user_id )
)
if result . scalar_one_or_none ( ) :
raise HTTPException ( status_code = 400 , detail = " Email already in use " )
user . email = user_data . email
if user_data . name is not None :
user . name = user_data . name
if user_data . password is not None and user_data . password . strip ( ) :
# Only update password if provided and not empty
user . password = hash_password ( user_data . password )
if user_data . role is not None :
try :
role_enum = UserRole [ user_data . role . upper ( ) ]
user . role = role_enum
except KeyError :
raise HTTPException ( status_code = 400 , detail = f " Invalid role: { user_data . role } " )
if user_data . is_active is not None :
user . is_active = user_data . is_active
await db . commit ( )
await db . refresh ( user )
logger . info ( f " Admin { admin . email } updated user { user . email } " )
return {
" message " : " User updated successfully " ,
" user " : user_to_dict ( user )
}
@api_router.put ( " /admin/users/ {user_id} /toggle-active " )
async def toggle_user_active (
user_id : str ,
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Toggle user active status """
user = await _get_or_404 ( db , User , user_id , " User not found " )
# Prevent deactivating yourself
if user . id == admin . id :
raise HTTPException ( status_code = 400 , detail = " Cannot deactivate your own account " )
user . is_active = not user . is_active
await db . commit ( )
await db . refresh ( user )
status = " activated " if user . is_active else " deactivated "
logger . info ( f " Admin { admin . email } { status } user { user . email } " )
return {
" message " : f " User { status } successfully " ,
" user " : user_to_dict ( user )
}
@api_router.delete ( " /admin/users/ {user_id} " )
async def delete_user (
user_id : str ,
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Delete a user (admin only) """
user = await _get_or_404 ( db , User , user_id , " User not found " )
# Prevent deleting yourself
if user . id == admin . id :
raise HTTPException ( status_code = 400 , detail = " Cannot delete your own account " )
await db . delete ( user )
await db . commit ( )
logger . info ( f " Admin { admin . email } deleted user { user . email } " )
return { " message " : " User deleted successfully " }
# ================== ABOUT PAGE CMS ==================
# Pydantic schemas for About page
class AboutContentCreate ( BaseModel ) :
section : str
title : Optional [ str ] = None
subtitle : Optional [ str ] = None
content : Optional [ str ] = None
image_url : Optional [ str ] = None
data : Optional [ dict ] = None
display_order : int = 0
is_active : bool = True
class AboutContentUpdate ( BaseModel ) :
title : Optional [ str ] = None
subtitle : Optional [ str ] = None
content : Optional [ str ] = None
image_url : Optional [ str ] = None
data : Optional [ dict ] = None
display_order : Optional [ int ] = None
is_active : Optional [ bool ] = None
class TeamMemberCreate ( BaseModel ) :
name : str
role : str
bio : Optional [ str ] = None
image_url : Optional [ str ] = None
email : Optional [ str ] = None
linkedin : Optional [ str ] = None
display_order : int = 0
is_active : bool = True
class TeamMemberUpdate ( BaseModel ) :
name : Optional [ str ] = None
role : Optional [ str ] = None
bio : Optional [ str ] = None
image_url : Optional [ str ] = None
email : Optional [ str ] = None
linkedin : Optional [ str ] = None
display_order : Optional [ int ] = None
is_active : Optional [ bool ] = None
class CompanyValueCreate ( BaseModel ) :
title : str
description : str
icon : str
display_order : int = 0
is_active : bool = True
class CompanyValueUpdate ( BaseModel ) :
title : Optional [ str ] = None
description : Optional [ str ] = None
icon : Optional [ str ] = None
display_order : Optional [ int ] = None
is_active : Optional [ bool ] = None
# Public endpoints (no auth required)
@api_router.get ( " /about/content " )
async def get_about_content ( db : AsyncSession = Depends ( get_db ) ) :
""" Get all active about content sections """
result = await db . execute (
select ( AboutContent )
. where ( AboutContent . is_active == True )
. order_by ( AboutContent . section , AboutContent . display_order )
)
content = result . scalars ( ) . all ( )
return [ {
" id " : str ( item . id ) ,
" section " : item . section ,
" title " : item . title ,
" subtitle " : item . subtitle ,
" content " : item . content ,
" image_url " : item . image_url ,
" data " : item . data ,
" display_order " : item . display_order ,
" created_at " : item . created_at . isoformat ( ) if item . created_at else None ,
" updated_at " : item . updated_at . isoformat ( ) if item . updated_at else None
} for item in content ]
@api_router.get ( " /about/team " )
async def get_team_members ( db : AsyncSession = Depends ( get_db ) ) :
""" Get all active team members """
result = await db . execute (
select ( TeamMember )
. where ( TeamMember . is_active == True )
. order_by ( TeamMember . display_order )
)
members = result . scalars ( ) . all ( )
return [ {
" id " : str ( member . id ) ,
" name " : member . name ,
" role " : member . role ,
" bio " : member . bio ,
" image_url " : member . image_url ,
" email " : member . email ,
" linkedin " : member . linkedin ,
" display_order " : member . display_order
} for member in members ]
@api_router.get ( " /about/values " )
async def get_company_values ( db : AsyncSession = Depends ( get_db ) ) :
""" Get all active company values """
result = await db . execute (
select ( CompanyValue )
. where ( CompanyValue . is_active == True )
. order_by ( CompanyValue . display_order )
)
values = result . scalars ( ) . all ( )
return [ {
" id " : str ( value . id ) ,
" title " : value . title ,
" description " : value . description ,
" icon " : value . icon ,
" display_order " : value . display_order
} for value in values ]
# Admin endpoints (auth required)
@api_router.get ( " /admin/about/content " )
async def admin_get_all_about_content (
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Get all about content sections (including inactive) """
result = await db . execute (
select ( AboutContent ) . order_by ( AboutContent . section , AboutContent . display_order )
)
content = result . scalars ( ) . all ( )
return [ {
" id " : str ( item . id ) ,
" section " : item . section ,
" title " : item . title ,
" subtitle " : item . subtitle ,
" content " : item . content ,
" image_url " : item . image_url ,
" data " : item . data ,
" display_order " : item . display_order ,
" is_active " : item . is_active ,
" created_at " : item . created_at . isoformat ( ) if item . created_at else None ,
" updated_at " : item . updated_at . isoformat ( ) if item . updated_at else None
} for item in content ]
@api_router.post ( " /admin/about/content " )
async def admin_create_about_content (
content_data : AboutContentCreate ,
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Create new about content section """
content = AboutContent (
section = content_data . section ,
title = content_data . title ,
subtitle = content_data . subtitle ,
content = content_data . content ,
image_url = content_data . image_url ,
data = content_data . data ,
display_order = content_data . display_order ,
is_active = content_data . is_active
)
db . add ( content )
await db . commit ( )
await db . refresh ( content )
logger . info ( f " Admin { admin . email } created about content section: { content . section } " )
return {
" id " : str ( content . id ) ,
" section " : content . section ,
" title " : content . title ,
" subtitle " : content . subtitle ,
" content " : content . content ,
" image_url " : content . image_url ,
" data " : content . data ,
" display_order " : content . display_order ,
" is_active " : content . is_active ,
" created_at " : content . created_at . isoformat ( ) if content . created_at else None ,
" updated_at " : content . updated_at . isoformat ( ) if content . updated_at else None
}
@api_router.put ( " /admin/about/content/ {content_id} " )
async def admin_update_about_content (
content_id : str ,
content_data : AboutContentUpdate ,
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Update about content section """
2026-02-01 22:31:00 -06:00
result = await db . execute ( select ( AboutContent ) . where ( AboutContent . id == content_id ) )
2026-01-27 18:07:00 -06:00
content = result . scalar_one_or_none ( )
if not content :
raise HTTPException ( status_code = 404 , detail = " Content not found " )
# Update fields
if content_data . title is not None :
content . title = content_data . title
if content_data . subtitle is not None :
content . subtitle = content_data . subtitle
if content_data . content is not None :
content . content = content_data . content
if content_data . image_url is not None :
content . image_url = content_data . image_url
if content_data . data is not None :
content . data = content_data . data
if content_data . display_order is not None :
content . display_order = content_data . display_order
if content_data . is_active is not None :
content . is_active = content_data . is_active
content . updated_at = datetime . utcnow ( )
await db . commit ( )
await db . refresh ( content )
logger . info ( f " Admin { admin . email } updated about content: { content . section } " )
return {
" id " : str ( content . id ) ,
" section " : content . section ,
" title " : content . title ,
" subtitle " : content . subtitle ,
" content " : content . content ,
" image_url " : content . image_url ,
" data " : content . data ,
" display_order " : content . display_order ,
" is_active " : content . is_active ,
" updated_at " : content . updated_at . isoformat ( ) if content . updated_at else None
}
@api_router.delete ( " /admin/about/content/ {content_id} " )
async def admin_delete_about_content (
content_id : str ,
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Delete about content section """
2026-02-01 22:31:00 -06:00
result = await db . execute ( select ( AboutContent ) . where ( AboutContent . id == content_id ) )
2026-01-27 18:07:00 -06:00
content = result . scalar_one_or_none ( )
if not content :
raise HTTPException ( status_code = 404 , detail = " Content not found " )
await db . delete ( content )
await db . commit ( )
logger . info ( f " Admin { admin . email } deleted about content: { content . section } " )
return { " message " : " Content deleted successfully " }
# Team Members CRUD
@api_router.get ( " /admin/about/team " )
async def admin_get_all_team_members (
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Get all team members (including inactive) """
result = await db . execute (
select ( TeamMember ) . order_by ( TeamMember . display_order )
)
members = result . scalars ( ) . all ( )
return [ {
" id " : str ( member . id ) ,
" name " : member . name ,
" role " : member . role ,
" bio " : member . bio ,
" image_url " : member . image_url ,
" email " : member . email ,
" linkedin " : member . linkedin ,
" display_order " : member . display_order ,
" is_active " : member . is_active ,
" created_at " : member . created_at . isoformat ( ) if member . created_at else None ,
" updated_at " : member . updated_at . isoformat ( ) if member . updated_at else None
} for member in members ]
@api_router.post ( " /admin/about/team " )
async def admin_create_team_member (
member_data : TeamMemberCreate ,
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Create new team member """
member = TeamMember (
name = member_data . name ,
role = member_data . role ,
bio = member_data . bio ,
image_url = member_data . image_url ,
email = member_data . email ,
linkedin = member_data . linkedin ,
display_order = member_data . display_order ,
is_active = member_data . is_active
)
db . add ( member )
await db . commit ( )
await db . refresh ( member )
logger . info ( f " Admin { admin . email } created team member: { member . name } " )
return {
" id " : str ( member . id ) ,
" name " : member . name ,
" role " : member . role ,
" bio " : member . bio ,
" image_url " : member . image_url ,
" email " : member . email ,
" linkedin " : member . linkedin ,
" display_order " : member . display_order ,
" is_active " : member . is_active ,
" created_at " : member . created_at . isoformat ( ) if member . created_at else None
}
@api_router.put ( " /admin/about/team/ {member_id} " )
async def admin_update_team_member (
member_id : str ,
member_data : TeamMemberUpdate ,
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Update team member """
2026-02-01 22:31:00 -06:00
result = await db . execute ( select ( TeamMember ) . where ( TeamMember . id == member_id ) )
2026-01-27 18:07:00 -06:00
member = result . scalar_one_or_none ( )
if not member :
raise HTTPException ( status_code = 404 , detail = " Team member not found " )
# Update fields
if member_data . name is not None :
member . name = member_data . name
if member_data . role is not None :
member . role = member_data . role
if member_data . bio is not None :
member . bio = member_data . bio
if member_data . image_url is not None :
member . image_url = member_data . image_url
if member_data . email is not None :
member . email = member_data . email
if member_data . linkedin is not None :
member . linkedin = member_data . linkedin
if member_data . display_order is not None :
member . display_order = member_data . display_order
if member_data . is_active is not None :
member . is_active = member_data . is_active
member . updated_at = datetime . utcnow ( )
await db . commit ( )
await db . refresh ( member )
logger . info ( f " Admin { admin . email } updated team member: { member . name } " )
return {
" id " : str ( member . id ) ,
" name " : member . name ,
" role " : member . role ,
" bio " : member . bio ,
" image_url " : member . image_url ,
" email " : member . email ,
" linkedin " : member . linkedin ,
" display_order " : member . display_order ,
" is_active " : member . is_active ,
" updated_at " : member . updated_at . isoformat ( ) if member . updated_at else None
}
@api_router.delete ( " /admin/about/team/ {member_id} " )
async def admin_delete_team_member (
member_id : str ,
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Delete team member """
2026-02-01 22:31:00 -06:00
result = await db . execute ( select ( TeamMember ) . where ( TeamMember . id == member_id ) )
2026-01-27 18:07:00 -06:00
member = result . scalar_one_or_none ( )
if not member :
raise HTTPException ( status_code = 404 , detail = " Team member not found " )
await db . delete ( member )
await db . commit ( )
logger . info ( f " Admin { admin . email } deleted team member: { member . name } " )
return { " message " : " Team member deleted successfully " }
# Company Values CRUD
@api_router.get ( " /admin/about/values " )
async def admin_get_all_company_values (
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Get all company values (including inactive) """
result = await db . execute (
select ( CompanyValue ) . order_by ( CompanyValue . display_order )
)
values = result . scalars ( ) . all ( )
return [ {
" id " : str ( value . id ) ,
" title " : value . title ,
" description " : value . description ,
" icon " : value . icon ,
" display_order " : value . display_order ,
" is_active " : value . is_active ,
" created_at " : value . created_at . isoformat ( ) if value . created_at else None ,
" updated_at " : value . updated_at . isoformat ( ) if value . updated_at else None
} for value in values ]
@api_router.post ( " /admin/about/values " )
async def admin_create_company_value (
value_data : CompanyValueCreate ,
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Create new company value """
value = CompanyValue (
title = value_data . title ,
description = value_data . description ,
icon = value_data . icon ,
display_order = value_data . display_order ,
is_active = value_data . is_active
)
db . add ( value )
await db . commit ( )
await db . refresh ( value )
logger . info ( f " Admin { admin . email } created company value: { value . title } " )
return {
" id " : str ( value . id ) ,
" title " : value . title ,
" description " : value . description ,
" icon " : value . icon ,
" display_order " : value . display_order ,
" is_active " : value . is_active ,
" created_at " : value . created_at . isoformat ( ) if value . created_at else None
}
@api_router.put ( " /admin/about/values/ {value_id} " )
async def admin_update_company_value (
value_id : str ,
value_data : CompanyValueUpdate ,
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Update company value """
2026-02-01 22:31:00 -06:00
result = await db . execute ( select ( CompanyValue ) . where ( CompanyValue . id == value_id ) )
2026-01-27 18:07:00 -06:00
value = result . scalar_one_or_none ( )
if not value :
raise HTTPException ( status_code = 404 , detail = " Company value not found " )
# Update fields
if value_data . title is not None :
value . title = value_data . title
if value_data . description is not None :
value . description = value_data . description
if value_data . icon is not None :
value . icon = value_data . icon
if value_data . display_order is not None :
value . display_order = value_data . display_order
if value_data . is_active is not None :
value . is_active = value_data . is_active
value . updated_at = datetime . utcnow ( )
await db . commit ( )
await db . refresh ( value )
logger . info ( f " Admin { admin . email } updated company value: { value . title } " )
return {
" id " : str ( value . id ) ,
" title " : value . title ,
" description " : value . description ,
" icon " : value . icon ,
" display_order " : value . display_order ,
" is_active " : value . is_active ,
" updated_at " : value . updated_at . isoformat ( ) if value . updated_at else None
}
@api_router.delete ( " /admin/about/values/ {value_id} " )
async def admin_delete_company_value (
value_id : str ,
admin : User = Depends ( get_admin_user ) ,
db : AsyncSession = Depends ( get_db )
) :
""" Delete company value """
2026-02-01 22:31:00 -06:00
result = await db . execute ( select ( CompanyValue ) . where ( CompanyValue . id == value_id ) )
2026-01-27 18:07:00 -06:00
value = result . scalar_one_or_none ( )
if not value :
raise HTTPException ( status_code = 404 , detail = " Company value not found " )
await db . delete ( value )
await db . commit ( )
logger . info ( f " Admin { admin . email } deleted company value: { value . title } " )
return { " message " : " Company value deleted successfully " }
# ================== SEED DATA ==================
@api_router.post ( " /seed " )
async def seed_data ( db : AsyncSession = Depends ( get_db ) ) :
# Check if data exists
result = await db . execute ( select ( func . count ( Product . id ) ) )
if result . scalar ( ) > 0 :
return { " message " : " Data already seeded " }
# Create admin user
admin = User (
email = " admin@techzone.com " ,
name = " Admin " ,
password = hash_password ( " admin123 " ) ,
role = UserRole . ADMIN
)
db . add ( admin )
# Create products
products = [
Product ( name = " MacBook Pro 16 \" " , description = " Powerful laptop with M3 Pro chip, 18GB RAM, 512GB SSD. " , price = 2499.99 , category = " laptops " , image_url = " https://images.unsplash.com/photo-1517336714731-489689fd1ca8?w=800 " , stock = 15 , brand = " Apple " , specs = { " processor " : " M3 Pro " , " ram " : " 18GB " , " storage " : " 512GB SSD " } ) ,
Product ( name = " Dell XPS 15 " , description = " Ultra-thin laptop with Intel Core i7, 16GB RAM, stunning OLED display. " , price = 1799.99 , category = " laptops " , image_url = " https://images.unsplash.com/photo-1593642632559-0c6d3fc62b89?w=800 " , stock = 20 , brand = " Dell " , specs = { " processor " : " Intel i7 " , " ram " : " 16GB " , " storage " : " 512GB SSD " } ) ,
Product ( name = " iPhone 15 Pro Max " , description = " Latest iPhone with titanium design, A17 Pro chip, 48MP camera. " , price = 1199.99 , category = " phones " , image_url = " https://images.unsplash.com/photo-1695048133142-1a20484d2569?w=800 " , stock = 30 , brand = " Apple " , specs = { " display " : " 6.7 \" OLED " , " camera " : " 48MP " , " storage " : " 256GB " } ) ,
Product ( name = " Samsung Galaxy S24 Ultra " , description = " Premium Android phone with S Pen, 200MP camera, AI features. " , price = 1299.99 , category = " phones " , image_url = " https://images.unsplash.com/photo-1610945265064-0e34e5519bbf?w=800 " , stock = 25 , brand = " Samsung " , specs = { " display " : " 6.8 \" AMOLED " , " camera " : " 200MP " , " storage " : " 512GB " } ) ,
Product ( name = " Sony WH-1000XM5 " , description = " Industry-leading noise cancellation, 30-hour battery. " , price = 349.99 , category = " accessories " , image_url = " https://images.unsplash.com/photo-1505740420928-5e560c06d30e?w=800 " , stock = 40 , brand = " Sony " , specs = { " type " : " Over-ear " , " battery " : " 30 hours " } ) ,
Product ( name = " iPad Pro 12.9 \" " , description = " Powerful tablet with M2 chip, Liquid Retina XDR display. " , price = 1099.99 , category = " tablets " , image_url = " https://images.unsplash.com/photo-1544244015-0df4b3ffc6b0?w=800 " , stock = 18 , brand = " Apple " , specs = { " processor " : " M2 " , " display " : " 12.9 \" XDR " } ) ,
Product ( name = " Apple Watch Ultra 2 " , description = " Rugged smartwatch with titanium case, GPS, 36-hour battery. " , price = 799.99 , category = " wearables " , image_url = " https://images.unsplash.com/photo-1434493789847-2f02dc6ca35d?w=800 " , stock = 22 , brand = " Apple " , specs = { " display " : " 49mm " , " battery " : " 36 hours " } ) ,
Product ( name = " Logitech MX Master 3S " , description = " Premium wireless mouse with 8K DPI sensor, silent clicks. " , price = 99.99 , category = " accessories " , image_url = " https://images.unsplash.com/photo-1527864550417-7fd91fc51a46?w=800 " , stock = 50 , brand = " Logitech " , specs = { " sensor " : " 8K DPI " , " battery " : " 70 days " } ) ,
]
for p in products :
db . add ( p )
# Create services
services = [
Service ( name = " Screen Repair " , description = " Professional screen replacement for phones, tablets, and laptops. " , price = 149.99 , duration = " 1-2 hours " , image_url = " https://images.unsplash.com/photo-1581092918056-0c4c3acd3789?w=800 " , category = " repair " ) ,
Service ( name = " Battery Replacement " , description = " Restore your device ' s battery life with genuine replacement. " , price = 79.99 , duration = " 30-60 mins " , image_url = " https://images.unsplash.com/photo-1609091839311-d5365f9ff1c5?w=800 " , category = " repair " ) ,
Service ( name = " Data Recovery " , description = " Professional data recovery from damaged drives. " , price = 199.99 , duration = " 2-5 days " , image_url = " https://images.unsplash.com/photo-1558494949-ef010cbdcc31?w=800 " , category = " data " ) ,
Service ( name = " Virus Removal " , description = " Complete malware and virus removal with system optimization. " , price = 89.99 , duration = " 1-3 hours " , image_url = " https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5?w=800 " , category = " software " ) ,
Service ( name = " Hardware Upgrade " , description = " Upgrade your RAM, SSD, or other components. " , price = 49.99 , duration = " 1-2 hours " , image_url = " https://images.unsplash.com/photo-1591799265444-d66432b91588?w=800 " , category = " upgrade " ) ,
Service ( name = " Device Setup " , description = " Complete setup service for new devices including data transfer. " , price = 59.99 , duration = " 1-2 hours " , image_url = " https://images.unsplash.com/photo-1531297484001-80022131f5a1?w=800 " , category = " setup " ) ,
]
for s in services :
db . add ( s )
await db . commit ( )
return { " message " : " Data seeded successfully " }
# ================== ROOT ==================
@api_router.get ( " / " )
async def root ( ) :
return { " message " : " TechZone API is running " , " version " : " 2.0.0 " , " status " : " healthy " }
2026-02-01 22:31:00 -06:00
# ============================================
# MEDIA MANAGEMENT ENDPOINTS
# ============================================
def get_media_type ( filename : str ) - > MediaType :
""" Determine media type from file extension """
ext = Path ( filename ) . suffix . lower ( )
if ext in ALLOWED_IMAGE_EXTENSIONS :
return MediaType . IMAGE
elif ext in ALLOWED_DOC_EXTENSIONS :
return MediaType . DOCUMENT
elif ext in ALLOWED_VIDEO_EXTENSIONS :
return MediaType . VIDEO
return MediaType . OTHER
def get_image_dimensions ( file_path : Path ) - > tuple :
""" Get image dimensions if PIL is available """
try :
from PIL import Image
with Image . open ( file_path ) as img :
return img . size
except :
return None , None
@api_router.get ( " /media " )
async def get_all_media (
page : int = Query ( 1 , ge = 1 ) ,
limit : int = Query ( 20 , ge = 1 , le = 100 ) ,
media_type : str = Query ( None ) ,
search : str = Query ( None ) ,
db : AsyncSession = Depends ( get_db ) ,
current_user : User = Depends ( get_current_user )
) :
""" Get all media files with pagination """
if current_user . role not in [ UserRole . ADMIN , UserRole . EMPLOYEE ] :
raise HTTPException ( status_code = 403 , detail = " Not authorized " )
try :
query = select ( Media ) . where ( Media . is_active == True )
if media_type :
query = query . where ( Media . media_type == MediaType ( media_type ) )
if search :
query = query . where (
or_ (
Media . original_filename . ilike ( f " % { search } % " ) ,
Media . title . ilike ( f " % { search } % " ) ,
Media . alt_text . ilike ( f " % { search } % " )
)
)
# Get total count
count_query = select ( func . count ( Media . id ) ) . where ( Media . is_active == True )
if media_type :
count_query = count_query . where ( Media . media_type == MediaType ( media_type ) )
if search :
count_query = count_query . where (
or_ (
Media . original_filename . ilike ( f " % { search } % " ) ,
Media . title . ilike ( f " % { search } % " )
)
)
total_result = await db . execute ( count_query )
total = total_result . scalar ( )
# Apply pagination
offset = ( page - 1 ) * limit
query = query . order_by ( Media . created_at . desc ( ) ) . offset ( offset ) . limit ( limit )
result = await db . execute ( query )
media_items = result . scalars ( ) . all ( )
return {
" items " : [ {
" id " : m . id ,
" filename " : m . filename ,
" original_filename " : m . original_filename ,
" file_url " : m . file_url ,
" file_size " : m . file_size ,
" mime_type " : m . mime_type ,
" media_type " : m . media_type . value ,
" alt_text " : m . alt_text ,
" title " : m . title ,
" width " : m . width ,
" height " : m . height ,
" created_at " : m . created_at . isoformat ( ) if m . created_at else None
} for m in media_items ] ,
" total " : total ,
" page " : page ,
" limit " : limit ,
" pages " : ( total + limit - 1 ) / / limit
}
except Exception as e :
logger . error ( f " Error fetching media: { e } " )
raise HTTPException ( status_code = 500 , detail = str ( e ) )
@api_router.post ( " /media/upload " )
async def upload_media (
file : UploadFile = File ( . . . ) ,
alt_text : str = Form ( None ) ,
title : str = Form ( None ) ,
description : str = Form ( None ) ,
db : AsyncSession = Depends ( get_db ) ,
current_user : User = Depends ( get_current_user )
) :
""" Upload a media file """
if current_user . role not in [ UserRole . ADMIN , UserRole . EMPLOYEE ] :
raise HTTPException ( status_code = 403 , detail = " Not authorized " )
try :
# Validate file
if not file . filename :
raise HTTPException ( status_code = 400 , detail = " No file provided " )
# Check file extension
ext = Path ( file . filename ) . suffix . lower ( )
all_allowed = ALLOWED_IMAGE_EXTENSIONS | ALLOWED_DOC_EXTENSIONS | ALLOWED_VIDEO_EXTENSIONS
if ext not in all_allowed :
raise HTTPException (
status_code = 400 ,
detail = f " File type not allowed. Allowed: { ' , ' . join ( all_allowed ) } "
)
# Read file content
content = await file . read ( )
file_size = len ( content )
# Check file size
if file_size > MAX_FILE_SIZE :
raise HTTPException (
status_code = 400 ,
detail = f " File too large. Maximum size: { MAX_FILE_SIZE / / ( 1024 * 1024 ) } MB "
)
# Generate unique filename
unique_filename = f " { uuid . uuid4 ( ) } { ext } "
file_path = MEDIA_UPLOAD_DIR / unique_filename
# Save file
async with aiofiles . open ( file_path , ' wb ' ) as f :
await f . write ( content )
# Get image dimensions if applicable
width , height = None , None
if ext in ALLOWED_IMAGE_EXTENSIONS :
width , height = get_image_dimensions ( file_path )
# Determine media type
media_type = get_media_type ( file . filename )
# Create media record
media = Media (
filename = unique_filename ,
original_filename = file . filename ,
file_path = str ( file_path ) ,
file_url = f " /uploads/media/ { unique_filename } " ,
file_size = file_size ,
mime_type = file . content_type ,
media_type = media_type ,
alt_text = alt_text or file . filename ,
title = title or Path ( file . filename ) . stem ,
description = description ,
width = width ,
height = height ,
uploaded_by = current_user . id
)
db . add ( media )
await db . commit ( )
await db . refresh ( media )
logger . info ( f " Media uploaded: { unique_filename } by { current_user . email } " )
return {
" id " : media . id ,
" filename " : media . filename ,
" original_filename " : media . original_filename ,
" file_url " : media . file_url ,
" file_size " : media . file_size ,
" mime_type " : media . mime_type ,
" media_type " : media . media_type . value ,
" alt_text " : media . alt_text ,
" title " : media . title ,
" width " : media . width ,
" height " : media . height ,
" created_at " : media . created_at . isoformat ( ) if media . created_at else None
}
except HTTPException :
raise
except Exception as e :
logger . error ( f " Error uploading media: { e } " )
raise HTTPException ( status_code = 500 , detail = str ( e ) )
@api_router.post ( " /media/upload-multiple " )
async def upload_multiple_media (
files : List [ UploadFile ] = File ( . . . ) ,
db : AsyncSession = Depends ( get_db ) ,
current_user : User = Depends ( get_current_user )
) :
""" Upload multiple media files at once """
if current_user . role not in [ UserRole . ADMIN , UserRole . EMPLOYEE ] :
raise HTTPException ( status_code = 403 , detail = " Not authorized " )
uploaded = [ ]
errors = [ ]
for file in files :
try :
if not file . filename :
continue
ext = Path ( file . filename ) . suffix . lower ( )
all_allowed = ALLOWED_IMAGE_EXTENSIONS | ALLOWED_DOC_EXTENSIONS | ALLOWED_VIDEO_EXTENSIONS
if ext not in all_allowed :
errors . append ( { " filename " : file . filename , " error " : " File type not allowed " } )
continue
content = await file . read ( )
file_size = len ( content )
if file_size > MAX_FILE_SIZE :
errors . append ( { " filename " : file . filename , " error " : " File too large " } )
continue
unique_filename = f " { uuid . uuid4 ( ) } { ext } "
file_path = MEDIA_UPLOAD_DIR / unique_filename
async with aiofiles . open ( file_path , ' wb ' ) as f :
await f . write ( content )
width , height = None , None
if ext in ALLOWED_IMAGE_EXTENSIONS :
width , height = get_image_dimensions ( file_path )
media_type = get_media_type ( file . filename )
media = Media (
filename = unique_filename ,
original_filename = file . filename ,
file_path = str ( file_path ) ,
file_url = f " /uploads/media/ { unique_filename } " ,
file_size = file_size ,
mime_type = file . content_type ,
media_type = media_type ,
alt_text = file . filename ,
title = Path ( file . filename ) . stem ,
width = width ,
height = height ,
uploaded_by = current_user . id
)
db . add ( media )
uploaded . append ( {
" filename " : file . filename ,
" file_url " : media . file_url
} )
except Exception as e :
errors . append ( { " filename " : file . filename , " error " : str ( e ) } )
await db . commit ( )
return {
" uploaded " : uploaded ,
" errors " : errors ,
" total_uploaded " : len ( uploaded ) ,
" total_errors " : len ( errors )
}
@api_router.get ( " /media/ {media_id} " )
async def get_media (
media_id : str ,
db : AsyncSession = Depends ( get_db ) ,
current_user : User = Depends ( get_current_user )
) :
""" Get a single media item """
if current_user . role not in [ UserRole . ADMIN , UserRole . EMPLOYEE ] :
raise HTTPException ( status_code = 403 , detail = " Not authorized " )
result = await db . execute ( select ( Media ) . where ( Media . id == media_id ) )
media = result . scalar_one_or_none ( )
if not media :
raise HTTPException ( status_code = 404 , detail = " Media not found " )
return {
" id " : media . id ,
" filename " : media . filename ,
" original_filename " : media . original_filename ,
" file_url " : media . file_url ,
" file_size " : media . file_size ,
" mime_type " : media . mime_type ,
" media_type " : media . media_type . value ,
" alt_text " : media . alt_text ,
" title " : media . title ,
" description " : media . description ,
" width " : media . width ,
" height " : media . height ,
" created_at " : media . created_at . isoformat ( ) if media . created_at else None
}
@api_router.put ( " /media/ {media_id} " )
async def update_media (
media_id : str ,
alt_text : str = Form ( None ) ,
title : str = Form ( None ) ,
description : str = Form ( None ) ,
db : AsyncSession = Depends ( get_db ) ,
current_user : User = Depends ( get_current_user )
) :
""" Update media metadata """
if current_user . role not in [ UserRole . ADMIN , UserRole . EMPLOYEE ] :
raise HTTPException ( status_code = 403 , detail = " Not authorized " )
result = await db . execute ( select ( Media ) . where ( Media . id == media_id ) )
media = result . scalar_one_or_none ( )
if not media :
raise HTTPException ( status_code = 404 , detail = " Media not found " )
if alt_text is not None :
media . alt_text = alt_text
if title is not None :
media . title = title
if description is not None :
media . description = description
await db . commit ( )
await db . refresh ( media )
return {
" id " : media . id ,
" filename " : media . filename ,
" file_url " : media . file_url ,
" alt_text " : media . alt_text ,
" title " : media . title ,
" description " : media . description
}
@api_router.delete ( " /media/ {media_id} " )
async def delete_media (
media_id : str ,
db : AsyncSession = Depends ( get_db ) ,
current_user : User = Depends ( get_current_user )
) :
""" Delete a media file """
if current_user . role != UserRole . ADMIN :
raise HTTPException ( status_code = 403 , detail = " Admin access required " )
result = await db . execute ( select ( Media ) . where ( Media . id == media_id ) )
media = result . scalar_one_or_none ( )
if not media :
raise HTTPException ( status_code = 404 , detail = " Media not found " )
# Delete physical file
try :
file_path = Path ( media . file_path )
if file_path . exists ( ) :
file_path . unlink ( )
except Exception as e :
logger . warning ( f " Could not delete file { media . file_path } : { e } " )
# Delete from database
await db . delete ( media )
await db . commit ( )
logger . info ( f " Media deleted: { media . filename } by { current_user . email } " )
return { " message " : " Media deleted successfully " }
2026-01-27 18:07:00 -06:00
@api_router.get ( " /health " )
async def health_check ( db : AsyncSession = Depends ( get_db ) ) :
""" Comprehensive health check endpoint """
try :
# Test database connection
result = await db . execute ( select ( func . count ( User . id ) ) )
user_count = result . scalar ( )
return {
" status " : " healthy " ,
" database " : " connected " ,
" api_version " : " 2.0.0 " ,
" timestamp " : datetime . now ( timezone . utc ) . isoformat ( )
}
except Exception as e :
logger . error ( f " Health check failed: { e } " )
return {
" status " : " degraded " ,
" database " : " error " ,
" error " : str ( e ) ,
" timestamp " : datetime . now ( timezone . utc ) . isoformat ( )
}
2026-02-01 22:31:00 -06:00
# Add CORS middleware BEFORE including routes
2026-01-27 18:07:00 -06:00
app . add_middleware (
CORSMiddleware ,
allow_credentials = True ,
2026-02-01 22:31:00 -06:00
allow_origins = [ " http://localhost:5300 " , " http://localhost:3000 " , " http://127.0.0.1:5300 " , " http://prompttech.dynns.com " ] ,
allow_methods = [ " GET " , " POST " , " PUT " , " DELETE " , " OPTIONS " , " PATCH " ] ,
2026-01-27 18:07:00 -06:00
allow_headers = [ " * " ] ,
2026-02-01 22:31:00 -06:00
expose_headers = [ " * " ] ,
2026-01-27 18:07:00 -06:00
)
2026-02-01 22:31:00 -06:00
# Include the router
app . include_router ( api_router )
# Mount static files for uploaded images
app . mount ( " /uploads " , StaticFiles ( directory = str ( ROOT_DIR / " uploads " ) ) , name = " uploads " )
2026-01-27 18:07:00 -06:00
if __name__ == " __main__ " :
import uvicorn
uvicorn . run ( app , host = " 0.0.0.0 " , port = 8181 )