codenuk_frontend_mine/src/components/admin/feature-review-dialog.tsx
2025-08-25 08:17:46 +05:30

312 lines
11 KiB
TypeScript

"use client"
import { useState, useEffect } from 'react'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Label } from '@/components/ui/label'
import {
CheckCircle,
XCircle,
Copy,
AlertTriangle,
Search,
ExternalLink
} from 'lucide-react'
import { AdminFeature, FeatureSimilarity, FeatureReviewData } from '@/types/admin.types'
import { adminApi, formatDate, getStatusColor, getComplexityColor } from '@/lib/api/admin'
interface FeatureReviewDialogProps {
feature: AdminFeature
open: boolean
onOpenChange: (open: boolean) => void
onReview: (featureId: string, reviewData: FeatureReviewData) => Promise<void>
}
export function FeatureReviewDialog({
feature,
open,
onOpenChange,
onReview
}: FeatureReviewDialogProps) {
const [status, setStatus] = useState<'approved' | 'rejected' | 'duplicate'>('approved')
const [notes, setNotes] = useState('')
const [canonicalFeatureId, setCanonicalFeatureId] = useState('')
const [similarFeatures, setSimilarFeatures] = useState<FeatureSimilarity[]>([])
const [loadingSimilar, setLoadingSimilar] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
// Load similar features when dialog opens
useEffect(() => {
if (open && feature.name) {
loadSimilarFeatures(feature.name)
}
}, [open, feature.name])
const loadSimilarFeatures = async (query: string) => {
try {
setLoadingSimilar(true)
const features = await adminApi.findSimilarFeatures(query, 0.7, 5)
setSimilarFeatures(features)
} catch (error) {
console.error('Error loading similar features:', error)
} finally {
setLoadingSimilar(false)
}
}
const handleSubmit = async () => {
if (!status) return
const reviewData: FeatureReviewData = {
status,
notes: notes.trim() || undefined,
canonical_feature_id: status === 'duplicate' ? canonicalFeatureId : undefined,
admin_reviewed_by: 'admin' // TODO: Get from auth context
}
try {
setSubmitting(true)
await onReview(feature.id, reviewData)
} catch (error) {
console.error('Error reviewing feature:', error)
} finally {
setSubmitting(false)
}
}
const handleStatusChange = (newStatus: string) => {
setStatus(newStatus as 'approved' | 'rejected' | 'duplicate')
if (newStatus !== 'duplicate') {
setCanonicalFeatureId('')
}
}
const filteredSimilarFeatures = similarFeatures.filter(f =>
f.name.toLowerCase().includes(searchQuery.toLowerCase())
)
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Review Feature: {feature.name}</DialogTitle>
<DialogDescription>
Review and approve, reject, or mark as duplicate this custom feature submission.
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* Feature Details */}
<Card>
<CardContent className="pt-6">
<div className="space-y-4">
<div className="flex items-center space-x-2">
<h3 className="text-lg font-semibold">{feature.name}</h3>
<Badge className={getStatusColor(feature.status)}>
{feature.status}
</Badge>
<Badge className={getComplexityColor(feature.complexity)}>
{feature.complexity}
</Badge>
</div>
{feature.description && (
<p className="text-gray-600">{feature.description}</p>
)}
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Template:</span> {feature.template_title || 'Unknown'}
</div>
<div>
<span className="font-medium">Submitted:</span> {formatDate(feature.created_at)}
</div>
{feature.similarity_score && (
<div>
<span className="font-medium">Similarity Score:</span> {(feature.similarity_score * 100).toFixed(1)}%
</div>
)}
<div>
<span className="font-medium">Usage Count:</span> {feature.usage_count}
</div>
</div>
{feature.business_rules && (
<div>
<h4 className="font-medium mb-2">Business Rules:</h4>
<pre className="text-sm bg-gray-50 p-2 rounded">
{JSON.stringify(feature.business_rules, null, 2)}
</pre>
</div>
)}
{feature.technical_requirements && (
<div>
<h4 className="font-medium mb-2">Technical Requirements:</h4>
<pre className="text-sm bg-gray-50 p-2 rounded">
{JSON.stringify(feature.technical_requirements, null, 2)}
</pre>
</div>
)}
</div>
</CardContent>
</Card>
{/* Similar Features */}
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between mb-4">
<h4 className="font-medium">Similar Features</h4>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="Search similar features..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 pr-4 py-2 border rounded-md text-sm"
/>
</div>
</div>
{loadingSimilar ? (
<div className="text-center py-4">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-gray-900 mx-auto"></div>
<p className="text-sm text-gray-500 mt-2">Loading similar features...</p>
</div>
) : filteredSimilarFeatures.length > 0 ? (
<div className="space-y-2">
{filteredSimilarFeatures.map((similar) => (
<div
key={similar.id}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50"
>
<div className="flex-1">
<div className="flex items-center space-x-2">
<span className="font-medium">{similar.name}</span>
<Badge className={getComplexityColor(similar.complexity)}>
{similar.complexity}
</Badge>
<Badge variant="outline">
{similar.match_type} ({(similar.score * 100).toFixed(1)}%)
</Badge>
</div>
<p className="text-sm text-gray-500">{similar.feature_type}</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => setCanonicalFeatureId(similar.id)}
disabled={status !== 'duplicate'}
>
<ExternalLink className="h-4 w-4 mr-1" />
Select as Duplicate
</Button>
</div>
))}
</div>
) : (
<div className="text-center py-4 text-gray-500">
<AlertTriangle className="h-8 w-8 mx-auto mb-2 text-gray-300" />
<p>No similar features found</p>
</div>
)}
</CardContent>
</Card>
{/* Review Form */}
<div className="space-y-4">
<div>
<Label htmlFor="status">Review Decision</Label>
<Select value={status} onValueChange={handleStatusChange}>
<SelectTrigger>
<SelectValue placeholder="Select review decision" />
</SelectTrigger>
<SelectContent>
<SelectItem value="approved">
<div className="flex items-center space-x-2">
<CheckCircle className="h-4 w-4 text-green-600" />
<span>Approve</span>
</div>
</SelectItem>
<SelectItem value="rejected">
<div className="flex items-center space-x-2">
<XCircle className="h-4 w-4 text-red-600" />
<span>Reject</span>
</div>
</SelectItem>
<SelectItem value="duplicate">
<div className="flex items-center space-x-2">
<Copy className="h-4 w-4 text-orange-600" />
<span>Mark as Duplicate</span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{status === 'duplicate' && (
<div>
<Label htmlFor="canonical">Canonical Feature ID</Label>
<input
id="canonical"
type="text"
value={canonicalFeatureId}
onChange={(e) => setCanonicalFeatureId(e.target.value)}
placeholder="Enter the ID of the canonical feature"
className="w-full px-3 py-2 border rounded-md"
/>
<p className="text-sm text-gray-500 mt-1">
Select a similar feature above or enter the canonical feature ID manually
</p>
</div>
)}
<div>
<Label htmlFor="notes">Admin Notes (Optional)</Label>
<Textarea
id="notes"
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Add notes about your decision..."
rows={3}
/>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={submitting || (status === 'duplicate' && !canonicalFeatureId)}
>
{submitting ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Submitting...
</>
) : (
`Submit ${status.charAt(0).toUpperCase() + status.slice(1)}`
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}