Initial commit: SIBU 2.0 MISSION
This commit is contained in:
2
backend/app/api/__init__.py
Normal file
2
backend/app/api/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
"""API routes."""
|
||||
|
||||
122
backend/app/api/analytics.py
Normal file
122
backend/app/api/analytics.py
Normal file
@ -0,0 +1,122 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlmodel import Session, select, func
|
||||
from app.core.database import get_session
|
||||
from app.models.analytics import AnalyticsEvent
|
||||
from app.models.user import User
|
||||
from typing import Optional, Dict
|
||||
from datetime import datetime, timedelta
|
||||
from app.api.deps import get_current_user_optional
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/event")
|
||||
async def log_event(
|
||||
event: Dict,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: Optional[User] = Depends(get_current_user_optional)
|
||||
):
|
||||
user_id = current_user.id if current_user else None
|
||||
|
||||
new_event = AnalyticsEvent(
|
||||
event_name=event.get("event_name"),
|
||||
user_id=user_id,
|
||||
screen_name=event.get("screen_name"),
|
||||
item_id=event.get("item_id"),
|
||||
properties=event.get("properties", {})
|
||||
)
|
||||
session.add(new_event)
|
||||
session.commit()
|
||||
return {"status": "ok"}
|
||||
|
||||
@router.get("/strategic")
|
||||
async def get_strategic_analysis(
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Deep analysis of how businesses and shuttles are performing."""
|
||||
|
||||
# 1. SHUTTLE PERFORMANCE
|
||||
shuttle_previews = session.exec(
|
||||
select(AnalyticsEvent.item_id, func.count(AnalyticsEvent.id))
|
||||
.where(AnalyticsEvent.event_name == "shuttle_view")
|
||||
.group_by(AnalyticsEvent.item_id)
|
||||
).all()
|
||||
|
||||
shuttle_contacts = session.exec(
|
||||
select(AnalyticsEvent.item_id, func.count(AnalyticsEvent.id))
|
||||
.where(AnalyticsEvent.event_name == "shuttle_contact")
|
||||
.group_by(AnalyticsEvent.item_id)
|
||||
).all()
|
||||
|
||||
shuttle_map = {r[0]: {"views": r[1], "contacts": 0} for r in shuttle_previews if r[0]}
|
||||
for r in shuttle_contacts:
|
||||
if r[0] in shuttle_map:
|
||||
shuttle_map[r[0]]["contacts"] = r[1]
|
||||
else:
|
||||
shuttle_map[r[0]] = {"views": 0, "contacts": r[1]}
|
||||
|
||||
# 2. BUSINESS PERFORMANCE
|
||||
biz_views = session.exec(
|
||||
select(AnalyticsEvent.item_id, func.count(AnalyticsEvent.id))
|
||||
.where(AnalyticsEvent.event_name == "business_view")
|
||||
.group_by(AnalyticsEvent.item_id)
|
||||
).all()
|
||||
|
||||
promo_clicks = session.exec(
|
||||
select(AnalyticsEvent.item_id, func.count(AnalyticsEvent.id))
|
||||
.where(AnalyticsEvent.event_name == "promo_click")
|
||||
.group_by(AnalyticsEvent.item_id)
|
||||
).all()
|
||||
|
||||
biz_map = {r[0]: {"views": r[1], "promos": 0} for r in biz_views if r[0]}
|
||||
for r in promo_clicks:
|
||||
if r[0] in biz_map:
|
||||
biz_map[r[0]]["promos"] = r[1]
|
||||
else:
|
||||
biz_map[r[0]] = {"views": 0, "promos": r[1]}
|
||||
|
||||
# 3. TOP STOPS (CASETAS CON MÁS GENTE)
|
||||
top_stops = session.exec(
|
||||
select(AnalyticsEvent.item_id, func.count(AnalyticsEvent.id))
|
||||
.where(AnalyticsEvent.event_name == "stop_selected")
|
||||
.group_by(AnalyticsEvent.item_id)
|
||||
.order_by(func.count(AnalyticsEvent.id).desc())
|
||||
.limit(10)
|
||||
).all()
|
||||
|
||||
# 4. ACTIVE USERS
|
||||
total_active_users = session.exec(select(func.count(func.distinct(AnalyticsEvent.user_id))).where(AnalyticsEvent.user_id != None)).one()
|
||||
|
||||
# 5. PEAK HOURS BY USER TYPE
|
||||
hour_expr = func.extract('hour', AnalyticsEvent.timestamp)
|
||||
reg_usage = session.exec(select(hour_expr, func.count(AnalyticsEvent.id)).where(AnalyticsEvent.user_id != None).group_by(hour_expr)).all()
|
||||
guest_usage = session.exec(select(hour_expr, func.count(AnalyticsEvent.id)).where(AnalyticsEvent.user_id == None).group_by(hour_expr)).all()
|
||||
|
||||
usage_patterns = {
|
||||
"registered": {int(h): c for h, c in reg_usage},
|
||||
"guests": {int(h): c for h, c in guest_usage}
|
||||
}
|
||||
|
||||
return {
|
||||
"shuttles": shuttle_map,
|
||||
"businesses": biz_map,
|
||||
"top_stops": [{"id": r[0], "count": r[1]} for r in top_stops],
|
||||
"users": {
|
||||
"registered_active": total_active_users,
|
||||
"patterns": usage_patterns
|
||||
},
|
||||
"summary": {
|
||||
"total_shuttle_contacts": sum(sh[1] for sh in shuttle_contacts),
|
||||
"total_promo_clicks": sum(p[1] for p in promo_clicks),
|
||||
"total_biz_views": sum(b[1] for b in biz_views)
|
||||
}
|
||||
}
|
||||
|
||||
@router.get("/dashboard/stats")
|
||||
async def get_dashboard_stats(
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
# Base dashboard stats for general overview
|
||||
total_events = session.exec(select(func.count(AnalyticsEvent.id))).one()
|
||||
return {
|
||||
"total_events": total_events
|
||||
}
|
||||
233
backend/app/api/auth/__init__.py
Normal file
233
backend/app/api/auth/__init__.py
Normal file
@ -0,0 +1,233 @@
|
||||
import os
|
||||
import shutil
|
||||
from uuid import uuid4
|
||||
from typing import Annotated, Optional
|
||||
from fastapi import APIRouter, HTTPException, status, Depends, UploadFile, File, Form
|
||||
from sqlmodel import Session, select
|
||||
from app.core.database import get_session
|
||||
from app.core.security import verify_password, get_password_hash, create_access_token, get_token_payload
|
||||
from app.models.user import User, DriverProfile, UserRole, VehicleType
|
||||
from app.api.deps import oauth2_scheme
|
||||
from app.schemas.user import PassengerCreate, Token, UserResponse, LoginRequest
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
UPLOAD_DIR = "uploads"
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(
|
||||
data: LoginRequest,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
print(f"DEBUG: Login attempt for email: {data.email}")
|
||||
user = session.exec(select(User).where(User.email == data.email)).first()
|
||||
if not user:
|
||||
print(f"DEBUG: User not found: {data.email}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if not verify_password(data.password, user.hashed_password):
|
||||
print(f"DEBUG: Invalid password for: {data.email}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
print(f"DEBUG: Successful login for: {data.email} as {user.role}")
|
||||
|
||||
# Token expiration can be extended if keep_session is true
|
||||
import datetime
|
||||
expires = datetime.timedelta(days=30) if data.keep_session else datetime.timedelta(days=1)
|
||||
|
||||
access_token = create_access_token(
|
||||
subject=user.id,
|
||||
role=user.role,
|
||||
full_name=user.full_name,
|
||||
expires_delta=expires
|
||||
)
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"role": user.role,
|
||||
"full_name": user.full_name,
|
||||
"profile_photo_url": user.profile_photo_url
|
||||
}
|
||||
|
||||
|
||||
@router.post("/register/passenger", response_model=UserResponse)
|
||||
async def register_passenger(
|
||||
data: PassengerCreate,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
# Check if user exists
|
||||
existing_user = session.exec(select(User).where(User.email == data.email)).first()
|
||||
if existing_user:
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
|
||||
new_user = User(
|
||||
email=data.email,
|
||||
full_name=data.full_name,
|
||||
hashed_password=get_password_hash(data.password),
|
||||
role=UserRole.PASSENGER
|
||||
)
|
||||
session.add(new_user)
|
||||
session.commit()
|
||||
session.refresh(new_user)
|
||||
return new_user
|
||||
|
||||
|
||||
@router.post("/register/driver", response_model=UserResponse)
|
||||
async def register_driver(
|
||||
full_name: str = Form(...),
|
||||
email: str = Form(...),
|
||||
phone_number: str = Form(...),
|
||||
password: str = Form(...),
|
||||
cedula: str = Form(...),
|
||||
vehicle_type: VehicleType = Form(...),
|
||||
license_plate: str = Form(...),
|
||||
cooperative_name: Optional[str] = Form(None),
|
||||
profile_photo: Optional[UploadFile] = File(None),
|
||||
vehicle_photo: UploadFile = File(...),
|
||||
shift: Optional[str] = Form(None),
|
||||
payment_methods: Optional[str] = Form(None),
|
||||
speaks_english: bool = Form(False),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
# Check if user exists
|
||||
existing_user = session.exec(select(User).where(User.email == email)).first()
|
||||
if existing_user:
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
|
||||
# Save photos
|
||||
profile_photo_url = None
|
||||
if profile_photo:
|
||||
ext = os.path.splitext(profile_photo.filename)[1]
|
||||
filename = f"{uuid4()}{ext}"
|
||||
path = os.path.join(UPLOAD_DIR, "profiles", filename)
|
||||
with open(path, "wb") as buffer:
|
||||
shutil.copyfileobj(profile_photo.file, buffer)
|
||||
profile_photo_url = f"/uploads/profiles/{filename}"
|
||||
|
||||
ext_v = os.path.splitext(vehicle_photo.filename)[1]
|
||||
v_filename = f"{uuid4()}{ext_v}"
|
||||
v_path = os.path.join(UPLOAD_DIR, "vehicles", v_filename)
|
||||
with open(v_path, "wb") as buffer:
|
||||
shutil.copyfileobj(vehicle_photo.file, buffer)
|
||||
vehicle_photo_url = f"/uploads/vehicles/{v_filename}"
|
||||
|
||||
# Create User
|
||||
new_user = User(
|
||||
email=email,
|
||||
full_name=full_name,
|
||||
hashed_password=get_password_hash(password),
|
||||
role=UserRole.DRIVER,
|
||||
is_verified=True, # Auto verify since it's admin registered now
|
||||
profile_photo_url=profile_photo_url
|
||||
)
|
||||
session.add(new_user)
|
||||
session.commit()
|
||||
session.refresh(new_user)
|
||||
|
||||
# Create Driver Profile
|
||||
profile = DriverProfile(
|
||||
user_id=new_user.id,
|
||||
cedula=cedula,
|
||||
vehicle_type=vehicle_type,
|
||||
license_plate=license_plate,
|
||||
photo_url=profile_photo_url,
|
||||
vehicle_photo_url=vehicle_photo_url,
|
||||
cooperative_name=cooperative_name,
|
||||
shift=shift,
|
||||
payment_methods=payment_methods,
|
||||
speaks_english=speaks_english
|
||||
)
|
||||
session.add(profile)
|
||||
session.commit()
|
||||
|
||||
return new_user
|
||||
|
||||
@router.get("/me")
|
||||
async def get_current_user(
|
||||
token: Annotated[str, Depends(oauth2_scheme)],
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get current logged in user details."""
|
||||
payload = get_token_payload(token)
|
||||
user_id = payload.get("sub")
|
||||
|
||||
user = session.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
result = {
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"full_name": user.full_name,
|
||||
"role": user.role,
|
||||
"is_verified": user.is_verified,
|
||||
"profile_photo_url": user.profile_photo_url,
|
||||
"driver_profile": None
|
||||
}
|
||||
|
||||
if user.driver_profile:
|
||||
dp = user.driver_profile
|
||||
result["driver_profile"] = {
|
||||
"cedula": dp.cedula,
|
||||
"vehicle_type": dp.vehicle_type,
|
||||
"license_plate": dp.license_plate,
|
||||
"cooperative_name": dp.cooperative_name,
|
||||
"photo_url": dp.photo_url,
|
||||
"shift": dp.shift,
|
||||
"payment_methods": dp.payment_methods,
|
||||
"speaks_english": dp.speaks_english
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@router.patch("/me", response_model=UserResponse)
|
||||
async def update_me(
|
||||
full_name: Optional[str] = Form(None),
|
||||
password: Optional[str] = Form(None),
|
||||
profile_photo: Optional[UploadFile] = File(None),
|
||||
token: Annotated[str, Depends(oauth2_scheme)] = None,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Update current user profile info."""
|
||||
payload = get_token_payload(token)
|
||||
user_id = payload.get("sub")
|
||||
user = session.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
if full_name:
|
||||
user.full_name = full_name
|
||||
if password:
|
||||
user.hashed_password = get_password_hash(password)
|
||||
|
||||
if profile_photo:
|
||||
# Create directory if not exists
|
||||
profile_dir = os.path.join(UPLOAD_DIR, "profiles")
|
||||
os.makedirs(profile_dir, exist_ok=True)
|
||||
|
||||
ext = os.path.splitext(profile_photo.filename)[1]
|
||||
filename = f"{uuid4()}{ext}"
|
||||
path = os.path.join(profile_dir, filename)
|
||||
with open(path, "wb") as buffer:
|
||||
shutil.copyfileobj(profile_photo.file, buffer)
|
||||
user.profile_photo_url = f"/uploads/profiles/{filename}"
|
||||
|
||||
# If user is driver, also update driver profile photo_url for backwards compatibility/sync
|
||||
if user.driver_profile:
|
||||
user.driver_profile.photo_url = user.profile_photo_url
|
||||
session.add(user.driver_profile)
|
||||
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
return user
|
||||
91
backend/app/api/bus_stops/__init__.py
Normal file
91
backend/app/api/bus_stops/__init__.py
Normal file
@ -0,0 +1,91 @@
|
||||
"""Bus stops API endpoints."""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session, select
|
||||
from typing import List
|
||||
|
||||
from app.core.database import get_session
|
||||
from app.models.bus_stop import BusStop
|
||||
from app.schemas.bus_stop import BusStopResponse, BusStopCreate, BusStopUpdate
|
||||
from app.api.deps import get_current_admin
|
||||
|
||||
router = APIRouter(prefix="/api/bus-stops", tags=["bus-stops"])
|
||||
|
||||
|
||||
@router.get("", response_model=List[BusStopResponse])
|
||||
async def get_bus_stops(session: Session = Depends(get_session)):
|
||||
"""Get all bus stops."""
|
||||
statement = select(BusStop)
|
||||
stops = session.exec(statement).all()
|
||||
return stops
|
||||
|
||||
|
||||
@router.get("/{stop_id}", response_model=BusStopResponse)
|
||||
async def get_bus_stop(stop_id: str, session: Session = Depends(get_session)):
|
||||
"""Get a single bus stop by ID."""
|
||||
stop = session.get(BusStop, stop_id)
|
||||
if not stop:
|
||||
raise HTTPException(status_code=404, detail="Bus stop not found")
|
||||
return stop
|
||||
|
||||
|
||||
@router.get("/{stop_id}/routes")
|
||||
async def get_bus_stop_routes(stop_id: str, session: Session = Depends(get_session)):
|
||||
"""Get all routes passing through a bus stop."""
|
||||
from app.models.route_stop import RouteStop
|
||||
from app.models.route import Route
|
||||
|
||||
statement = select(Route).join(
|
||||
RouteStop, RouteStop.route_id == Route.id
|
||||
).where(RouteStop.stop_id == stop_id)
|
||||
|
||||
routes = session.exec(statement).all()
|
||||
return routes
|
||||
|
||||
@router.post("", response_model=BusStopResponse)
|
||||
async def create_bus_stop(
|
||||
bus_stop: BusStopCreate,
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Create a new bus stop (Admin only)."""
|
||||
db_stop = BusStop.model_validate(bus_stop)
|
||||
session.add(db_stop)
|
||||
session.commit()
|
||||
session.refresh(db_stop)
|
||||
return db_stop
|
||||
|
||||
@router.put("/{stop_id}", response_model=BusStopResponse)
|
||||
async def update_bus_stop(
|
||||
stop_id: str,
|
||||
stop_update: BusStopUpdate,
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Update a bus stop (Admin only)."""
|
||||
db_stop = session.get(BusStop, stop_id)
|
||||
if not db_stop:
|
||||
raise HTTPException(status_code=404, detail="Bus stop not found")
|
||||
|
||||
stop_data = stop_update.model_dump(exclude_unset=True)
|
||||
for key, value in stop_data.items():
|
||||
setattr(db_stop, key, value)
|
||||
|
||||
session.add(db_stop)
|
||||
session.commit()
|
||||
session.refresh(db_stop)
|
||||
return db_stop
|
||||
|
||||
@router.delete("/{stop_id}")
|
||||
async def delete_bus_stop(
|
||||
stop_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Delete a bus stop (Admin only)."""
|
||||
db_stop = session.get(BusStop, stop_id)
|
||||
if not db_stop:
|
||||
raise HTTPException(status_code=404, detail="Bus stop not found")
|
||||
|
||||
session.delete(db_stop)
|
||||
session.commit()
|
||||
return {"ok": True}
|
||||
158
backend/app/api/businesses.py
Normal file
158
backend/app/api/businesses.py
Normal file
@ -0,0 +1,158 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Form, File, UploadFile
|
||||
from sqlmodel import Session, select
|
||||
from typing import List, Optional
|
||||
from app.core.database import get_session
|
||||
from app.models.business import Business
|
||||
from app.models.user import User, UserRole
|
||||
from app.api.deps import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/businesses", tags=["businesses"])
|
||||
|
||||
@router.get("", response_model=List[Business])
|
||||
async def list_businesses(
|
||||
*,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""List all businesses."""
|
||||
statement = select(Business)
|
||||
businesses = session.exec(statement).all()
|
||||
return businesses
|
||||
|
||||
@router.post("", response_model=Business)
|
||||
async def create_business(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
name: str = Form(...),
|
||||
category: str = Form(...),
|
||||
address: str = Form(...),
|
||||
phone: Optional[str] = Form(None),
|
||||
social_media: Optional[str] = Form(None),
|
||||
latitude: Optional[float] = Form(None),
|
||||
longitude: Optional[float] = Form(None),
|
||||
image: Optional[UploadFile] = File(None),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Create a new business (Promoters and Admins only)."""
|
||||
if current_user.role not in [UserRole.ADMIN, UserRole.PROMOTER]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only promoters and admins can manage businesses"
|
||||
)
|
||||
|
||||
image_url = None
|
||||
if image:
|
||||
import os
|
||||
import shutil
|
||||
from uuid import uuid4
|
||||
UPLOAD_DIR = "uploads/businesses"
|
||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||
ext = os.path.splitext(image.filename)[1]
|
||||
filename = f"{uuid4()}{ext}"
|
||||
path = os.path.join(UPLOAD_DIR, filename)
|
||||
with open(path, "wb") as buffer:
|
||||
shutil.copyfileobj(image.file, buffer)
|
||||
image_url = f"/uploads/businesses/{filename}"
|
||||
|
||||
db_business = Business(
|
||||
name=name,
|
||||
category=category,
|
||||
address=address,
|
||||
phone=phone,
|
||||
social_media=social_media,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
image_url=image_url
|
||||
)
|
||||
session.add(db_business)
|
||||
session.commit()
|
||||
session.refresh(db_business)
|
||||
return db_business
|
||||
|
||||
@router.patch("/{business_id}", response_model=Business)
|
||||
async def update_business(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
business_id: str,
|
||||
name: Optional[str] = Form(None),
|
||||
category: Optional[str] = Form(None),
|
||||
address: Optional[str] = Form(None),
|
||||
phone: Optional[str] = Form(None),
|
||||
social_media: Optional[str] = Form(None),
|
||||
latitude: Optional[float] = Form(None),
|
||||
longitude: Optional[float] = Form(None),
|
||||
image: Optional[UploadFile] = File(None),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Update a business (Promoters and Admins only)."""
|
||||
if current_user.role not in [UserRole.ADMIN, UserRole.PROMOTER]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only promoters and admins can manage businesses"
|
||||
)
|
||||
|
||||
db_business = session.get(Business, business_id)
|
||||
if not db_business:
|
||||
raise HTTPException(status_code=404, detail="Business not found")
|
||||
|
||||
if name is not None:
|
||||
db_business.name = name
|
||||
if category is not None:
|
||||
db_business.category = category
|
||||
if address is not None:
|
||||
db_business.address = address
|
||||
if phone is not None:
|
||||
db_business.phone = phone
|
||||
if social_media is not None:
|
||||
db_business.social_media = social_media
|
||||
if latitude is not None:
|
||||
db_business.latitude = latitude
|
||||
if longitude is not None:
|
||||
db_business.longitude = longitude
|
||||
|
||||
if image:
|
||||
import os
|
||||
import shutil
|
||||
from uuid import uuid4
|
||||
UPLOAD_DIR = "uploads/businesses"
|
||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||
ext = os.path.splitext(image.filename)[1]
|
||||
filename = f"{uuid4()}{ext}"
|
||||
path = os.path.join(UPLOAD_DIR, filename)
|
||||
with open(path, "wb") as buffer:
|
||||
shutil.copyfileobj(image.file, buffer)
|
||||
db_business.image_url = f"/uploads/businesses/{filename}"
|
||||
|
||||
session.add(db_business)
|
||||
session.commit()
|
||||
session.refresh(db_business)
|
||||
return db_business
|
||||
|
||||
@router.get("/{business_id}", response_model=Business)
|
||||
async def get_business(business_id: str, session: Session = Depends(get_session)):
|
||||
"""Get a single business by ID."""
|
||||
business = session.get(Business, business_id)
|
||||
if not business:
|
||||
raise HTTPException(status_code=404, detail="Business not found")
|
||||
return business
|
||||
|
||||
@router.delete("/{business_id}")
|
||||
async def delete_business(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
business_id: str,
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Delete a business (Promoters and Admins only)."""
|
||||
if current_user.role not in [UserRole.ADMIN, UserRole.PROMOTER]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only promoters and admins can manage businesses"
|
||||
)
|
||||
|
||||
db_business = session.get(Business, business_id)
|
||||
if not db_business:
|
||||
raise HTTPException(status_code=404, detail="Business not found")
|
||||
|
||||
session.delete(db_business)
|
||||
session.commit()
|
||||
return {"status": "success", "message": "Business deleted"}
|
||||
94
backend/app/api/coupons.py
Normal file
94
backend/app/api/coupons.py
Normal file
@ -0,0 +1,94 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
from sqlalchemy.orm import joinedload
|
||||
from typing import List
|
||||
from app.core.database import get_session
|
||||
from app.models.coupon import Coupon, CouponCreate, CouponUpdate
|
||||
from app.models.user import User, UserRole
|
||||
from app.api.deps import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/coupons", tags=["coupons"])
|
||||
|
||||
@router.get("", response_model=List[Coupon])
|
||||
async def list_coupons(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
active_only: bool = True
|
||||
):
|
||||
"""List all coupons."""
|
||||
statement = select(Coupon).options(joinedload(Coupon.business))
|
||||
if active_only:
|
||||
statement = statement.where(Coupon.is_active)
|
||||
|
||||
coupons = session.exec(statement).all()
|
||||
return coupons
|
||||
|
||||
@router.post("", response_model=Coupon)
|
||||
async def create_coupon(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
coupon_in: CouponCreate,
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Create a new coupon (Promoters and Admins only)."""
|
||||
if current_user.role not in [UserRole.ADMIN, UserRole.PROMOTER]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only promoters and admins can create coupons"
|
||||
)
|
||||
|
||||
db_coupon = Coupon.from_orm(coupon_in)
|
||||
session.add(db_coupon)
|
||||
session.commit()
|
||||
session.refresh(db_coupon)
|
||||
return db_coupon
|
||||
|
||||
@router.patch("/{coupon_id}", response_model=Coupon)
|
||||
async def update_coupon(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
coupon_id: str,
|
||||
coupon_in: CouponUpdate,
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Update a coupon (Promoters and Admins only)."""
|
||||
if current_user.role not in [UserRole.ADMIN, UserRole.PROMOTER]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only promoters and admins can update coupons"
|
||||
)
|
||||
|
||||
db_coupon = session.get(Coupon, coupon_id)
|
||||
if not db_coupon:
|
||||
raise HTTPException(status_code=404, detail="Coupon not found")
|
||||
|
||||
coupon_data = coupon_in.dict(exclude_unset=True)
|
||||
for key, value in coupon_data.items():
|
||||
setattr(db_coupon, key, value)
|
||||
|
||||
session.add(db_coupon)
|
||||
session.commit()
|
||||
session.refresh(db_coupon)
|
||||
return db_coupon
|
||||
|
||||
@router.delete("/{coupon_id}")
|
||||
async def delete_coupon(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
coupon_id: str,
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Delete a coupon (Promoters and Admins only)."""
|
||||
if current_user.role not in [UserRole.ADMIN, UserRole.PROMOTER]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only promoters and admins can delete coupons"
|
||||
)
|
||||
|
||||
db_coupon = session.get(Coupon, coupon_id)
|
||||
if not db_coupon:
|
||||
raise HTTPException(status_code=404, detail="Coupon not found")
|
||||
|
||||
session.delete(db_coupon)
|
||||
session.commit()
|
||||
return {"status": "success", "message": "Coupon deleted"}
|
||||
76
backend/app/api/deps.py
Normal file
76
backend/app/api/deps.py
Normal file
@ -0,0 +1,76 @@
|
||||
from typing import Annotated, Dict, Optional
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import jwt, JWTError
|
||||
from app.core.config import settings
|
||||
from app.core.security import ALGORITHM
|
||||
from sqlmodel import Session
|
||||
from app.core.database import get_session
|
||||
from app.models.user import User
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login", auto_error=False)
|
||||
|
||||
def get_token_payload(token: Annotated[str, Depends(oauth2_scheme)]) -> Dict:
|
||||
if not token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Not authenticated",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, settings.secret_key, algorithms=[ALGORITHM])
|
||||
return payload
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
|
||||
async def get_current_user_token(token: Annotated[str, Depends(oauth2_scheme)]) -> Dict:
|
||||
return get_token_payload(token)
|
||||
|
||||
async def get_current_admin(token: Annotated[str, Depends(oauth2_scheme)]) -> bool:
|
||||
from app.models.user import UserRole
|
||||
payload = get_token_payload(token)
|
||||
role: str = payload.get("role")
|
||||
|
||||
# Check for both "admin" and "ADMIN" for robust authorization
|
||||
if role not in ["admin", "ADMIN", UserRole.ADMIN]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="The user doesn't have enough privileges",
|
||||
)
|
||||
return True
|
||||
|
||||
async def get_current_user(
|
||||
token: Annotated[str, Depends(oauth2_scheme)],
|
||||
session: Session = Depends(get_session)
|
||||
) -> User:
|
||||
payload = get_token_payload(token)
|
||||
user_id = payload.get("sub")
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
)
|
||||
user = session.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return user
|
||||
|
||||
async def get_current_user_optional(
|
||||
token: Optional[str] = Depends(oauth2_scheme),
|
||||
session: Session = Depends(get_session)
|
||||
) -> Optional[User]:
|
||||
if not token:
|
||||
return None
|
||||
try:
|
||||
payload = jwt.decode(token, settings.secret_key, algorithms=[ALGORITHM])
|
||||
user_id = payload.get("sub")
|
||||
if not user_id:
|
||||
return None
|
||||
return session.get(User, user_id)
|
||||
except JWTError:
|
||||
return None
|
||||
109
backend/app/api/favorites/__init__.py
Normal file
109
backend/app/api/favorites/__init__.py
Normal file
@ -0,0 +1,109 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlmodel import Session, select
|
||||
from typing import List
|
||||
from app.core.database import get_session
|
||||
from app.models.favorite import Favorite
|
||||
from app.api.deps import get_current_user
|
||||
from app.models.user import User
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/favorites", tags=["favorites"])
|
||||
|
||||
|
||||
class FavoriteCreate(BaseModel):
|
||||
item_type: str # 'coupon', 'business', 'taxi', 'route'
|
||||
item_id: str
|
||||
item_name: str | None = None
|
||||
item_image: str | None = None
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_favorites(
|
||||
item_type: str | None = None,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(get_current_user)
|
||||
) -> List[Favorite]:
|
||||
"""Get all favorites for the current user, optionally filtered by type."""
|
||||
statement = select(Favorite).where(Favorite.user_id == current_user.id)
|
||||
|
||||
if item_type:
|
||||
statement = statement.where(Favorite.item_type == item_type)
|
||||
|
||||
statement = statement.order_by(Favorite.created_at.desc())
|
||||
favorites = session.exec(statement).all()
|
||||
return list(favorites)
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def add_favorite(
|
||||
favorite_data: FavoriteCreate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(get_current_user)
|
||||
) -> Favorite:
|
||||
"""Add an item to favorites."""
|
||||
# Check if already favorited
|
||||
existing = session.exec(
|
||||
select(Favorite).where(
|
||||
Favorite.user_id == current_user.id,
|
||||
Favorite.item_type == favorite_data.item_type,
|
||||
Favorite.item_id == favorite_data.item_id
|
||||
)
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Item already in favorites")
|
||||
|
||||
favorite = Favorite(
|
||||
user_id=current_user.id,
|
||||
item_type=favorite_data.item_type,
|
||||
item_id=favorite_data.item_id,
|
||||
item_name=favorite_data.item_name,
|
||||
item_image=favorite_data.item_image
|
||||
)
|
||||
session.add(favorite)
|
||||
session.commit()
|
||||
session.refresh(favorite)
|
||||
return favorite
|
||||
|
||||
|
||||
@router.delete("/{item_type}/{item_id}")
|
||||
async def remove_favorite(
|
||||
item_type: str,
|
||||
item_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Remove an item from favorites."""
|
||||
favorite = session.exec(
|
||||
select(Favorite).where(
|
||||
Favorite.user_id == current_user.id,
|
||||
Favorite.item_type == item_type,
|
||||
Favorite.item_id == item_id
|
||||
)
|
||||
).first()
|
||||
|
||||
if not favorite:
|
||||
raise HTTPException(status_code=404, detail="Favorite not found")
|
||||
|
||||
session.delete(favorite)
|
||||
session.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/check/{item_type}/{item_id}")
|
||||
async def check_favorite(
|
||||
item_type: str,
|
||||
item_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: User = Depends(get_current_user)
|
||||
) -> dict:
|
||||
"""Check if an item is favorited."""
|
||||
favorite = session.exec(
|
||||
select(Favorite).where(
|
||||
Favorite.user_id == current_user.id,
|
||||
Favorite.item_type == item_type,
|
||||
Favorite.item_id == item_id
|
||||
)
|
||||
).first()
|
||||
|
||||
return {"is_favorite": favorite is not None}
|
||||
96
backend/app/api/reports.py
Normal file
96
backend/app/api/reports.py
Normal file
@ -0,0 +1,96 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from app.core.database import get_session
|
||||
from app.api.deps import get_current_admin, get_current_user_optional
|
||||
from app.models.report import Report
|
||||
from app.models.user import User
|
||||
from app.schemas.report import ReportCreate, ReportUpdate, ReportResponse
|
||||
|
||||
router = APIRouter(prefix="/api/reports", tags=["reports"])
|
||||
|
||||
@router.post("", response_model=ReportResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_report(
|
||||
report_in: ReportCreate,
|
||||
session: Session = Depends(get_session),
|
||||
current_user: Optional[User] = Depends(get_current_user_optional)
|
||||
):
|
||||
"""Create a new user report."""
|
||||
report = Report(
|
||||
message=report_in.message,
|
||||
user_id=current_user.id if current_user else None
|
||||
)
|
||||
session.add(report)
|
||||
session.commit()
|
||||
session.refresh(report)
|
||||
|
||||
return ReportResponse(
|
||||
id=report.id,
|
||||
user_id=report.user_id,
|
||||
user_name=current_user.full_name if current_user else "Anónimo",
|
||||
message=report.message,
|
||||
status=report.status,
|
||||
created_at=report.created_at
|
||||
)
|
||||
|
||||
@router.get("", response_model=List[ReportResponse])
|
||||
async def get_reports(
|
||||
session: Session = Depends(get_session),
|
||||
admin_auth: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Get all reports (Admin only)."""
|
||||
statement = select(Report)
|
||||
results = session.exec(statement).all()
|
||||
|
||||
reports = []
|
||||
for report in results:
|
||||
user_name = "Anónimo"
|
||||
if report.user_id:
|
||||
user = session.get(User, report.user_id)
|
||||
if user:
|
||||
user_name = user.full_name
|
||||
|
||||
reports.append(ReportResponse(
|
||||
id=report.id,
|
||||
user_id=report.user_id,
|
||||
user_name=user_name,
|
||||
message=report.message,
|
||||
status=report.status,
|
||||
created_at=report.created_at
|
||||
))
|
||||
|
||||
return reports
|
||||
|
||||
@router.patch("/{report_id}", response_model=ReportResponse)
|
||||
async def update_report_status(
|
||||
report_id: UUID,
|
||||
report_update: ReportUpdate,
|
||||
session: Session = Depends(get_session),
|
||||
admin_auth: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Update report status (Admin only)."""
|
||||
report = session.get(Report, report_id)
|
||||
if not report:
|
||||
raise HTTPException(status_code=404, detail="Report not found")
|
||||
|
||||
report.status = report_update.status
|
||||
session.add(report)
|
||||
session.commit()
|
||||
session.refresh(report)
|
||||
|
||||
user_name = "Anónimo"
|
||||
if report.user_id:
|
||||
user = session.get(User, report.user_id)
|
||||
if user:
|
||||
user_name = user.full_name
|
||||
|
||||
return ReportResponse(
|
||||
id=report.id,
|
||||
user_id=report.user_id,
|
||||
user_name=user_name,
|
||||
message=report.message,
|
||||
status=report.status,
|
||||
created_at=report.created_at
|
||||
)
|
||||
229
backend/app/api/routes/__init__.py
Normal file
229
backend/app/api/routes/__init__.py
Normal file
@ -0,0 +1,229 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlmodel import Session, select
|
||||
from typing import List, Optional
|
||||
from sqlalchemy import func
|
||||
|
||||
from app.core.database import get_session
|
||||
from app.models.route import Route
|
||||
from app.models.route_stop import RouteStop
|
||||
from app.schemas.route import RouteResponse, RouteCreate, RouteUpdate
|
||||
from app.schemas.route_stop import RouteStopCreate, RouteStopUpdate
|
||||
from app.api.deps import get_current_admin
|
||||
|
||||
router = APIRouter(prefix="/api/routes", tags=["routes"])
|
||||
|
||||
|
||||
@router.get("", response_model=List[RouteResponse])
|
||||
async def get_routes(
|
||||
origin_city: Optional[str] = Query(None),
|
||||
destination_city: Optional[str] = Query(None),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all routes with optional filtering by origin and destination city."""
|
||||
statement = select(Route)
|
||||
|
||||
if origin_city:
|
||||
statement = statement.where(Route.origin_city.contains(origin_city))
|
||||
|
||||
if destination_city:
|
||||
statement = statement.where(Route.destination_city.contains(destination_city))
|
||||
|
||||
routes = session.exec(statement).all()
|
||||
return routes
|
||||
|
||||
|
||||
@router.get("/{route_id}", response_model=RouteResponse)
|
||||
async def get_route(route_id: str, session: Session = Depends(get_session)):
|
||||
"""Get a single route by ID."""
|
||||
route = session.get(Route, route_id)
|
||||
if not route:
|
||||
raise HTTPException(status_code=404, detail="Route not found")
|
||||
return route
|
||||
|
||||
|
||||
@router.get("/{route_id}/stops")
|
||||
async def get_route_stops(route_id: str, session: Session = Depends(get_session)):
|
||||
"""Get all stops for a route."""
|
||||
from app.models.route_stop import RouteStop
|
||||
from app.models.bus_stop import BusStop
|
||||
|
||||
statement = select(RouteStop, BusStop).join(
|
||||
BusStop, RouteStop.stop_id == BusStop.id
|
||||
).where(RouteStop.route_id == route_id).order_by(RouteStop.stop_order)
|
||||
|
||||
results = session.exec(statement).all()
|
||||
# Merge RouteStop data into BusStop response
|
||||
stops = []
|
||||
for route_stop, bus_stop in results:
|
||||
stop_data = bus_stop.model_dump()
|
||||
stop_data['stop_order'] = route_stop.stop_order
|
||||
stop_data['travel_time_minutes'] = route_stop.travel_time_minutes
|
||||
stop_data['stop_delay_minutes'] = route_stop.stop_delay_minutes
|
||||
stop_data['is_pickup_point'] = route_stop.is_pickup_point
|
||||
stop_data['is_dropoff_point'] = route_stop.is_dropoff_point
|
||||
stops.append(stop_data)
|
||||
|
||||
return stops
|
||||
|
||||
@router.post("", response_model=RouteResponse)
|
||||
async def create_route(
|
||||
route: RouteCreate,
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Create a new route (Admin only)."""
|
||||
db_route = Route.model_validate(route)
|
||||
session.add(db_route)
|
||||
session.commit()
|
||||
session.refresh(db_route)
|
||||
return db_route
|
||||
|
||||
@router.put("/{route_id}", response_model=RouteResponse)
|
||||
async def update_route(
|
||||
route_id: str,
|
||||
route_update: RouteUpdate,
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Update a route (Admin only)."""
|
||||
db_route = session.get(Route, route_id)
|
||||
if not db_route:
|
||||
raise HTTPException(status_code=404, detail="Route not found")
|
||||
|
||||
route_data = route_update.model_dump(exclude_unset=True)
|
||||
for key, value in route_data.items():
|
||||
setattr(db_route, key, value)
|
||||
|
||||
session.add(db_route)
|
||||
session.commit()
|
||||
session.refresh(db_route)
|
||||
return db_route
|
||||
|
||||
@router.delete("/{route_id}")
|
||||
async def delete_route(
|
||||
route_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Delete a route (Admin only)."""
|
||||
db_route = session.get(Route, route_id)
|
||||
if not db_route:
|
||||
raise HTTPException(status_code=404, detail="Route not found")
|
||||
|
||||
session.delete(db_route)
|
||||
session.commit()
|
||||
return {"ok": True}
|
||||
|
||||
# Route Stop Management with Cascade
|
||||
|
||||
@router.post("/{route_id}/stops")
|
||||
async def add_stop_to_route(
|
||||
route_id: str,
|
||||
stop_data: RouteStopCreate,
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Add a stop to a route with cascading order adjustment."""
|
||||
# 1. Check if route exists
|
||||
route = session.get(Route, route_id)
|
||||
if not route:
|
||||
raise HTTPException(status_code=404, detail="Route not found")
|
||||
|
||||
# 2. Determine stop order
|
||||
if stop_data.stop_order is None:
|
||||
# Append to end
|
||||
max_order = session.exec(select(func.max(RouteStop.stop_order)).where(RouteStop.route_id == route_id)).one()
|
||||
# handle case where max_order is None (no stops)
|
||||
stop_data.stop_order = (max_order or 0) + 1
|
||||
else:
|
||||
# Shift existing stops equal to or greater than new order
|
||||
existing_stops = session.exec(
|
||||
select(RouteStop).where(RouteStop.route_id == route_id, RouteStop.stop_order >= stop_data.stop_order)
|
||||
).all()
|
||||
for stop in existing_stops:
|
||||
stop.stop_order += 1
|
||||
session.add(stop)
|
||||
|
||||
# 3. Create new RouteStop
|
||||
new_stop = RouteStop(
|
||||
route_id=route_id,
|
||||
stop_id=stop_data.stop_id,
|
||||
stop_order=stop_data.stop_order,
|
||||
travel_time_minutes=stop_data.travel_time_minutes,
|
||||
stop_delay_minutes=stop_data.stop_delay_minutes or 0,
|
||||
is_pickup_point=stop_data.is_pickup_point,
|
||||
is_dropoff_point=stop_data.is_dropoff_point
|
||||
)
|
||||
session.add(new_stop)
|
||||
session.commit()
|
||||
session.refresh(new_stop)
|
||||
return new_stop
|
||||
|
||||
@router.put("/{route_id}/stops/{stop_id}")
|
||||
async def update_route_stop_order(
|
||||
route_id: str,
|
||||
stop_id: str,
|
||||
update_data: RouteStopUpdate,
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Update a route stop, potentially reordering others."""
|
||||
# This assumes we find the connection by route_id and stop_id.
|
||||
# NOTE: If a stop is on a route multiple times, this logic needs ID, but for now assuming unique stop per route.
|
||||
# Actually RouteStop has its own ID but we are using stop_id in path. Let's find the RouteStop entry.
|
||||
|
||||
route_stop = session.exec(
|
||||
select(RouteStop).where(RouteStop.route_id == route_id, RouteStop.stop_id == stop_id)
|
||||
).first()
|
||||
|
||||
if not route_stop:
|
||||
raise HTTPException(status_code=404, detail="Stop not found on this route")
|
||||
|
||||
old_order = route_stop.stop_order
|
||||
|
||||
# Update fields
|
||||
if update_data.is_pickup_point is not None:
|
||||
route_stop.is_pickup_point = update_data.is_pickup_point
|
||||
if update_data.is_dropoff_point is not None:
|
||||
route_stop.is_dropoff_point = update_data.is_dropoff_point
|
||||
if update_data.travel_time_minutes is not None:
|
||||
route_stop.travel_time_minutes = update_data.travel_time_minutes
|
||||
if update_data.stop_delay_minutes is not None:
|
||||
route_stop.stop_delay_minutes = update_data.stop_delay_minutes
|
||||
|
||||
# Reordering logic
|
||||
if update_data.stop_order is not None and update_data.stop_order != old_order:
|
||||
new_order = update_data.stop_order
|
||||
|
||||
if new_order > old_order:
|
||||
# Moving down: shift stops between old+1 and new DOWN (-1)
|
||||
stops_to_shift = session.exec(
|
||||
select(RouteStop).where(
|
||||
RouteStop.route_id == route_id,
|
||||
RouteStop.stop_order > old_order,
|
||||
RouteStop.stop_order <= new_order
|
||||
)
|
||||
).all()
|
||||
for s in stops_to_shift:
|
||||
s.stop_order -= 1
|
||||
session.add(s)
|
||||
else:
|
||||
# Moving up: shift stops between new and old-1 UP (+1)
|
||||
stops_to_shift = session.exec(
|
||||
select(RouteStop).where(
|
||||
RouteStop.route_id == route_id,
|
||||
RouteStop.stop_order >= new_order,
|
||||
RouteStop.stop_order < old_order
|
||||
)
|
||||
).all()
|
||||
for s in stops_to_shift:
|
||||
s.stop_order += 1
|
||||
session.add(s)
|
||||
|
||||
route_stop.stop_order = new_order
|
||||
|
||||
session.add(route_stop)
|
||||
session.commit()
|
||||
session.refresh(route_stop)
|
||||
return route_stop
|
||||
|
||||
86
backend/app/api/schedules/__init__.py
Normal file
86
backend/app/api/schedules/__init__.py
Normal file
@ -0,0 +1,86 @@
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from sqlmodel import Session, select
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from app.core.database import get_session
|
||||
from app.models.bus_schedule import BusSchedule
|
||||
from app.api.deps import get_current_admin
|
||||
|
||||
router = APIRouter(prefix="/api/schedules", tags=["schedules"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_schedules(
|
||||
route_id: Optional[UUID] = Query(None),
|
||||
stop_id: Optional[UUID] = Query(None),
|
||||
only_published: bool = Query(True),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get schedules for a route or stop."""
|
||||
statement = select(BusSchedule)
|
||||
|
||||
if only_published:
|
||||
statement = statement.where(BusSchedule.is_published)
|
||||
|
||||
if route_id:
|
||||
statement = statement.where(BusSchedule.route_id == route_id)
|
||||
|
||||
if stop_id:
|
||||
from app.models.route_stop import RouteStop
|
||||
statement = statement.join(
|
||||
RouteStop, BusSchedule.route_id == RouteStop.route_id
|
||||
).where(RouteStop.stop_id == stop_id)
|
||||
|
||||
schedules = session.exec(statement).all()
|
||||
return schedules
|
||||
|
||||
@router.post("")
|
||||
async def create_schedule(
|
||||
schedule: BusSchedule,
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Create a new bus schedule (Admin only)."""
|
||||
db_schedule = BusSchedule.model_validate(schedule)
|
||||
session.add(db_schedule)
|
||||
session.commit()
|
||||
session.refresh(db_schedule)
|
||||
return db_schedule
|
||||
|
||||
@router.put("/{schedule_id}")
|
||||
async def update_schedule(
|
||||
schedule_id: UUID,
|
||||
schedule_update: dict,
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Update a bus schedule (Admin only)."""
|
||||
db_schedule = session.get(BusSchedule, schedule_id)
|
||||
if not db_schedule:
|
||||
raise HTTPException(status_code=404, detail="Schedule not found")
|
||||
|
||||
for key, value in schedule_update.items():
|
||||
if hasattr(db_schedule, key):
|
||||
setattr(db_schedule, key, value)
|
||||
|
||||
session.add(db_schedule)
|
||||
session.commit()
|
||||
session.refresh(db_schedule)
|
||||
return db_schedule
|
||||
|
||||
@router.delete("/{schedule_id}")
|
||||
async def delete_schedule(
|
||||
schedule_id: UUID,
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Delete a bus schedule (Admin only)."""
|
||||
db_schedule = session.get(BusSchedule, schedule_id)
|
||||
if not db_schedule:
|
||||
raise HTTPException(status_code=404, detail="Schedule not found")
|
||||
|
||||
session.delete(db_schedule)
|
||||
session.commit()
|
||||
return {"ok": True}
|
||||
|
||||
161
backend/app/api/shuttles/__init__.py
Normal file
161
backend/app/api/shuttles/__init__.py
Normal file
@ -0,0 +1,161 @@
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException, UploadFile, File, Form
|
||||
from sqlmodel import Session, select
|
||||
from typing import Optional, List
|
||||
import os
|
||||
import shutil
|
||||
from uuid import uuid4, UUID
|
||||
from app.core.database import get_session
|
||||
from app.models.shuttle import Shuttle
|
||||
from app.api.deps import get_current_admin
|
||||
|
||||
router = APIRouter(prefix="/api/shuttles", tags=["shuttles"])
|
||||
|
||||
UPLOAD_DIR = "uploads"
|
||||
|
||||
@router.get("", response_model=List[Shuttle])
|
||||
async def get_shuttles(
|
||||
origin: Optional[str] = Query(None),
|
||||
destination: Optional[str] = Query(None),
|
||||
company_name: Optional[str] = Query(None),
|
||||
trip_type: Optional[str] = Query(None),
|
||||
is_active: bool = Query(True),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all shuttles with optional filters."""
|
||||
statement = select(Shuttle).where(Shuttle.is_active == is_active)
|
||||
|
||||
if origin:
|
||||
statement = statement.where(Shuttle.origin.contains(origin))
|
||||
if destination:
|
||||
statement = statement.where(Shuttle.destination.contains(destination))
|
||||
if company_name:
|
||||
statement = statement.where(Shuttle.company_name.contains(company_name))
|
||||
if trip_type:
|
||||
statement = statement.where(Shuttle.trip_type == trip_type)
|
||||
|
||||
shuttles = session.exec(statement).all()
|
||||
return shuttles
|
||||
|
||||
@router.post("", response_model=Shuttle)
|
||||
async def create_shuttle(
|
||||
route_name: str = Form(...),
|
||||
origin: str = Form(...),
|
||||
destination: str = Form(...),
|
||||
vehicle_type: str = Form(...),
|
||||
company_name: Optional[str] = Form(None),
|
||||
trip_type: str = Form("one_way"),
|
||||
price_per_person: Optional[float] = Form(None),
|
||||
price_private_trip: Optional[float] = Form(None),
|
||||
estimated_duration: str = Form(...),
|
||||
contact_whatsapp: str = Form(...),
|
||||
phone_number: Optional[str] = Form(None),
|
||||
english_speaking: bool = Form(False),
|
||||
description: Optional[str] = Form(None),
|
||||
departure_times: Optional[str] = Form(None),
|
||||
is_active: bool = Form(True),
|
||||
image: Optional[UploadFile] = File(None),
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Create a new shuttle trip (Admin only)."""
|
||||
image_url = None
|
||||
if image:
|
||||
ext = os.path.splitext(image.filename)[1]
|
||||
filename = f"{uuid4()}{ext}"
|
||||
path = os.path.join(UPLOAD_DIR, "vehicles", filename)
|
||||
with open(path, "wb") as buffer:
|
||||
shutil.copyfileobj(image.file, buffer)
|
||||
image_url = f"/uploads/vehicles/{filename}"
|
||||
|
||||
shuttle = Shuttle(
|
||||
route_name=route_name,
|
||||
origin=origin,
|
||||
destination=destination,
|
||||
vehicle_type=vehicle_type,
|
||||
company_name=company_name,
|
||||
trip_type=trip_type,
|
||||
price_per_person=price_per_person,
|
||||
price_private_trip=price_private_trip,
|
||||
estimated_duration=estimated_duration,
|
||||
contact_whatsapp=contact_whatsapp,
|
||||
phone_number=phone_number,
|
||||
english_speaking=english_speaking,
|
||||
description=description,
|
||||
departure_times=departure_times,
|
||||
image_url=image_url,
|
||||
is_active=is_active
|
||||
)
|
||||
session.add(shuttle)
|
||||
session.commit()
|
||||
session.refresh(shuttle)
|
||||
return shuttle
|
||||
|
||||
@router.put("/{shuttle_id}", response_model=Shuttle)
|
||||
async def update_shuttle(
|
||||
shuttle_id: UUID,
|
||||
route_name: str = Form(...),
|
||||
origin: str = Form(...),
|
||||
destination: str = Form(...),
|
||||
vehicle_type: str = Form(...),
|
||||
company_name: Optional[str] = Form(None),
|
||||
trip_type: str = Form("one_way"),
|
||||
price_per_person: Optional[float] = Form(None),
|
||||
price_private_trip: Optional[float] = Form(None),
|
||||
estimated_duration: str = Form(...),
|
||||
contact_whatsapp: str = Form(...),
|
||||
phone_number: Optional[str] = Form(None),
|
||||
english_speaking: bool = Form(False),
|
||||
description: Optional[str] = Form(None),
|
||||
departure_times: Optional[str] = Form(None),
|
||||
is_active: bool = Form(True),
|
||||
image: Optional[UploadFile] = File(None),
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Update a shuttle trip (Admin only)."""
|
||||
db_shuttle = session.get(Shuttle, shuttle_id)
|
||||
if not db_shuttle:
|
||||
raise HTTPException(status_code=404, detail="Shuttle not found")
|
||||
|
||||
db_shuttle.route_name = route_name
|
||||
db_shuttle.origin = origin
|
||||
db_shuttle.destination = destination
|
||||
db_shuttle.vehicle_type = vehicle_type
|
||||
db_shuttle.company_name = company_name
|
||||
db_shuttle.trip_type = trip_type
|
||||
db_shuttle.price_per_person = price_per_person
|
||||
db_shuttle.price_private_trip = price_private_trip
|
||||
db_shuttle.estimated_duration = estimated_duration
|
||||
db_shuttle.contact_whatsapp = contact_whatsapp
|
||||
db_shuttle.phone_number = phone_number
|
||||
db_shuttle.english_speaking = english_speaking
|
||||
db_shuttle.description = description
|
||||
db_shuttle.departure_times = departure_times
|
||||
db_shuttle.is_active = is_active
|
||||
|
||||
if image:
|
||||
ext = os.path.splitext(image.filename)[1]
|
||||
filename = f"{uuid4()}{ext}"
|
||||
path = os.path.join(UPLOAD_DIR, "vehicles", filename)
|
||||
with open(path, "wb") as buffer:
|
||||
shutil.copyfileobj(image.file, buffer)
|
||||
db_shuttle.image_url = f"/uploads/vehicles/{filename}"
|
||||
|
||||
session.add(db_shuttle)
|
||||
session.commit()
|
||||
session.refresh(db_shuttle)
|
||||
return db_shuttle
|
||||
|
||||
@router.delete("/{shuttle_id}")
|
||||
async def delete_shuttle(
|
||||
shuttle_id: UUID,
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Delete a shuttle trip (Admin only)."""
|
||||
db_shuttle = session.get(Shuttle, shuttle_id)
|
||||
if not db_shuttle:
|
||||
raise HTTPException(status_code=404, detail="Shuttle not found")
|
||||
session.delete(db_shuttle)
|
||||
session.commit()
|
||||
return {"ok": True}
|
||||
145
backend/app/api/taxis/__init__.py
Normal file
145
backend/app/api/taxis/__init__.py
Normal file
@ -0,0 +1,145 @@
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException, UploadFile, File, Form
|
||||
from sqlmodel import Session, select
|
||||
from typing import Optional
|
||||
import os
|
||||
import shutil
|
||||
from uuid import uuid4
|
||||
from app.core.database import get_session
|
||||
from app.models.taxi import Taxi
|
||||
from app.api.deps import get_current_admin
|
||||
|
||||
router = APIRouter(prefix="/api/taxis", tags=["taxis"])
|
||||
|
||||
UPLOAD_DIR = "uploads"
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_taxis(
|
||||
corregimiento: Optional[str] = Query(None),
|
||||
shift: Optional[str] = Query(None),
|
||||
english_speaking: Optional[bool] = Query(None),
|
||||
is_active: Optional[bool] = Query(None),
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""Get all taxis with optional filters."""
|
||||
statement = select(Taxi)
|
||||
|
||||
if is_active is not None:
|
||||
statement = statement.where(Taxi.is_active == is_active)
|
||||
|
||||
if corregimiento:
|
||||
statement = statement.where(Taxi.corregimiento.contains(corregimiento))
|
||||
|
||||
if shift:
|
||||
statement = statement.where(Taxi.shift == shift)
|
||||
|
||||
if english_speaking is not None:
|
||||
statement = statement.where(Taxi.english_speaking == english_speaking)
|
||||
|
||||
taxis = session.exec(statement).all()
|
||||
return taxis
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_taxi(
|
||||
owner_name: str = Form(...),
|
||||
phone_number: str = Form(...),
|
||||
license_plate: str = Form(...),
|
||||
corregimiento: str = Form(...),
|
||||
shift: str = Form(...),
|
||||
cooperative: Optional[str] = Form(None),
|
||||
rating: float = Form(5.0),
|
||||
english_speaking: bool = Form(False),
|
||||
is_active: bool = Form(True),
|
||||
image: Optional[UploadFile] = File(None),
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Create a new taxi entry (Admin only)."""
|
||||
image_url = None
|
||||
if image:
|
||||
ext = os.path.splitext(image.filename)[1]
|
||||
filename = f"{uuid4()}{ext}"
|
||||
path = os.path.join(UPLOAD_DIR, "profiles", filename)
|
||||
with open(path, "wb") as buffer:
|
||||
shutil.copyfileobj(image.file, buffer)
|
||||
image_url = f"/uploads/profiles/{filename}"
|
||||
|
||||
taxi = Taxi(
|
||||
owner_name=owner_name,
|
||||
phone_number=phone_number,
|
||||
license_plate=license_plate,
|
||||
cooperative=cooperative,
|
||||
corregimiento=corregimiento,
|
||||
shift=shift,
|
||||
rating=rating,
|
||||
english_speaking=english_speaking,
|
||||
image_url=image_url,
|
||||
is_active=is_active
|
||||
)
|
||||
session.add(taxi)
|
||||
session.commit()
|
||||
session.refresh(taxi)
|
||||
return taxi
|
||||
|
||||
|
||||
@router.put("/{taxi_id}")
|
||||
async def update_taxi(
|
||||
taxi_id: str,
|
||||
owner_name: str = Form(...),
|
||||
phone_number: str = Form(...),
|
||||
license_plate: str = Form(...),
|
||||
corregimiento: str = Form(...),
|
||||
shift: str = Form(...),
|
||||
cooperative: Optional[str] = Form(None),
|
||||
rating: float = Form(5.0),
|
||||
english_speaking: bool = Form(False),
|
||||
is_active: bool = Form(True),
|
||||
image: Optional[UploadFile] = File(None),
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Update a taxi entry (Admin only)."""
|
||||
db_taxi = session.get(Taxi, taxi_id)
|
||||
if not db_taxi:
|
||||
raise HTTPException(status_code=404, detail="Taxi not found")
|
||||
|
||||
# Update fields
|
||||
db_taxi.owner_name = owner_name
|
||||
db_taxi.phone_number = phone_number
|
||||
db_taxi.license_plate = license_plate
|
||||
db_taxi.corregimiento = corregimiento
|
||||
db_taxi.shift = shift
|
||||
db_taxi.cooperative = cooperative
|
||||
db_taxi.rating = rating
|
||||
db_taxi.english_speaking = english_speaking
|
||||
db_taxi.is_active = is_active
|
||||
|
||||
# Handle image upload
|
||||
if image:
|
||||
ext = os.path.splitext(image.filename)[1]
|
||||
filename = f"{uuid4()}{ext}"
|
||||
path = os.path.join(UPLOAD_DIR, "profiles", filename)
|
||||
with open(path, "wb") as buffer:
|
||||
shutil.copyfileobj(image.file, buffer)
|
||||
db_taxi.image_url = f"/uploads/profiles/{filename}"
|
||||
|
||||
session.add(db_taxi)
|
||||
session.commit()
|
||||
session.refresh(db_taxi)
|
||||
return db_taxi
|
||||
|
||||
|
||||
@router.delete("/{taxi_id}")
|
||||
async def delete_taxi(
|
||||
taxi_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Delete a taxi entry (Admin only)."""
|
||||
db_taxi = session.get(Taxi, taxi_id)
|
||||
if not db_taxi:
|
||||
raise HTTPException(status_code=404, detail="Taxi not found")
|
||||
session.delete(db_taxi)
|
||||
session.commit()
|
||||
return {"ok": True}
|
||||
84
backend/app/api/telemetry.py
Normal file
84
backend/app/api/telemetry.py
Normal file
@ -0,0 +1,84 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
from typing import List
|
||||
from datetime import datetime, timedelta
|
||||
from app.core.database import get_session
|
||||
from app.models.telemetry import Telemetry, TelemetryCreate, VehicleStatus
|
||||
from app.models.user import User
|
||||
from app.api.deps import get_current_user
|
||||
|
||||
router = APIRouter(prefix="/api/telemetry", tags=["telemetry"])
|
||||
|
||||
@router.post("", response_model=Telemetry)
|
||||
async def create_telemetry_record(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
telemetry_in: TelemetryCreate,
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Create a new telemetry record for the current driver.
|
||||
"""
|
||||
if current_user.role != "driver":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only drivers can send telemetry data"
|
||||
)
|
||||
|
||||
db_telemetry = Telemetry(
|
||||
user_id=current_user.id,
|
||||
latitude=telemetry_in.latitude,
|
||||
longitude=telemetry_in.longitude,
|
||||
speed=telemetry_in.speed,
|
||||
heading=telemetry_in.heading,
|
||||
status=telemetry_in.status
|
||||
)
|
||||
|
||||
session.add(db_telemetry)
|
||||
session.commit()
|
||||
session.refresh(db_telemetry)
|
||||
return db_telemetry
|
||||
|
||||
@router.get("/active", response_model=List[dict])
|
||||
async def get_active_units(
|
||||
*,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
"""
|
||||
Get the latest location of all active units (last 5 minutes).
|
||||
"""
|
||||
# Subquery to get the latest timestamp per user
|
||||
five_minutes_ago = datetime.utcnow() - timedelta(minutes=5)
|
||||
|
||||
# This is a bit complex in SQLModel/SQLAlchemy for "latest record per group"
|
||||
# We'll use a simpler approach: get all records from last 5 mins and filter in python
|
||||
# or use a more efficient distinct on if supported.
|
||||
|
||||
statement = (
|
||||
select(Telemetry)
|
||||
.where(Telemetry.timestamp >= five_minutes_ago)
|
||||
.where(Telemetry.status == VehicleStatus.ACTIVE)
|
||||
.order_by(Telemetry.user_id, Telemetry.timestamp.desc())
|
||||
)
|
||||
|
||||
results = session.exec(statement).all()
|
||||
|
||||
# Filter to get only the latest unique user_id
|
||||
latest_units = {}
|
||||
for t in results:
|
||||
if t.user_id not in latest_units:
|
||||
# We also want the driver name and vehicle info
|
||||
user = session.get(User, t.user_id)
|
||||
latest_units[t.user_id] = {
|
||||
"user_id": t.user_id,
|
||||
"full_name": user.full_name if user else "Unknown",
|
||||
"latitude": t.latitude,
|
||||
"longitude": t.longitude,
|
||||
"speed": t.speed,
|
||||
"heading": t.heading,
|
||||
"timestamp": t.timestamp,
|
||||
"vehicle_type": user.driver_profile.vehicle_type if user and user.driver_profile else "unknown",
|
||||
"license_plate": user.driver_profile.license_plate if user and user.driver_profile else "unknown"
|
||||
}
|
||||
|
||||
return list(latest_units.values())
|
||||
116
backend/app/api/users.py
Normal file
116
backend/app/api/users.py
Normal file
@ -0,0 +1,116 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlmodel import Session, select
|
||||
from uuid import UUID
|
||||
|
||||
from app.core.database import get_session
|
||||
from app.models.user import User
|
||||
from app.api.deps import get_current_admin
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["users"])
|
||||
|
||||
@router.get("/search")
|
||||
async def search_users(
|
||||
email: str = Query(..., description="Email to search for"),
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Search for users by email (Admin only)."""
|
||||
statement = select(User).where(User.email.contains(email))
|
||||
users = session.exec(statement).all()
|
||||
|
||||
# Clean response (don't send hashed passwords)
|
||||
return [
|
||||
{
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"full_name": user.full_name,
|
||||
"role": user.role,
|
||||
"is_verified": user.is_verified,
|
||||
"created_at": user.created_at
|
||||
} for user in users
|
||||
]
|
||||
|
||||
@router.get("/{user_id}")
|
||||
async def get_user_details(
|
||||
user_id: UUID,
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Get detailed user info including driver profile (Admin only)."""
|
||||
user = session.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
result = {
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"full_name": user.full_name,
|
||||
"role": user.role,
|
||||
"is_active": user.is_active,
|
||||
"is_verified": user.is_verified,
|
||||
"created_at": user.created_at,
|
||||
"driver_profile": None
|
||||
}
|
||||
|
||||
if user.driver_profile:
|
||||
dp = user.driver_profile
|
||||
result["driver_profile"] = {
|
||||
"cedula": dp.cedula,
|
||||
"vehicle_type": dp.vehicle_type,
|
||||
"license_plate": dp.license_plate,
|
||||
"cooperative_name": dp.cooperative_name,
|
||||
"photo_url": dp.photo_url,
|
||||
"vehicle_photo_url": dp.vehicle_photo_url,
|
||||
"shift": dp.shift,
|
||||
"payment_methods": dp.payment_methods,
|
||||
"speaks_english": dp.speaks_english
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@router.get("/pending-drivers")
|
||||
async def get_pending_drivers(
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""List drivers waiting for verification (Admin only)."""
|
||||
# Find users with DRIVER role who are NOT verified
|
||||
from app.models.user import UserRole
|
||||
statement = select(User).where(User.role == UserRole.DRIVER, User.is_verified.is_(False))
|
||||
|
||||
return [
|
||||
{
|
||||
"id": driver.id,
|
||||
"email": driver.email,
|
||||
"full_name": driver.full_name,
|
||||
"created_at": driver.created_at,
|
||||
"driver_profile": {
|
||||
"cedula": driver.driver_profile.cedula,
|
||||
"vehicle_type": driver.driver_profile.vehicle_type,
|
||||
"license_plate": driver.driver_profile.license_plate,
|
||||
"cooperative_name": driver.driver_profile.cooperative_name,
|
||||
"shift": driver.driver_profile.shift,
|
||||
"payment_methods": driver.driver_profile.payment_methods,
|
||||
"speaks_english": driver.driver_profile.speaks_english
|
||||
} if driver.driver_profile else None
|
||||
} for driver in session.exec(statement).all()
|
||||
]
|
||||
|
||||
@router.post("/{user_id}/verify")
|
||||
async def verify_user(
|
||||
user_id: UUID,
|
||||
is_verified: bool = Query(..., description="True to approve, False to stay unverified/reject"),
|
||||
session: Session = Depends(get_session),
|
||||
_: bool = Depends(get_current_admin)
|
||||
):
|
||||
"""Approve or Reject a user verification (Admin only)."""
|
||||
user = session.get(User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
user.is_verified = is_verified
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
|
||||
return {"id": user.id, "email": user.email, "is_verified": user.is_verified}
|
||||
Reference in New Issue
Block a user