Add background filtering jobs, improved date picker, AI reasoning display

- Implement background job system for AI filtering to avoid HTTP timeouts
- Add FilteringJob model to track progress of long-running filtering operations
- Add real-time progress polling for filtering operations on round details page
- Create custom DateTimePicker component with calendar popup (no year picker hassle)
- Fix round date persistence bug (refetchOnWindowFocus was resetting form state)
- Integrate filtering controls into round details page for filtering rounds
- Display AI reasoning for flagged/filtered projects in results table
- Add onboarding system scaffolding (schema, routes, basic UI)
- Allow setting round dates in the past for manual overrides

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 19:48:41 +01:00
parent 8be740a4fb
commit e2782b2b19
24 changed files with 3692 additions and 443 deletions

View File

@@ -0,0 +1,192 @@
'use client'
import * as React from 'react'
import { format, setHours, setMinutes } from 'date-fns'
import { CalendarIcon, Clock } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { Calendar } from '@/components/ui/calendar'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
interface DateTimePickerProps {
value?: Date | null
onChange?: (date: Date | null) => void
placeholder?: string
disabled?: boolean
className?: string
/** If true, only shows date picker without time */
dateOnly?: boolean
/** If true, allows clearing the value */
clearable?: boolean
}
// Generate hour options (00-23)
const hours = Array.from({ length: 24 }, (_, i) => i)
// Generate minute options (00, 15, 30, 45) for easier selection
const minutes = [0, 15, 30, 45]
export function DateTimePicker({
value,
onChange,
placeholder = 'Select date and time',
disabled = false,
className,
dateOnly = false,
clearable = true,
}: DateTimePickerProps) {
const [open, setOpen] = React.useState(false)
const [selectedDate, setSelectedDate] = React.useState<Date | undefined>(
value ?? undefined
)
// Sync internal state with external value
React.useEffect(() => {
setSelectedDate(value ?? undefined)
}, [value])
const handleDateSelect = (date: Date | undefined) => {
if (!date) {
setSelectedDate(undefined)
onChange?.(null)
return
}
// Preserve time from previous selection or use noon as default
const newDate = selectedDate
? setHours(setMinutes(date, selectedDate.getMinutes()), selectedDate.getHours())
: setHours(setMinutes(date, 0), 12) // Default to noon
setSelectedDate(newDate)
onChange?.(newDate)
}
const handleTimeChange = (type: 'hour' | 'minute', valueStr: string) => {
if (!selectedDate) return
const numValue = parseInt(valueStr, 10)
let newDate: Date
if (type === 'hour') {
newDate = setHours(selectedDate, numValue)
} else {
newDate = setMinutes(selectedDate, numValue)
}
setSelectedDate(newDate)
onChange?.(newDate)
}
const handleClear = (e: React.MouseEvent) => {
e.stopPropagation()
setSelectedDate(undefined)
onChange?.(null)
}
const formatDisplayDate = (date: Date) => {
if (dateOnly) {
return format(date, 'MMM d, yyyy')
}
return format(date, 'MMM d, yyyy HH:mm')
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
disabled={disabled}
className={cn(
'w-full justify-start text-left font-normal',
!selectedDate && 'text-muted-foreground',
className
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{selectedDate ? formatDisplayDate(selectedDate) : placeholder}
{clearable && selectedDate && (
<span
role="button"
tabIndex={0}
className="ml-auto text-muted-foreground hover:text-foreground"
onClick={handleClear}
onKeyDown={(e) => e.key === 'Enter' && handleClear(e as unknown as React.MouseEvent)}
>
×
</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<div className="flex flex-col">
<Calendar
mode="single"
selected={selectedDate}
onSelect={handleDateSelect}
initialFocus
/>
{!dateOnly && (
<div className="border-t p-3">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">Time:</span>
<Select
value={selectedDate ? String(selectedDate.getHours()) : undefined}
onValueChange={(v) => handleTimeChange('hour', v)}
disabled={!selectedDate}
>
<SelectTrigger className="w-[70px]">
<SelectValue placeholder="HH" />
</SelectTrigger>
<SelectContent>
{hours.map((hour) => (
<SelectItem key={hour} value={String(hour)}>
{String(hour).padStart(2, '0')}
</SelectItem>
))}
</SelectContent>
</Select>
<span className="text-muted-foreground">:</span>
<Select
value={selectedDate ? String(selectedDate.getMinutes()) : undefined}
onValueChange={(v) => handleTimeChange('minute', v)}
disabled={!selectedDate}
>
<SelectTrigger className="w-[70px]">
<SelectValue placeholder="MM" />
</SelectTrigger>
<SelectContent>
{minutes.map((minute) => (
<SelectItem key={minute} value={String(minute)}>
{String(minute).padStart(2, '0')}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{selectedDate && (
<p className="mt-2 text-xs text-muted-foreground">
Selected: {format(selectedDate, 'EEEE, MMMM d, yyyy')} at{' '}
{format(selectedDate, 'HH:mm')}
</p>
)}
</div>
)}
</div>
</PopoverContent>
</Popover>
)
}