|
@@ -16,7 +16,7 @@ router = APIRouter(tags=["portfolio"])
|
|
|
@router.get("/portfolio")
|
|
@router.get("/portfolio")
|
|
|
async def get_public_portfolio():
|
|
async def get_public_portfolio():
|
|
|
query = """
|
|
query = """
|
|
|
- SELECT p.id, p.file_path, COALESCE(o.material_name, 'Showcase') as material_name, p.order_id
|
|
|
|
|
|
|
+ SELECT p.id, p.file_path, COALESCE(o.material_name, 'Showcase') as material_name, p.order_id, p.caption
|
|
|
FROM order_photos p
|
|
FROM order_photos p
|
|
|
LEFT JOIN orders o ON p.order_id = o.id
|
|
LEFT JOIN orders o ON p.order_id = o.id
|
|
|
WHERE p.is_public = TRUE AND (o.id IS NULL OR o.allow_portfolio = TRUE)
|
|
WHERE p.is_public = TRUE AND (o.id IS NULL OR o.allow_portfolio = TRUE)
|
|
@@ -27,10 +27,10 @@ async def get_public_portfolio():
|
|
|
@router.get("/admin/all-photos")
|
|
@router.get("/admin/all-photos")
|
|
|
async def admin_get_all_photos(admin: dict = Depends(require_admin)):
|
|
async def admin_get_all_photos(admin: dict = Depends(require_admin)):
|
|
|
query = """
|
|
query = """
|
|
|
- SELECT p.id, p.file_path, p.is_public, p.order_id, o.allow_portfolio,
|
|
|
|
|
|
|
+ SELECT p.id, p.file_path, p.is_public, p.order_id, p.caption, o.allow_portfolio,
|
|
|
o.first_name, o.last_name, COALESCE(o.material_name, 'Manual') as material_name
|
|
o.first_name, o.last_name, COALESCE(o.material_name, 'Manual') as material_name
|
|
|
FROM order_photos p
|
|
FROM order_photos p
|
|
|
- JOIN orders o ON p.order_id = o.id
|
|
|
|
|
|
|
+ LEFT JOIN orders o ON p.order_id = o.id
|
|
|
ORDER BY p.created_at DESC
|
|
ORDER BY p.created_at DESC
|
|
|
"""
|
|
"""
|
|
|
return db.execute_query(query)
|
|
return db.execute_query(query)
|
|
@@ -39,6 +39,7 @@ async def admin_get_all_photos(admin: dict = Depends(require_admin)):
|
|
|
async def admin_upload_order_photo(
|
|
async def admin_upload_order_photo(
|
|
|
order_id: int,
|
|
order_id: int,
|
|
|
is_public: bool = Form(False),
|
|
is_public: bool = Form(False),
|
|
|
|
|
+ caption: Optional[str] = Form(None),
|
|
|
file: UploadFile = File(...),
|
|
file: UploadFile = File(...),
|
|
|
admin: dict = Depends(require_admin)
|
|
admin: dict = Depends(require_admin)
|
|
|
):
|
|
):
|
|
@@ -54,15 +55,15 @@ async def admin_upload_order_photo(
|
|
|
with open(disk_path, "wb") as buffer:
|
|
with open(disk_path, "wb") as buffer:
|
|
|
shutil.copyfileobj(file.file, buffer)
|
|
shutil.copyfileobj(file.file, buffer)
|
|
|
|
|
|
|
|
- query = "INSERT INTO order_photos (order_id, file_path, is_public) VALUES (%s, %s, %s)"
|
|
|
|
|
- photo_id = db.execute_commit(query, (order_id, db_file_path, is_public))
|
|
|
|
|
|
|
+ query = "INSERT INTO order_photos (order_id, file_path, is_public, caption) VALUES (%s, %s, %s, %s)"
|
|
|
|
|
+ photo_id = db.execute_commit(query, (order_id, db_file_path, is_public, caption))
|
|
|
|
|
|
|
|
await audit_service.log(
|
|
await audit_service.log(
|
|
|
user_id=admin['id'],
|
|
user_id=admin['id'],
|
|
|
action="upload_order_photo",
|
|
action="upload_order_photo",
|
|
|
target_type="order",
|
|
target_type="order",
|
|
|
target_id=order_id,
|
|
target_id=order_id,
|
|
|
- details={"photo_id": photo_id, "is_public": is_public}
|
|
|
|
|
|
|
+ details={"photo_id": photo_id, "is_public": is_public, "caption": caption}
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
# NOTIFY USER VIA WEBSOCKET
|
|
# NOTIFY USER VIA WEBSOCKET
|
|
@@ -70,32 +71,64 @@ async def admin_upload_order_photo(
|
|
|
if order_info:
|
|
if order_info:
|
|
|
await global_manager.notify_order_update(order_info[0]['user_id'], order_id)
|
|
await global_manager.notify_order_update(order_info[0]['user_id'], order_id)
|
|
|
|
|
|
|
|
- return {"id": photo_id, "file_path": db_file_path, "is_public": is_public}
|
|
|
|
|
|
|
+ return {"id": photo_id, "file_path": db_file_path, "is_public": is_public, "caption": caption}
|
|
|
|
|
+
|
|
|
|
|
+@router.post("/admin/portfolio/upload")
|
|
|
|
|
+async def admin_upload_portfolio_photo(
|
|
|
|
|
+ is_public: bool = Form(True),
|
|
|
|
|
+ caption: Optional[str] = Form(None),
|
|
|
|
|
+ file: UploadFile = File(...),
|
|
|
|
|
+ admin: dict = Depends(require_admin)
|
|
|
|
|
+):
|
|
|
|
|
+ if not file.filename: raise HTTPException(status_code=400, detail="Invalid file")
|
|
|
|
|
+ unique_filename = f"{uuid.uuid4()}{os.path.splitext(file.filename)[1]}"
|
|
|
|
|
+ disk_path = os.path.join(config.UPLOAD_DIR, unique_filename)
|
|
|
|
|
+ db_file_path = f"uploads/{unique_filename}"
|
|
|
|
|
+
|
|
|
|
|
+ with open(disk_path, "wb") as buffer:
|
|
|
|
|
+ shutil.copyfileobj(file.file, buffer)
|
|
|
|
|
+
|
|
|
|
|
+ query = "INSERT INTO order_photos (order_id, file_path, is_public, caption) VALUES (%s, %s, %s, %s)"
|
|
|
|
|
+ photo_id = db.execute_commit(query, (None, db_file_path, is_public, caption))
|
|
|
|
|
+
|
|
|
|
|
+ await audit_service.log(
|
|
|
|
|
+ user_id=admin['id'],
|
|
|
|
|
+ action="upload_portfolio_photo",
|
|
|
|
|
+ target_type="photo",
|
|
|
|
|
+ target_id=photo_id,
|
|
|
|
|
+ details={"is_public": is_public, "caption": caption}
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ return {"id": photo_id, "file_path": db_file_path, "is_public": is_public, "caption": caption}
|
|
|
|
|
|
|
|
@router.patch("/admin/photos/{photo_id}")
|
|
@router.patch("/admin/photos/{photo_id}")
|
|
|
async def admin_update_photo_status(photo_id: int, data: schemas.PhotoUpdate, admin: dict = Depends(require_admin)):
|
|
async def admin_update_photo_status(photo_id: int, data: schemas.PhotoUpdate, admin: dict = Depends(require_admin)):
|
|
|
- query = "SELECT p.*, o.allow_portfolio FROM order_photos p JOIN orders o ON p.order_id = o.id WHERE p.id = %s"
|
|
|
|
|
|
|
+ query = "SELECT p.*, o.allow_portfolio FROM order_photos p LEFT JOIN orders o ON p.order_id = o.id WHERE p.id = %s"
|
|
|
photo_data = db.execute_query(query, (photo_id,))
|
|
photo_data = db.execute_query(query, (photo_id,))
|
|
|
if not photo_data: raise HTTPException(status_code=404, detail="Photo not found")
|
|
if not photo_data: raise HTTPException(status_code=404, detail="Photo not found")
|
|
|
- if data.is_public and not photo_data[0]['allow_portfolio']:
|
|
|
|
|
|
|
+
|
|
|
|
|
+ # Check consent if linked to order
|
|
|
|
|
+ if data.is_public and photo_data[0]['order_id'] is not None and not photo_data[0]['allow_portfolio']:
|
|
|
raise HTTPException(status_code=400, detail="Cannot make public: User did not consent to portfolio usage")
|
|
raise HTTPException(status_code=400, detail="Cannot make public: User did not consent to portfolio usage")
|
|
|
- db.execute_commit("UPDATE order_photos SET is_public = %s WHERE id = %s", (data.is_public, photo_id))
|
|
|
|
|
|
|
+
|
|
|
|
|
+ db.execute_commit("UPDATE order_photos SET is_public = %s, caption = %s WHERE id = %s", (data.is_public, data.caption, photo_id))
|
|
|
|
|
|
|
|
await audit_service.log(
|
|
await audit_service.log(
|
|
|
user_id=admin['id'],
|
|
user_id=admin['id'],
|
|
|
action="update_photo_visibility",
|
|
action="update_photo_visibility",
|
|
|
target_type="photo",
|
|
target_type="photo",
|
|
|
target_id=photo_id,
|
|
target_id=photo_id,
|
|
|
- details={"is_public": data.is_public}
|
|
|
|
|
|
|
+ details={"is_public": data.is_public, "caption": data.caption}
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- # NOTIFY USER VIA WEBSOCKET
|
|
|
|
|
|
|
+ # NOTIFY USER VIA WEBSOCKET if linked to order
|
|
|
order_id = photo_data[0]['order_id']
|
|
order_id = photo_data[0]['order_id']
|
|
|
- order_info = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
|
|
|
|
|
- if order_info:
|
|
|
|
|
- await global_manager.notify_order_update(order_info[0]['user_id'], order_id)
|
|
|
|
|
|
|
+ if order_id:
|
|
|
|
|
+ order_info = db.execute_query("SELECT user_id FROM orders WHERE id = %s", (order_id,))
|
|
|
|
|
+ if order_info:
|
|
|
|
|
+ await global_manager.notify_order_update(order_info[0]['user_id'], order_id)
|
|
|
|
|
|
|
|
- return {"id": photo_id, "is_public": data.is_public}
|
|
|
|
|
|
|
+ return {"id": photo_id, "is_public": data.is_public, "caption": data.caption}
|
|
|
|
|
|
|
|
@router.delete("/admin/photos/{photo_id}")
|
|
@router.delete("/admin/photos/{photo_id}")
|
|
|
async def admin_delete_photo(photo_id: int, admin: dict = Depends(require_admin)):
|
|
async def admin_delete_photo(photo_id: int, admin: dict = Depends(require_admin)):
|