updates
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -17,11 +17,12 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|||||||
# Consider moving to nonces/hashes in future for stricter policy
|
# Consider moving to nonces/hashes in future for stricter policy
|
||||||
security_headers['Content-Security-Policy'] = (
|
security_headers['Content-Security-Policy'] = (
|
||||||
"default-src 'self'; "
|
"default-src 'self'; "
|
||||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com; "
|
||||||
"style-src 'self' 'unsafe-inline'; "
|
"style-src 'self' 'unsafe-inline'; "
|
||||||
"img-src 'self' data: https:; "
|
"img-src 'self' data: https:; "
|
||||||
"font-src 'self' data:; "
|
"font-src 'self' data:; "
|
||||||
"connect-src 'self' https:; "
|
"connect-src 'self' https: https://js.stripe.com https://hooks.stripe.com; "
|
||||||
|
"frame-src 'self' https://js.stripe.com https://hooks.stripe.com; "
|
||||||
"base-uri 'self'; "
|
"base-uri 'self'; "
|
||||||
"form-action 'self'; "
|
"form-action 'self'; "
|
||||||
"frame-ancestors 'none'; "
|
"frame-ancestors 'none'; "
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,7 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||||
from ..utils.response_helpers import success_response
|
from ..utils.response_helpers import success_response, error_response
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session, joinedload
|
||||||
|
from sqlalchemy import func
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..config.logging_config import get_logger
|
from ..config.logging_config import get_logger
|
||||||
@@ -21,20 +22,49 @@ async def get_room_reviews(
|
|||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
query = db.query(Review).filter(Review.room_id == room_id, Review.status == ReviewStatus.approved)
|
# Calculate average rating and total reviews for approved reviews
|
||||||
|
stats = db.query(
|
||||||
|
func.avg(Review.rating).label('average_rating'),
|
||||||
|
func.count(Review.id).label('total_reviews')
|
||||||
|
).filter(
|
||||||
|
Review.room_id == room_id,
|
||||||
|
Review.status == ReviewStatus.approved
|
||||||
|
).first()
|
||||||
|
|
||||||
|
average_rating = round(float(stats.average_rating or 0), 1) if stats and stats.average_rating else 0
|
||||||
|
total_reviews = stats.total_reviews or 0 if stats else 0
|
||||||
|
|
||||||
|
query = db.query(Review).options(
|
||||||
|
joinedload(Review.user)
|
||||||
|
).filter(Review.room_id == room_id, Review.status == ReviewStatus.approved)
|
||||||
total = query.count()
|
total = query.count()
|
||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
reviews = query.order_by(Review.created_at.desc()).offset(offset).limit(limit).all()
|
reviews = query.order_by(Review.created_at.desc()).offset(offset).limit(limit).all()
|
||||||
result = []
|
result = []
|
||||||
for review in reviews:
|
for review in reviews:
|
||||||
review_dict = {'id': review.id, 'user_id': review.user_id, 'room_id': review.room_id, 'rating': review.rating, 'comment': review.comment, 'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status, 'created_at': review.created_at.isoformat() if review.created_at else None}
|
review_dict = {
|
||||||
|
'id': review.id,
|
||||||
|
'user_id': review.user_id,
|
||||||
|
'room_id': review.room_id,
|
||||||
|
'rating': review.rating,
|
||||||
|
'comment': review.comment,
|
||||||
|
'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status,
|
||||||
|
'created_at': review.created_at.isoformat() if review.created_at else None
|
||||||
|
}
|
||||||
if review.user:
|
if review.user:
|
||||||
review_dict['user'] = {'id': review.user.id, 'full_name': review.user.full_name, 'email': review.user.email}
|
review_dict['user'] = {
|
||||||
|
'id': review.user.id,
|
||||||
|
'full_name': review.user.full_name,
|
||||||
|
'name': review.user.full_name, # Add name alias for compatibility
|
||||||
|
'email': review.user.email
|
||||||
|
}
|
||||||
result.append(review_dict)
|
result.append(review_dict)
|
||||||
return {
|
return {
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'data': {
|
'data': {
|
||||||
'reviews': result,
|
'reviews': result,
|
||||||
|
'average_rating': average_rating,
|
||||||
|
'total_reviews': total_reviews,
|
||||||
'pagination': {
|
'pagination': {
|
||||||
'total': total,
|
'total': total,
|
||||||
'page': page,
|
'page': page,
|
||||||
@@ -51,7 +81,10 @@ async def get_room_reviews(
|
|||||||
@router.get('/', dependencies=[Depends(authorize_roles('admin'))])
|
@router.get('/', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
async def get_all_reviews(status_filter: Optional[str]=Query(None, alias='status'), page: int=Query(1, ge=1), limit: int=Query(10, ge=1, le=100), current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
async def get_all_reviews(status_filter: Optional[str]=Query(None, alias='status'), page: int=Query(1, ge=1), limit: int=Query(10, ge=1, le=100), current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||||
try:
|
try:
|
||||||
query = db.query(Review)
|
query = db.query(Review).options(
|
||||||
|
joinedload(Review.user),
|
||||||
|
joinedload(Review.room).joinedload(Room.room_type)
|
||||||
|
)
|
||||||
if status_filter:
|
if status_filter:
|
||||||
try:
|
try:
|
||||||
query = query.filter(Review.status == ReviewStatus(status_filter))
|
query = query.filter(Review.status == ReviewStatus(status_filter))
|
||||||
@@ -64,9 +97,25 @@ async def get_all_reviews(status_filter: Optional[str]=Query(None, alias='status
|
|||||||
for review in reviews:
|
for review in reviews:
|
||||||
review_dict = {'id': review.id, 'user_id': review.user_id, 'room_id': review.room_id, 'rating': review.rating, 'comment': review.comment, 'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status, 'created_at': review.created_at.isoformat() if review.created_at else None}
|
review_dict = {'id': review.id, 'user_id': review.user_id, 'room_id': review.room_id, 'rating': review.rating, 'comment': review.comment, 'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status, 'created_at': review.created_at.isoformat() if review.created_at else None}
|
||||||
if review.user:
|
if review.user:
|
||||||
review_dict['user'] = {'id': review.user.id, 'full_name': review.user.full_name, 'email': review.user.email, 'phone': review.user.phone}
|
review_dict['user'] = {
|
||||||
|
'id': review.user.id,
|
||||||
|
'full_name': review.user.full_name,
|
||||||
|
'name': review.user.full_name, # Add name alias for frontend compatibility
|
||||||
|
'email': review.user.email,
|
||||||
|
'phone': review.user.phone
|
||||||
|
}
|
||||||
if review.room:
|
if review.room:
|
||||||
review_dict['room'] = {'id': review.room.id, 'room_number': review.room.room_number}
|
room_dict = {
|
||||||
|
'id': review.room.id,
|
||||||
|
'room_number': review.room.room_number
|
||||||
|
}
|
||||||
|
# Include room type if available
|
||||||
|
if review.room.room_type:
|
||||||
|
room_dict['room_type'] = {
|
||||||
|
'id': review.room.room_type.id,
|
||||||
|
'name': review.room.room_type.name
|
||||||
|
}
|
||||||
|
review_dict['room'] = room_dict
|
||||||
result.append(review_dict)
|
result.append(review_dict)
|
||||||
return {'status': 'success', 'data': {'reviews': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
|
return {'status': 'success', 'data': {'reviews': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -79,58 +128,131 @@ async def create_review(review_data: CreateReviewRequest, current_user: User=Dep
|
|||||||
try:
|
try:
|
||||||
room_id = review_data.room_id
|
room_id = review_data.room_id
|
||||||
rating = review_data.rating
|
rating = review_data.rating
|
||||||
comment = review_data.comment
|
# Ensure comment is a string (not None) since the model requires it
|
||||||
|
comment = review_data.comment if review_data.comment else ''
|
||||||
|
|
||||||
room = db.query(Room).filter(Room.id == room_id).first()
|
room = db.query(Room).filter(Room.id == room_id).first()
|
||||||
if not room:
|
if not room:
|
||||||
raise HTTPException(status_code=404, detail='Room not found')
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=error_response(message='Room not found')
|
||||||
|
)
|
||||||
|
|
||||||
existing = db.query(Review).filter(Review.user_id == current_user.id, Review.room_id == room_id).first()
|
existing = db.query(Review).filter(Review.user_id == current_user.id, Review.room_id == room_id).first()
|
||||||
if existing:
|
if existing:
|
||||||
raise HTTPException(status_code=400, detail='You have already reviewed this room')
|
raise HTTPException(
|
||||||
review = Review(user_id=current_user.id, room_id=room_id, rating=rating, comment=comment, status=ReviewStatus.pending)
|
status_code=400,
|
||||||
|
detail=error_response(message='You have already reviewed this room')
|
||||||
|
)
|
||||||
|
|
||||||
|
review = Review(
|
||||||
|
user_id=current_user.id,
|
||||||
|
room_id=room_id,
|
||||||
|
rating=rating,
|
||||||
|
comment=comment or '', # Ensure comment is a string
|
||||||
|
status=ReviewStatus.pending
|
||||||
|
)
|
||||||
db.add(review)
|
db.add(review)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(review)
|
db.refresh(review)
|
||||||
return {'status': 'success', 'message': 'Review submitted successfully and is pending approval', 'data': {'review': review}}
|
|
||||||
|
review_dict = {
|
||||||
|
'id': review.id,
|
||||||
|
'user_id': review.user_id,
|
||||||
|
'room_id': review.room_id,
|
||||||
|
'rating': review.rating,
|
||||||
|
'comment': review.comment,
|
||||||
|
'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status,
|
||||||
|
'created_at': review.created_at.isoformat() if review.created_at else None
|
||||||
|
}
|
||||||
|
|
||||||
|
return success_response(
|
||||||
|
data={'review': review_dict},
|
||||||
|
message='Review submitted successfully and is pending approval'
|
||||||
|
)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logger.error(f'Error creating review: {str(e)}', exc_info=True)
|
logger.error(f'Error creating review: {str(e)}', exc_info=True)
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=error_response(message='An error occurred while creating the review')
|
||||||
|
)
|
||||||
|
|
||||||
@router.put('/{id}/approve', dependencies=[Depends(authorize_roles('admin'))])
|
@router.patch('/{id}/approve', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
async def approve_review(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
async def approve_review(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||||
try:
|
try:
|
||||||
review = db.query(Review).filter(Review.id == id).first()
|
review = db.query(Review).filter(Review.id == id).first()
|
||||||
if not review:
|
if not review:
|
||||||
raise HTTPException(status_code=404, detail='Review not found')
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=error_response(message='Review not found')
|
||||||
|
)
|
||||||
review.status = ReviewStatus.approved
|
review.status = ReviewStatus.approved
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(review)
|
db.refresh(review)
|
||||||
return {'status': 'success', 'message': 'Review approved successfully', 'data': {'review': review}}
|
|
||||||
|
review_dict = {
|
||||||
|
'id': review.id,
|
||||||
|
'user_id': review.user_id,
|
||||||
|
'room_id': review.room_id,
|
||||||
|
'rating': review.rating,
|
||||||
|
'comment': review.comment,
|
||||||
|
'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status,
|
||||||
|
'created_at': review.created_at.isoformat() if review.created_at else None
|
||||||
|
}
|
||||||
|
|
||||||
|
return success_response(
|
||||||
|
data={'review': review_dict},
|
||||||
|
message='Review approved successfully'
|
||||||
|
)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logger.error(f'Error approving review: {str(e)}', exc_info=True)
|
logger.error(f'Error approving review: {str(e)}', exc_info=True)
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=error_response(message='An error occurred while approving the review')
|
||||||
|
)
|
||||||
|
|
||||||
@router.put('/{id}/reject', dependencies=[Depends(authorize_roles('admin'))])
|
@router.patch('/{id}/reject', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
async def reject_review(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
async def reject_review(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||||
try:
|
try:
|
||||||
review = db.query(Review).filter(Review.id == id).first()
|
review = db.query(Review).filter(Review.id == id).first()
|
||||||
if not review:
|
if not review:
|
||||||
raise HTTPException(status_code=404, detail='Review not found')
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=error_response(message='Review not found')
|
||||||
|
)
|
||||||
review.status = ReviewStatus.rejected
|
review.status = ReviewStatus.rejected
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(review)
|
db.refresh(review)
|
||||||
return {'status': 'success', 'message': 'Review rejected successfully', 'data': {'review': review}}
|
|
||||||
|
review_dict = {
|
||||||
|
'id': review.id,
|
||||||
|
'user_id': review.user_id,
|
||||||
|
'room_id': review.room_id,
|
||||||
|
'rating': review.rating,
|
||||||
|
'comment': review.comment,
|
||||||
|
'status': review.status.value if isinstance(review.status, ReviewStatus) else review.status,
|
||||||
|
'created_at': review.created_at.isoformat() if review.created_at else None
|
||||||
|
}
|
||||||
|
|
||||||
|
return success_response(
|
||||||
|
data={'review': review_dict},
|
||||||
|
message='Review rejected successfully'
|
||||||
|
)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logger.error(f'Error rejecting review: {str(e)}', exc_info=True)
|
logger.error(f'Error rejecting review: {str(e)}', exc_info=True)
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=error_response(message='An error occurred while rejecting the review')
|
||||||
|
)
|
||||||
|
|
||||||
@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
async def delete_review(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
async def delete_review(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||||
|
|||||||
Binary file not shown.
@@ -9,7 +9,7 @@ class CreateReviewRequest(BaseModel):
|
|||||||
"""Schema for creating a review."""
|
"""Schema for creating a review."""
|
||||||
room_id: int = Field(..., gt=0, description="Room ID")
|
room_id: int = Field(..., gt=0, description="Room ID")
|
||||||
rating: int = Field(..., ge=1, le=5, description="Rating from 1 to 5")
|
rating: int = Field(..., ge=1, le=5, description="Rating from 1 to 5")
|
||||||
comment: Optional[str] = Field(None, max_length=2000, description="Review comment")
|
comment: str = Field(..., min_length=1, max_length=2000, description="Review comment")
|
||||||
|
|
||||||
model_config = {
|
model_config = {
|
||||||
"json_schema_extra": {
|
"json_schema_extra": {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<!-- Content Security Policy - Additional layer of XSS protection -->
|
<!-- Content Security Policy - Additional layer of XSS protection -->
|
||||||
<!-- Allows HTTP localhost connections for development, HTTPS for production -->
|
<!-- Allows HTTP localhost connections for development, HTTPS for production -->
|
||||||
<!-- Note: Backend CSP headers (production only) will override/merge with this meta tag -->
|
<!-- Note: Backend CSP headers (production only) will override/merge with this meta tag -->
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https: http: blob:; connect-src 'self' https: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* wss:; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self';" />
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://js.stripe.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https: http: blob:; connect-src 'self' https: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* wss: https://js.stripe.com https://hooks.stripe.com; frame-src 'self' https://js.stripe.com https://hooks.stripe.com; object-src 'none'; base-uri 'self'; form-action 'self';" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700;800;900&family=Cormorant+Garamond:wght@300;400;500;600;700&family=Cinzel:wght@400;500;600;700&family=Poppins:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700;800;900&family=Cormorant+Garamond:wght@300;400;500;600;700&family=Cinzel:wght@400;500;600;700&family=Poppins:wght@300;400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet" />
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ const RatePlanManagementPage = lazy(() => import('./pages/admin/RatePlanManageme
|
|||||||
const PackageManagementPage = lazy(() => import('./pages/admin/PackageManagementPage'));
|
const PackageManagementPage = lazy(() => import('./pages/admin/PackageManagementPage'));
|
||||||
const SecurityManagementPage = lazy(() => import('./pages/admin/SecurityManagementPage'));
|
const SecurityManagementPage = lazy(() => import('./pages/admin/SecurityManagementPage'));
|
||||||
const EmailCampaignManagementPage = lazy(() => import('./pages/admin/EmailCampaignManagementPage'));
|
const EmailCampaignManagementPage = lazy(() => import('./pages/admin/EmailCampaignManagementPage'));
|
||||||
|
const ReviewManagementPage = lazy(() => import('./pages/admin/ReviewManagementPage'));
|
||||||
|
|
||||||
const StaffDashboardPage = lazy(() => import('./pages/staff/DashboardPage'));
|
const StaffDashboardPage = lazy(() => import('./pages/staff/DashboardPage'));
|
||||||
const StaffBookingManagementPage = lazy(() => import('./pages/staff/BookingManagementPage'));
|
const StaffBookingManagementPage = lazy(() => import('./pages/staff/BookingManagementPage'));
|
||||||
@@ -464,6 +465,10 @@ function App() {
|
|||||||
path="email-campaigns"
|
path="email-campaigns"
|
||||||
element={<EmailCampaignManagementPage />}
|
element={<EmailCampaignManagementPage />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="reviews"
|
||||||
|
element={<ReviewManagementPage />}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{}
|
{}
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import {
|
|||||||
Mail,
|
Mail,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
Building2,
|
Building2,
|
||||||
Crown
|
Crown,
|
||||||
|
Star
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import useAuthStore from '../../store/useAuthStore';
|
import useAuthStore from '../../store/useAuthStore';
|
||||||
import { useResponsive } from '../../hooks';
|
import { useResponsive } from '../../hooks';
|
||||||
@@ -212,6 +213,11 @@ const SidebarAdmin: React.FC<SidebarAdminProps> = ({
|
|||||||
icon: Globe,
|
icon: Globe,
|
||||||
label: 'Page Content'
|
label: 'Page Content'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/reviews',
|
||||||
|
icon: Star,
|
||||||
|
label: 'Reviews'
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -74,9 +74,22 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await getRoomReviews(roomId);
|
const response = await getRoomReviews(roomId);
|
||||||
if (response.status === 'success' && response.data) {
|
if (response.status === 'success' && response.data) {
|
||||||
setReviews(response.data.reviews || []);
|
const reviewsList = response.data.reviews || [];
|
||||||
setAverageRating(response.data.average_rating || 0);
|
setReviews(reviewsList);
|
||||||
setTotalReviews(response.data.total_reviews || 0);
|
// Use backend values, but fallback to calculated values if backend doesn't provide them
|
||||||
|
const backendTotal = response.data.total_reviews || 0;
|
||||||
|
const backendAverage = response.data.average_rating || 0;
|
||||||
|
|
||||||
|
// If backend doesn't provide totals but we have reviews, calculate them
|
||||||
|
if (backendTotal === 0 && reviewsList.length > 0) {
|
||||||
|
const calculatedTotal = reviewsList.length;
|
||||||
|
const calculatedAverage = reviewsList.reduce((sum, r) => sum + r.rating, 0) / calculatedTotal;
|
||||||
|
setTotalReviews(calculatedTotal);
|
||||||
|
setAverageRating(calculatedAverage);
|
||||||
|
} else {
|
||||||
|
setTotalReviews(backendTotal);
|
||||||
|
setAverageRating(backendAverage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching reviews:', error);
|
console.error('Error fetching reviews:', error);
|
||||||
@@ -153,9 +166,11 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-2xl sm:text-3xl font-serif font-bold bg-gradient-to-r from-[#d4af37] to-[#f5d76e] bg-clip-text text-transparent">
|
<div className="text-2xl sm:text-3xl font-serif font-bold bg-gradient-to-r from-[#d4af37] to-[#f5d76e] bg-clip-text text-transparent">
|
||||||
{averageRating > 0
|
{totalReviews > 0 && averageRating > 0
|
||||||
? averageRating.toFixed(1)
|
? averageRating.toFixed(1)
|
||||||
: 'N/A'}
|
: totalReviews === 0
|
||||||
|
? '—'
|
||||||
|
: averageRating.toFixed(1)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<RatingStars
|
<RatingStars
|
||||||
@@ -164,7 +179,9 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] sm:text-xs text-gray-400 mt-1.5 font-light">
|
<div className="text-[10px] sm:text-xs text-gray-400 mt-1.5 font-light">
|
||||||
{totalReviews} review{totalReviews !== 1 ? 's' : ''}
|
{totalReviews > 0
|
||||||
|
? `${totalReviews} review${totalReviews !== 1 ? 's' : ''}`
|
||||||
|
: 'No reviews yet'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -274,7 +291,7 @@ const ReviewSection: React.FC<ReviewSectionProps> = ({
|
|||||||
{}
|
{}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-xs sm:text-sm font-serif font-semibold text-white mb-3 tracking-wide">
|
<h4 className="text-xs sm:text-sm font-serif font-semibold text-white mb-3 tracking-wide">
|
||||||
All Reviews ({totalReviews})
|
All Reviews ({reviews.length > 0 ? reviews.length : totalReviews})
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|||||||
@@ -146,7 +146,12 @@ const ReviewManagementPage: React.FC = () => {
|
|||||||
{reviews.map((review) => (
|
{reviews.map((review) => (
|
||||||
<tr key={review.id} className="hover:bg-gray-50">
|
<tr key={review.id} className="hover:bg-gray-50">
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm font-medium text-gray-900">{review.user?.name}</div>
|
<div className="text-sm font-medium text-gray-900">
|
||||||
|
{review.user?.full_name || review.user?.name || 'Unknown User'}
|
||||||
|
</div>
|
||||||
|
{review.user?.email && (
|
||||||
|
<div className="text-xs text-gray-500">{review.user.email}</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm text-gray-900">
|
<div className="text-sm text-gray-900">
|
||||||
|
|||||||
Reference in New Issue
Block a user