Files
MOPC-Portal/src/components/ui/datetime-picker.tsx
Matt e2782b2b19 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>
2026-02-03 19:48:41 +01:00

193 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}