Reply directly to this email to respond to the user.
"""
```
**Step 3: Commit**
```bash
git add backend/app/core/email.py
git commit -m "feat: add send_feedback_email to EmailService"
```
---
## Task 4: Backend Endpoint
**Files:**
- Create: `backend/app/api/endpoints/feedback.py`
- Modify: `backend/app/api/router.py`
**Step 1: Create the endpoint**
Create `backend/app/api/endpoints/feedback.py`:
```python
import logging
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.api.deps import get_current_active_user
from app.core.config import settings
from app.core.database import get_db
from app.core.email import EmailService
from app.core.rate_limit import limiter
from app.models.user import User
from app.models.account import Account
from app.schemas.feedback import FeedbackSubmission, FeedbackResponse
logger = logging.getLogger(__name__)
router = APIRouter(tags=["feedback"])
@router.post("/feedback", response_model=FeedbackResponse)
@limiter.limit("1/minute")
async def submit_feedback(
request: Request,
data: FeedbackSubmission,
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
):
"""Submit user feedback via email."""
if not settings.FEEDBACK_EMAIL:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Feedback submission is not configured",
)
# Get account info for the email
account_name = None
account_code = None
if current_user.account_id:
result = await db.execute(
select(Account).where(Account.id == current_user.account_id)
)
account = result.scalar_one_or_none()
if account:
account_name = account.name
account_code = account.display_code
sent = await EmailService.send_feedback_email(
to_email=settings.FEEDBACK_EMAIL,
reply_to_email=data.email,
feedback_type=data.feedback_type.value,
message=data.message,
user_email=current_user.email,
account_name=account_name,
account_code=account_code,
)
if not sent:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to send feedback. Please try again later.",
)
return FeedbackResponse(success=True, message="Thank you! Your feedback has been submitted.")
```
**Step 2: Register the router**
In `backend/app/api/router.py`, add the import and include:
Add to imports (line 6, after the existing imports):
```python
from app.api.endpoints import feedback
```
Add at the end of the router registrations:
```python
api_router.include_router(feedback.router)
```
**Step 3: Commit**
```bash
git add backend/app/api/endpoints/feedback.py backend/app/api/router.py
git commit -m "feat: add POST /feedback endpoint"
```
---
## Task 5: Backend Test
**Files:**
- Create: `backend/tests/test_feedback.py`
**Step 1: Write the test**
```python
import pytest
from unittest.mock import patch, AsyncMock
@pytest.mark.asyncio
async def test_submit_feedback(async_client, engineer_token, monkeypatch):
"""Test successful feedback submission."""
monkeypatch.setenv("FEEDBACK_EMAIL", "support@test.com")
# Reload settings to pick up the env var
from app.core.config import Settings
test_settings = Settings()
with patch("app.api.endpoints.feedback.settings") as mock_settings, \
patch("app.api.endpoints.feedback.EmailService") as mock_email:
mock_settings.FEEDBACK_EMAIL = "support@test.com"
mock_email.send_feedback_email = AsyncMock(return_value=True)
response = await async_client.post(
"/api/v1/feedback",
json={
"email": "engineer@resolutionflow.example.com",
"feedback_type": "Bug Report",
"message": "Something is broken in the tree editor when I try to save.",
},
headers={"Authorization": f"Bearer {engineer_token}"},
)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert "submitted" in data["message"].lower()
@pytest.mark.asyncio
async def test_submit_feedback_not_configured(async_client, engineer_token):
"""Test 503 when FEEDBACK_EMAIL is not set."""
with patch("app.api.endpoints.feedback.settings") as mock_settings:
mock_settings.FEEDBACK_EMAIL = None
response = await async_client.post(
"/api/v1/feedback",
json={
"email": "engineer@resolutionflow.example.com",
"feedback_type": "General Feedback",
"message": "This is a general feedback message for testing.",
},
headers={"Authorization": f"Bearer {engineer_token}"},
)
assert response.status_code == 503
@pytest.mark.asyncio
async def test_submit_feedback_validation(async_client, engineer_token):
"""Test validation — message too short."""
with patch("app.api.endpoints.feedback.settings") as mock_settings:
mock_settings.FEEDBACK_EMAIL = "support@test.com"
response = await async_client.post(
"/api/v1/feedback",
json={
"email": "engineer@resolutionflow.example.com",
"feedback_type": "Bug Report",
"message": "short",
},
headers={"Authorization": f"Bearer {engineer_token}"},
)
assert response.status_code == 422
@pytest.mark.asyncio
async def test_submit_feedback_invalid_type(async_client, engineer_token):
"""Test validation — invalid feedback type."""
with patch("app.api.endpoints.feedback.settings") as mock_settings:
mock_settings.FEEDBACK_EMAIL = "support@test.com"
response = await async_client.post(
"/api/v1/feedback",
json={
"email": "engineer@resolutionflow.example.com",
"feedback_type": "Invalid Type",
"message": "This should fail because the type is invalid.",
},
headers={"Authorization": f"Bearer {engineer_token}"},
)
assert response.status_code == 422
@pytest.mark.asyncio
async def test_submit_feedback_requires_auth(async_client):
"""Test that unauthenticated requests are rejected."""
response = await async_client.post(
"/api/v1/feedback",
json={
"email": "anon@example.com",
"feedback_type": "General Feedback",
"message": "This should fail because I'm not logged in.",
},
)
assert response.status_code == 401
```
**Step 2: Run the tests**
```bash
cd backend && pytest tests/test_feedback.py -v --override-ini="addopts="
```
Expected: All 5 tests pass. If any fail, debug and fix before proceeding.
**Step 3: Commit**
```bash
git add backend/tests/test_feedback.py
git commit -m "test: add feedback endpoint tests"
```
---
## Task 6: Frontend API Client
**Files:**
- Create: `frontend/src/api/feedback.ts`
- Modify: `frontend/src/api/index.ts`
**Step 1: Create the API module**
Create `frontend/src/api/feedback.ts`:
```typescript
import { apiClient } from './client'
export interface FeedbackSubmission {
email: string
feedback_type: string
message: string
}
export interface FeedbackResponse {
success: boolean
message: string
}
export const feedbackApi = {
submit: async (data: FeedbackSubmission): Promise => {
const { data: response } = await apiClient.post('/feedback', data)
return response
},
}
export default feedbackApi
```
**Step 2: Export from index**
In `frontend/src/api/index.ts`, add at the end:
```typescript
export { default as feedbackApi } from './feedback'
```
**Step 3: Commit**
```bash
git add frontend/src/api/feedback.ts frontend/src/api/index.ts
git commit -m "feat: add feedback API client"
```
---
## Task 7: Frontend Page
**Files:**
- Create: `frontend/src/pages/FeedbackPage.tsx`
**Step 1: Create the page component**
```tsx
import { useState } from 'react'
import { MessageSquareText, Send, CheckCircle2 } from 'lucide-react'
import { useAuthStore } from '@/store/authStore'
import { feedbackApi } from '@/api'
import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'
const FEEDBACK_TYPES = [
'Bug Report',
'Feature Request',
'Usability Issue',
'Documentation',
'General Feedback',
] as const
export function FeedbackPage() {
const user = useAuthStore(s => s.user)
const [email, setEmail] = useState(user?.email ?? '')
const [feedbackType, setFeedbackType] = useState('')
const [message, setMessage] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [submitted, setSubmitted] = useState(false)
const canSubmit = email.trim() && feedbackType && message.trim().length >= 10
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!canSubmit || isSubmitting) return
setIsSubmitting(true)
try {
const response = await feedbackApi.submit({
email: email.trim(),
feedback_type: feedbackType,
message: message.trim(),
})
if (response.success) {
setSubmitted(true)
setFeedbackType('')
setMessage('')
}
} catch (err: unknown) {
const error = err as { response?: { data?: { detail?: string } } }
toast.error(error.response?.data?.detail || 'Failed to submit feedback. Please try again.')
} finally {
setIsSubmitting(false)
}
}
const handleNewFeedback = () => {
setSubmitted(false)
setEmail(user?.email ?? '')
}
return (
{/* Page header */}
Send Feedback
Help us improve ResolutionFlow. Report bugs, request features, or share your thoughts.
{submitted ? (
Thank you for your feedback!
We've received your submission and will review it shortly.
) : (
)}
)
}
export default FeedbackPage
```
**Step 2: Commit**
```bash
git add frontend/src/pages/FeedbackPage.tsx
git commit -m "feat: add FeedbackPage component"
```
---
## Task 8: Router & Navigation
**Files:**
- Modify: `frontend/src/router.tsx`
- Modify: `frontend/src/components/layout/Sidebar.tsx`
- Modify: `frontend/src/pages/AccountSettingsPage.tsx`
**Step 1: Add the route**
In `frontend/src/router.tsx`:
Add to the lazy imports section (after the `MyAnalyticsPage` import, around line 32):
```typescript
const FeedbackPage = lazy(() => import('@/pages/FeedbackPage'))
```
Add as a child of the `'/'` route, after the `analytics/me` route (around line 228, before the admin routes comment):
```tsx
{
path: 'feedback',
element: (
}>
),
},
```
**Step 2: Add the sidebar nav item**
In `frontend/src/components/layout/Sidebar.tsx`:
Add `MessageSquareText` to the lucide-react import (line 3):
```typescript
import { LayoutGrid, Box, PenLine, Clock, FileText, Bookmark, BarChart3, Users, Settings, PanelLeftClose, PanelLeftOpen, MessageSquareText } from 'lucide-react'
```
In the collapsed sidebar section (around line 147, after the Analytics NavItem):
```tsx
```
In the expanded sidebar section, add in the footer area (around line 202, before the Team NavItem):
```tsx
```
**Step 3: Add the account settings link card**
In `frontend/src/pages/AccountSettingsPage.tsx`:
Add `MessageSquareText` to the lucide-react import.
Add a link card after the last existing link card (after the Target Lists card), outside any permission guard:
```tsx
{/* Feedback Link (all users) */}
Send Feedback
Report bugs, request features, or share your thoughts
→
```
**Step 4: Commit**
```bash
git add frontend/src/router.tsx frontend/src/components/layout/Sidebar.tsx frontend/src/pages/AccountSettingsPage.tsx
git commit -m "feat: add feedback route, sidebar nav item, and account link card"
```
---
## Task 9: Build Verification
**Step 1: Run the frontend build**
```bash
cd frontend && npm run build
```
Expected: Build succeeds with no errors. TypeScript type-checking is stricter in the build than `tsc --noEmit`.
**Step 2: Run the backend tests**
```bash
cd backend && pytest tests/test_feedback.py -v --override-ini="addopts="
```
Expected: All tests pass.
**Step 3: Set FEEDBACK_EMAIL in .env for local testing**
Add to `backend/.env`:
```
FEEDBACK_EMAIL=your-email@example.com
```
**Step 4: Manual smoke test**
1. Start backend: `cd backend && uvicorn app.main:app --reload`
2. Start frontend: `cd frontend && npm run dev`
3. Log in as any test user
4. Navigate to `/feedback` via sidebar
5. Verify form loads with email pre-filled
6. Submit feedback — verify success state
7. Check email inbox for the formatted feedback email
8. Verify the account settings page shows the "Send Feedback" link card
---
## Task 10: Final Commit & Cleanup
**Step 1: Run full backend test suite**
```bash
cd backend && pytest --override-ini="addopts="
```
Expected: All tests pass, no regressions.
**Step 2: Final frontend build check**
```bash
cd frontend && npm run build
```
Expected: Clean build.