Customer-facing review portal with intelligent funnel logic that routes happy customers to Google reviews and unhappy customers to internal support.
Date Created: October 22, 2025 Status: ✅ Complete and Ready for Production
Customer submits 4-5 star review
↓
Optional feedback text
↓
Mark google_review_requested = true
↓
Show Google review CTA
↓
Track Google link click
↓
Redirect to Google review page
↓
Send thank you email
Customer submits 1-3 star review
↓
Required detailed feedback
↓
Optional resolution request
↓
Create internal support ticket
↓
Mark google_review_requested = false
↓
Send "we'll make it right" email
↓
Management notified within 24 hours
File: src/app/(dashboard)/portal/review/[requestId]/page.tsx
URL: /portal/review/[requestId]
Features:
- Star Rating Selector: Interactive 1-5 star rating with hover effects
- Service Details Card: Shows service type, date, location
- Conditional UI: Changes based on selected rating
- Real-time Validation: Prevents submission without required fields
- Success State: Post-submission thank you with Google review option
- Responsive Design: Mobile-friendly layout
UI Flow:
Initial State:
┌─────────────────────────────────┐
│ How was your experience? │
│ │
│ ⭐ ⭐ ⭐ ⭐ ⭐ │
│ │
│ [Service Details Card] │
└─────────────────────────────────┘
After Selecting 5 Stars:
┌─────────────────────────────────┐
│ ⭐ ⭐ ⭐ ⭐ ⭐ Excellent! │
│ │
│ ✅ We're thrilled you're │
│ satisfied! │
│ │
│ [Optional feedback textarea] │
│ │
│ 💡 After submitting, you'll │
│ have the option to share │
│ on Google │
│ │
│ [Submit Review Button] │
└─────────────────────────────────┘
After Selecting 2 Stars:
┌─────────────────────────────────┐
│ ⭐ ⭐ ⚪ ⚪ ⚪ Below Expectations │
│ │
│ ⚠️ We're sorry we didn't │
│ meet expectations │
│ │
│ What went wrong? * │
│ [Required feedback textarea] │
│ │
│ What can we do to make this │
│ right? │
│ [Optional resolution input] │
│ │
│ 💡 We'll contact you within │
│ 24 hours │
│ │
│ [Submit Review Button] │
└─────────────────────────────────┘
After Submission (High Rating):
┌─────────────────────────────────┐
│ ✅ Thank You! │
│ │
│ We appreciate your feedback! │
│ │
│ ┌───────────────────────────┐ │
│ │ Love what we did? │ │
│ │ Share it with others! │ │
│ │ │ │
│ │ [Leave Google Review] │ │
│ │ │ │
│ │ Opens in new tab │ │
│ └───────────────────────────┘ │
│ │
│ [Back to Portal] │
└─────────────────────────────────┘
After Submission (Low Rating):
┌─────────────────────────────────┐
│ ✅ Thank You! │
│ │
│ We appreciate your feedback! │
│ │
│ ┌───────────────────────────┐ │
│ │ ⚠️ We're committed to │ │
│ │ making this right │ │
│ │ │ │
│ │ Our team will review your │ │
│ │ feedback and reach out │ │
│ │ within 24 hours. │ │
│ └───────────────────────────┘ │
│ │
│ [Back to Portal] │
└─────────────────────────────────┘Component State:
const [rating, setRating] = useState(0)
const [hoveredRating, setHoveredRating] = useState(0)
const [feedback, setFeedback] = useState('')
const [resolutionRequest, setResolutionRequest] = useState('')
const [submitted, setSubmitted] = useState(false)File: src/app/api/portal/reviews/[id]/route.ts
Endpoint: GET /api/portal/reviews/[id]
Purpose: Fetch specific review request for customer
Security:
- Verifies customer authentication
- Validates review request ownership
- Returns 403 if not owned by customer
Response:
{
"success": true,
"data": {
"reviewRequest": {
"id": "uuid",
"customer_id": "uuid",
"job_id": "uuid",
"portal_review_completed": false,
"status": "pending",
"jobs": {
"service_type": "Carpet Cleaning",
"completed_at": "2025-10-20T10:00:00Z",
"service_address": "123 Main St",
"total_amount": 150.00
}
}
}
}File: src/app/api/portal/reviews/[id]/submit/route.ts
Endpoint: POST /api/portal/reviews/[id]/submit
Request Body:
{
"rating": 5,
"feedback": "Excellent service! Very professional.",
"resolutionRequest": "" // Only for low ratings
}Validation:
const SubmitSchema = z.object({
rating: z.number().int().min(1).max(5),
feedback: z.string().max(2000).optional(),
resolutionRequest: z.string().max(500).optional(),
})Processing Logic:
For High Ratings (4-5 stars):
// 1. Update review request
UPDATE review_requests SET
portal_review_completed = true,
portal_review_rating = rating,
portal_review_text = feedback,
portal_review_submitted_at = NOW(),
google_review_requested = true // ← Key difference
WHERE id = reviewRequestId
// 2. Send thank you email with Google review option
// 3. Return success with googleReviewRequested = trueFor Low Ratings (1-3 stars):
// 1. Update review request
UPDATE review_requests SET
portal_review_completed = true,
portal_review_rating = rating,
portal_review_text = feedback,
portal_review_submitted_at = NOW(),
google_review_requested = false // ← No Google request
WHERE id = reviewRequestId
// 2. Create support ticket
INSERT INTO support_tickets (
customer_id,
job_id,
title: "Low Review Rating (${rating}/5)",
description: feedback,
category: "service_quality",
priority: rating === 1 ? "high" : "medium",
status: "open",
source: "review_system"
)
// 3. Send "we'll make it right" email
// 4. Return success with supportTicketCreated = trueResponse (High Rating):
{
"success": true,
"data": {
"message": "Review submitted successfully",
"googleReviewRequested": true
}
}Response (Low Rating):
{
"success": true,
"data": {
"message": "Review submitted successfully",
"googleReviewRequested": false,
"supportTicketCreated": true,
"supportTicketId": "uuid"
}
}File: src/app/api/portal/reviews/[id]/google-click/route.ts
Endpoint: POST /api/portal/reviews/[id]/google-click
Purpose: Track when customer clicks Google review link
Processing:
UPDATE review_requests SET
google_review_link_clicked = true,
google_review_clicked_at = NOW()
WHERE id = reviewRequestIdResponse:
{
"success": true,
"data": {
"message": "Google review click tracked successfully"
}
}Usage in Frontend:
const handleGoogleReviewClick = async () => {
// Track the click
await fetch(`/api/portal/reviews/${requestId}/google-click`, {
method: 'POST',
})
// Open Google review page
window.open(`/api/reviews/google/redirect/${requestId}`, '_blank')
}Title Format:
Low Review Rating (2/5) - Carpet Cleaning
Description Format:
Customer left a 2/5 star review with the following feedback:
The cleaning wasn't as thorough as expected. Some stains were still visible.
Resolution Request: Please re-clean the living room area.
Fields:
{
customer_id: "uuid",
job_id: "uuid",
title: "Low Review Rating (X/5) - Service Type",
description: "Feedback + Resolution Request",
category: "service_quality",
priority: rating === 1 ? "high" : "medium",
status: "open",
source: "review_system",
metadata: {
review_request_id: "uuid",
rating: 2,
feedback: "...",
resolution_request: "...",
auto_created: true
}
}Priority Logic:
- 1 star = high priority
- 2-3 stars = medium priority
Subject: "Thank you for your positive feedback! ⭐"
Content:
<!DOCTYPE html>
<html>
<head>
<title>Thank You!</title>
</head>
<body>
<div style="background-color: #14213d; color: white; padding: 30px;">
<h1>Thank You!</h1>
</div>
<div style="padding: 30px;">
<p>Hi [Customer Name],</p>
<p>Thank you for taking the time to share your positive feedback!
We're thrilled to hear that you're satisfied with our service.</p>
<div style="background-color: #eef2ff; padding: 20px; margin: 20px 0;">
<h3>Help Others Find Quality Service</h3>
<p>If you have a moment, we'd greatly appreciate if you could share
your experience on Google.</p>
<p style="text-align: center;">
<a href="[Google Review Link]"
style="background-color: #14213d; color: white; padding: 12px 24px;">
Leave a Google Review
</a>
</p>
</div>
<p>We appreciate your business!</p>
<p>Best regards,<br><strong>The Dirt Free Team</strong></p>
</div>
</body>
</html>Subject: "We're committed to making this right"
Content:
<!DOCTYPE html>
<html>
<head>
<title>We're Here to Help</title>
</head>
<body>
<div style="background-color: #14213d; color: white; padding: 30px;">
<h1>We're Committed to Making This Right</h1>
</div>
<div style="padding: 30px;">
<p>Hi [Customer Name],</p>
<p>Thank you for sharing your honest feedback. We're sorry that your
experience didn't meet your expectations.</p>
<p><strong>We take your feedback seriously.</strong> Our team has been
notified and will reach out to you within 24 hours.</p>
<div style="background-color: #fffbeb; border-left: 4px solid #fbbf24; padding: 15px;">
<p><strong>What happens next:</strong></p>
<ul>
<li>A member of our management team will contact you</li>
<li>We'll listen to your concerns and work on a solution</li>
<li>We'll take action to prevent similar issues</li>
</ul>
</div>
<p>We value your business and the opportunity to earn back your trust.</p>
<p>Best regards,<br><strong>The Dirt Free Team</strong></p>
</div>
</body>
</html>-
Receives Email:
- "We'd love your feedback! ⭐"
- Contains link to
/portal/review/[requestId]
-
Opens Review Page:
- Sees service details
- Selects 5 stars
- UI shows positive message
- Optional feedback textarea appears
-
Submits Review:
- Clicks "Submit Review"
- Success message appears
- Google review CTA shown
-
Clicks Google Review:
- Click is tracked (google_review_link_clicked = true)
- Opens Google review page in new tab
- Can leave public review
-
Receives Thank You Email:
- "Thank you for your positive feedback! ⭐"
- Includes another Google review link
-
Receives Email:
- "We'd love your feedback! ⭐"
- Contains link to
/portal/review/[requestId]
-
Opens Review Page:
- Sees service details
- Selects 2 stars
- UI shows apologetic message
- Required feedback textarea appears
- Optional resolution request appears
-
Provides Feedback:
- Types: "Service wasn't thorough, stains still visible"
- Resolution: "Please re-clean living room"
-
Submits Review:
- Clicks "Submit Review"
- Success message appears
- No Google review option (stays internal)
-
Support Ticket Created:
- Title: "Low Review Rating (2/5) - Carpet Cleaning"
- Priority: Medium
- Assigned to management team
-
Receives Email:
- "We're committed to making this right"
- Explains 24-hour response timeline
-
Management Follow-up:
- Receives call within 24 hours
- Issue is discussed
- Resolution is implemented
- Follow-up review request (optional)
After High Rating Submission:
UPDATE review_requests SET
portal_review_completed = true,
portal_review_rating = 5,
portal_review_text = "Great service!",
portal_review_submitted_at = '2025-10-22 14:30:00',
google_review_requested = true -- Eligible for Google
WHERE id = 'review-uuid'After Low Rating Submission:
UPDATE review_requests SET
portal_review_completed = true,
portal_review_rating = 2,
portal_review_text = "Service wasn't thorough",
portal_review_submitted_at = '2025-10-22 14:30:00',
google_review_requested = false -- Keep internal
WHERE id = 'review-uuid'After Google Click:
UPDATE review_requests SET
google_review_link_clicked = true,
google_review_clicked_at = '2025-10-22 14:32:00'
WHERE id = 'review-uuid'-
Submission Metrics:
- Total reviews submitted
- Reviews by rating (1-5 distribution)
- Average rating
- Completion rate
-
Google Review Metrics:
- Google reviews requested (4-5 stars)
- Google review link clicks
- Click-through rate
- Estimated Google reviews completed
-
Support Ticket Metrics:
- Low ratings (1-3 stars)
- Support tickets created
- Ticket resolution time
- Customer satisfaction after resolution
-
Email Metrics:
- Thank you emails sent
- Resolution emails sent
- Email open rates (if tracking enabled)
-- Review funnel by rating
SELECT
portal_review_rating,
COUNT(*) as total_reviews,
COUNT(*) FILTER (WHERE google_review_requested) as google_requested,
COUNT(*) FILTER (WHERE google_review_link_clicked) as google_clicked,
ROUND(
COUNT(*) FILTER (WHERE google_review_link_clicked)::numeric /
NULLIF(COUNT(*) FILTER (WHERE google_review_requested), 0) * 100,
2
) as click_through_rate
FROM review_requests
WHERE portal_review_completed = true
GROUP BY portal_review_rating
ORDER BY portal_review_rating DESCExpected Output:
rating | total | requested | clicked | ctr
-------|-------|-----------|---------|-------
5 | 45 | 45 | 32 | 71.11%
4 | 30 | 30 | 18 | 60.00%
3 | 10 | 0 | 0 | 0.00%
2 | 8 | 0 | 0 | 0.00%
1 | 7 | 0 | 0 | 0.00%
- Page loads with review request details
- Star rating is interactive (hover + click)
- Rating selection updates UI appropriately
- High rating (4-5) shows optional feedback
- Low rating (1-3) requires feedback
- Low rating shows resolution request field
- Submit button disabled until valid
- Already-submitted reviews show completion message
- Success state displays correctly
- Google review button works (high ratings)
- Mobile responsive layout
- GET request returns correct review data
- GET request enforces customer ownership
- Submit validates rating range (1-5)
- Submit requires feedback for low ratings
- High rating creates Google review request
- Low rating creates support ticket
- Thank you email sent after submission
- Google click tracking works
- Duplicate submissions prevented
- Ticket created for 1-3 star ratings
- Ticket has correct priority (1 star = high, 2-3 = medium)
- Ticket includes all feedback
- Ticket metadata includes review_request_id
- Ticket assigned to correct team
- Management notified of new tickets
- High rating email includes Google review link
- Low rating email explains resolution process
- Emails are mobile-responsive
- Links in emails work correctly
- Unsubscribe link included (if required)
-
Authentication:
- All endpoints require customer authentication
- Review requests verified against customer ownership
-
Authorization:
- Customer can only access their own review requests
- Returns 403 if attempting to access others' reviews
-
Input Validation:
- Zod schema validation on all inputs
- Rating range enforced (1-5)
- Text length limits (feedback: 2000, resolution: 500)
-
Rate Limiting:
- Consider implementing rate limits on submission endpoint
- Prevent spam submissions
-
CSRF Protection:
- Next.js built-in CSRF protection
- API routes use POST with JSON body
-
Database Indexes:
CREATE INDEX idx_review_requests_customer ON review_requests(customer_id); CREATE INDEX idx_review_requests_portal_rating ON review_requests(portal_review_rating) WHERE portal_review_rating IS NOT NULL;
-
Caching:
- Review request data can be cached for 5 minutes
- Clear cache on submission
-
Email Queue:
- Thank you emails sent asynchronously
- Don't block submission response
-
Support Ticket Creation:
- Create ticket asynchronously if slow
- Log but don't fail if ticket creation fails
- After issue resolution, send follow-up review request
- "Have we made things right?" second chance
- Offer discount code for completing review
- Loyalty points for feedback
- Split into multiple pages
- Rating → Feedback → Google (for high) or Resolution (for low)
- Allow customers to upload photos with feedback
- Especially useful for low ratings (evidence)
- Send review link via SMS
- Shorter, mobile-optimized form
- Share positive reviews on social media
- Facebook, Twitter, Instagram options
- Pre-written responses for staff
- Quick replies to common feedback
✅ Complete Smart Review Portal Interface
Files Created: 4
src/app/(dashboard)/portal/review/[requestId]/page.tsx- Review submission UIsrc/app/api/portal/reviews/[id]/route.ts- Get review requestsrc/app/api/portal/reviews/[id]/submit/route.ts- Submit review with smart logicsrc/app/api/portal/reviews/[id]/google-click/route.ts- Track Google clicks
Key Features:
- ✅ Interactive 1-5 star rating
- ✅ Conditional UI based on rating
- ✅ Smart funnel (4-5 → Google, 1-3 → Support)
- ✅ Support ticket auto-creation for low ratings
- ✅ Google review click tracking
- ✅ Professional email templates
- ✅ Mobile-responsive design
- ✅ Complete error handling
- ✅ Customer ownership verification
Smart Routing:
- High (4-5 stars): Optional feedback → Google review → Public visibility
- Low (1-3 stars): Required feedback → Support ticket → Private resolution
Production Ready: ✅ Yes
- TypeScript validation complete (except pre-existing toast hook issue)
- Security: Customer ownership verified
- Validation: Zod schemas on all inputs
- Error handling: Comprehensive try-catch blocks
- Email delivery: Async, non-blocking
- Support integration: Auto-ticket creation
The smart review portal interface is complete and ready to filter positive reviews to Google while handling negative reviews internally! 🎯