Files
MOPC-Portal/src/components/admin/advance-projects-dialog.tsx

280 lines
9.0 KiB
TypeScript
Raw Normal View History

'use client'
import { useState, useCallback, useEffect, useMemo } from 'react'
import { trpc } from '@/lib/trpc/client'
import { toast } from 'sonner'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Badge } from '@/components/ui/badge'
import { Label } from '@/components/ui/label'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { ArrowRightCircle, Loader2, Info } from 'lucide-react'
interface AdvanceProjectsDialogProps {
roundId: string
programId: string
open: boolean
onOpenChange: (open: boolean) => void
onSuccess?: () => void
}
export function AdvanceProjectsDialog({
roundId,
programId,
open,
onOpenChange,
onSuccess,
}: AdvanceProjectsDialogProps) {
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
const [targetRoundId, setTargetRoundId] = useState<string>('')
const utils = trpc.useUtils()
// Reset state when dialog opens
useEffect(() => {
if (open) {
setSelectedIds(new Set())
setTargetRoundId('')
}
}, [open])
// Fetch rounds in program
const { data: roundsData } = trpc.round.list.useQuery(
{ programId },
{ enabled: open }
)
// Fetch projects in current round
const { data: projectsData, isLoading } = trpc.project.list.useQuery(
{ roundId, page: 1, perPage: 200 },
{ enabled: open }
)
// Auto-select next round by sortOrder
const otherRounds = useMemo(() => {
if (!roundsData) return []
return roundsData
.filter((r) => r.id !== roundId)
.sort((a, b) => a.sortOrder - b.sortOrder)
}, [roundsData, roundId])
const currentRound = useMemo(() => {
return roundsData?.find((r) => r.id === roundId)
}, [roundsData, roundId])
// Auto-select next round in sort order
useEffect(() => {
if (open && otherRounds.length > 0 && !targetRoundId && currentRound) {
const nextRound = otherRounds.find(
(r) => r.sortOrder > currentRound.sortOrder
)
setTargetRoundId(nextRound?.id || otherRounds[0].id)
}
}, [open, otherRounds, targetRoundId, currentRound])
const advanceMutation = trpc.round.advanceProjects.useMutation({
onSuccess: (result) => {
const targetName = otherRounds.find((r) => r.id === targetRoundId)?.name
toast.success(
`${result.advanced} project${result.advanced !== 1 ? 's' : ''} advanced to ${targetName}`
)
utils.round.get.invalidate()
utils.project.list.invalidate()
onSuccess?.()
onOpenChange(false)
},
onError: (error) => {
toast.error(error.message)
},
})
const projects = projectsData?.projects ?? []
const toggleProject = useCallback((id: string) => {
setSelectedIds((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}, [])
const toggleAll = useCallback(() => {
if (selectedIds.size === projects.length) {
setSelectedIds(new Set())
} else {
setSelectedIds(new Set(projects.map((p) => p.id)))
}
}, [selectedIds.size, projects])
const handleAdvance = () => {
if (selectedIds.size === 0 || !targetRoundId) return
advanceMutation.mutate({
fromRoundId: roundId,
toRoundId: targetRoundId,
projectIds: Array.from(selectedIds),
})
}
const targetRoundName = otherRounds.find((r) => r.id === targetRoundId)?.name
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[85vh] flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<ArrowRightCircle className="h-5 w-5" />
Advance Projects
</DialogTitle>
<DialogDescription>
Select projects to advance to the next round. Projects will remain
visible in the current round.
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Label>Target Round</Label>
{otherRounds.length === 0 ? (
<p className="text-sm text-muted-foreground">
No other rounds available in this program. Create another round first.
</p>
) : (
<Select value={targetRoundId} onValueChange={setTargetRoundId}>
<SelectTrigger>
<SelectValue placeholder="Select target round" />
</SelectTrigger>
<SelectContent>
{otherRounds.map((r) => (
<SelectItem key={r.id} value={r.id}>
{r.name} (Order: {r.sortOrder})
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
<div className="flex items-start gap-2 rounded-lg bg-blue-50 p-3 text-sm text-blue-800 dark:bg-blue-950/50 dark:text-blue-200">
<Info className="h-4 w-4 mt-0.5 shrink-0" />
<span>
Projects will be copied to the target round with &quot;Submitted&quot; status.
They will remain in the current round with their existing status.
</span>
</div>
<div className="flex-1 min-h-0 overflow-hidden">
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : projects.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<p className="font-medium">No projects in this round</p>
<p className="text-sm text-muted-foreground">
Assign projects to this round first.
</p>
</div>
) : (
<div className="space-y-3">
<div className="flex items-center gap-2">
<Checkbox
checked={selectedIds.size === projects.length && projects.length > 0}
onCheckedChange={toggleAll}
/>
<span className="text-sm text-muted-foreground">
{selectedIds.size} of {projects.length} selected
</span>
</div>
<div className="rounded-lg border max-h-[300px] overflow-y-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12" />
<TableHead>Project</TableHead>
<TableHead>Team</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{projects.map((project) => (
<TableRow
key={project.id}
className={selectedIds.has(project.id) ? 'bg-muted/50' : 'cursor-pointer'}
onClick={() => toggleProject(project.id)}
>
<TableCell>
<Checkbox
checked={selectedIds.has(project.id)}
onCheckedChange={() => toggleProject(project.id)}
onClick={(e) => e.stopPropagation()}
/>
</TableCell>
<TableCell className="font-medium">
{project.title}
</TableCell>
<TableCell className="text-muted-foreground">
{project.teamName || '—'}
</TableCell>
<TableCell>
<Badge variant="secondary" className="text-xs">
{(project.roundProjects?.[0]?.status ?? 'SUBMITTED').replace('_', ' ')}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
onClick={handleAdvance}
disabled={
selectedIds.size === 0 ||
!targetRoundId ||
advanceMutation.isPending
}
>
{advanceMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<ArrowRightCircle className="mr-2 h-4 w-4" />
)}
Advance Selected ({selectedIds.size})
{targetRoundName ? ` to ${targetRoundName}` : ''}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}