@@ -15,17 +15,22 @@ interface ReviewQueueRow {
1515 name : string
1616 user_id : number
1717 user_display_name : string
18+ status : string
1819 tier : ProjectTier
1920 is_build_review : boolean
2021 waiting_since_iso : string
2122 claimed_by : { name : string ; avatar : string } | null
23+ flagged_by_name : string | null
2224 requirements_checked_by : string | null
2325}
2426
2527const REQUIREMENTS_QUEUE = 'requirements'
28+ const FLAGGED_QUEUE = 'flagged'
2629
2730function queueLabel ( key : string ) {
28- return key === REQUIREMENTS_QUEUE ? 'REQS' : key . toUpperCase ( )
31+ if ( key === REQUIREMENTS_QUEUE ) return 'REQS'
32+ if ( key === FLAGGED_QUEUE ) return 'Flagged'
33+ return key . toUpperCase ( )
2934}
3035
3136const FILTERS = [
@@ -34,6 +39,34 @@ const FILTERS = [
3439 { key : 'build' , label : 'Build' } ,
3540]
3641
42+ const STATUS_FILTERS = [
43+ { key : '' , label : 'All statuses' } ,
44+ { key : 'draft' , label : 'Draft' } ,
45+ { key : 'pending' , label : 'Pending' } ,
46+ { key : 'approved' , label : 'Approved' } ,
47+ { key : 'returned' , label : 'Returned' } ,
48+ { key : 'rejected' , label : 'Rejected' } ,
49+ { key : 'pitch_pending' , label : 'Pitch Pending' } ,
50+ { key : 'pitch_approved' , label : 'Pitch Approved' } ,
51+ ]
52+
53+ const STATUS_BADGE_VARIANT : Record < string , 'outline' | 'success' | 'warning' | 'destructive' | 'secondary' > = {
54+ draft : 'secondary' ,
55+ pending : 'outline' ,
56+ approved : 'success' ,
57+ returned : 'warning' ,
58+ rejected : 'destructive' ,
59+ pitch_pending : 'outline' ,
60+ pitch_approved : 'success' ,
61+ }
62+
63+ function statusLabel ( status : string ) {
64+ return status
65+ . split ( '_' )
66+ . map ( ( w ) => w [ 0 ] . toUpperCase ( ) + w . slice ( 1 ) )
67+ . join ( ' ' )
68+ }
69+
3770function waitBadgeVariant ( seconds : number , slaHours : number ) : 'outline' | 'warning' | 'destructive' {
3871 const hours = seconds / 3600
3972 if ( hours >= slaHours ) return 'destructive'
@@ -46,6 +79,7 @@ export default function AdminReviewsQueue({
4679 pagy,
4780 query,
4881 filter,
82+ status,
4983 tier,
5084 allowed_tiers,
5185 metrics,
@@ -55,13 +89,15 @@ export default function AdminReviewsQueue({
5589 pagy : PagyProps
5690 query : string
5791 filter : string
92+ status : string
5893 tier : string
5994 allowed_tiers : string [ ]
6095 metrics : QueueMetrics
6196 first_pending_id : number | null
6297} ) {
6398 const [ searchQuery , setSearchQuery ] = useState ( query )
6499 const [ now , setNow ] = useState ( ( ) => Date . now ( ) )
100+ const isFlaggedQueue = tier === FLAGGED_QUEUE
65101
66102 useEffect ( ( ) => {
67103 const t = setInterval ( ( ) => setNow ( Date . now ( ) ) , 30000 )
@@ -70,13 +106,25 @@ export default function AdminReviewsQueue({
70106
71107 function submitSearch ( e : React . FormEvent ) {
72108 e . preventDefault ( )
73- router . get ( `/admin/reviews/${ tier } ` , { query : searchQuery , filter : filter || undefined } , { preserveState : true } )
109+ router . get (
110+ `/admin/reviews/${ tier } ` ,
111+ { query : searchQuery , filter : filter || undefined , status : status || undefined } ,
112+ { preserveState : true } ,
113+ )
74114 }
75115
76116 function applyFilter ( key : string ) {
77117 router . get (
78118 `/admin/reviews/${ tier } ` ,
79- { query : searchQuery || undefined , filter : key || undefined } ,
119+ { query : searchQuery || undefined , filter : key || undefined , status : status || undefined } ,
120+ { preserveState : true } ,
121+ )
122+ }
123+
124+ function applyStatus ( key : string ) {
125+ router . get (
126+ `/admin/reviews/${ tier } ` ,
127+ { query : searchQuery || undefined , filter : filter || undefined , status : key || undefined } ,
80128 { preserveState : true } ,
81129 )
82130 }
@@ -86,12 +134,18 @@ export default function AdminReviewsQueue({
86134 < div className = "flex flex-col md:flex-row md:items-center md:justify-between gap-4" >
87135 < div >
88136 < h1 className = "text-2xl font-semibold tracking-tight" >
89- { tier === REQUIREMENTS_QUEUE ? 'Requirements Check Queue' : `Tier ${ tier . slice ( 1 ) } Review Queue` }
137+ { tier === REQUIREMENTS_QUEUE
138+ ? 'Requirements Check Queue'
139+ : tier === FLAGGED_QUEUE
140+ ? 'Flagged Projects'
141+ : `Tier ${ tier . slice ( 1 ) } Review Queue` }
90142 </ h1 >
91143 < p className = "text-sm text-muted-foreground mt-1" >
92144 { tier === REQUIREMENTS_QUEUE
93145 ? 'Projects waiting on a requirements check. Work the checklist, then pass it on or return it to the builder.'
94- : 'Oldest submissions first. Open one to start a timed review session — keyboard shortcuts make it fast.' }
146+ : tier === FLAGGED_QUEUE
147+ ? 'Projects a reviewer has pulled aside for a second look. Resolve the flag from inside the review.'
148+ : 'Oldest submissions first. Open one to start a timed review session — keyboard shortcuts make it fast.' }
95149 </ p >
96150 { allowed_tiers . length > 1 && (
97151 < div className = "flex gap-2 mt-3" >
@@ -144,6 +198,28 @@ export default function AdminReviewsQueue({
144198 </ button >
145199 )
146200 } ) }
201+ { isFlaggedQueue && (
202+ < >
203+ < span className = "w-px bg-border mx-1" />
204+ { STATUS_FILTERS . map ( ( f ) => {
205+ const isActive = ( status || '' ) === f . key
206+ return (
207+ < button
208+ key = { f . key }
209+ onClick = { ( ) => applyStatus ( f . key ) }
210+ className = { cn (
211+ 'inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-md border transition-colors cursor-pointer' ,
212+ isActive
213+ ? 'bg-primary text-primary-foreground border-primary'
214+ : 'border-border bg-background text-muted-foreground hover:bg-accent hover:text-foreground' ,
215+ ) }
216+ >
217+ { f . label }
218+ </ button >
219+ )
220+ } ) }
221+ </ >
222+ ) }
147223 </ div >
148224 < form onSubmit = { submitSearch } className = "flex gap-2 items-center" >
149225 < div className = "relative" >
@@ -164,16 +240,19 @@ export default function AdminReviewsQueue({
164240
165241 < div className = "rounded-md border border-border bg-card overflow-hidden" >
166242 { projects . length === 0 ? (
167- < p className = "text-sm text-muted-foreground py-12 text-center" > Nothing pending — queue is clear. 🎉</ p >
243+ < p className = "text-sm text-muted-foreground py-12 text-center" >
244+ { isFlaggedQueue ? 'No flagged projects — queue is clear. 🎉' : 'Nothing pending — queue is clear. 🎉' }
245+ </ p >
168246 ) : (
169247 < Table >
170248 < TableHeader >
171249 < TableRow >
172250 < TableHead > Project</ TableHead >
173251 < TableHead > Author</ TableHead >
174252 < TableHead > Type</ TableHead >
253+ { isFlaggedQueue && < TableHead > Status</ TableHead > }
175254 < TableHead > Waiting</ TableHead >
176- < TableHead > Claimed</ TableHead >
255+ < TableHead > { isFlaggedQueue ? 'Flagged by' : ' Claimed' } </ TableHead >
177256 < TableHead className = "w-24" > </ TableHead >
178257 </ TableRow >
179258 </ TableHeader >
@@ -206,14 +285,25 @@ export default function AdminReviewsQueue({
206285 < span className = "text-xs text-muted-foreground capitalize" > { p . tier . replace ( '_' , ' ' ) } </ span >
207286 ) }
208287 </ TableCell >
288+ { isFlaggedQueue && (
289+ < TableCell >
290+ < Badge variant = { STATUS_BADGE_VARIANT [ p . status ] ?? 'outline' } > { statusLabel ( p . status ) } </ Badge >
291+ </ TableCell >
292+ ) }
209293 < TableCell >
210294 < Badge variant = { waitBadgeVariant ( waitSeconds , metrics . sla_hours ) } >
211295 { formatDuration ( waitSeconds ) }
212296 { waitSeconds / 3600 >= metrics . sla_hours && ' overdue' }
213297 </ Badge >
214298 </ TableCell >
215299 < TableCell >
216- { p . claimed_by ? (
300+ { isFlaggedQueue ? (
301+ p . flagged_by_name ? (
302+ < span className = "text-xs text-muted-foreground" > { p . flagged_by_name } </ span >
303+ ) : (
304+ < span className = "text-xs text-muted-foreground/50" > —</ span >
305+ )
306+ ) : p . claimed_by ? (
217307 < span className = "flex items-center gap-1.5 text-xs text-muted-foreground" >
218308 < img src = { p . claimed_by . avatar } alt = "" className = "size-4 rounded-full" />
219309 { p . claimed_by . name }
0 commit comments