Round system redesign: Phases 1-7 complete
Full pipeline/track/stage architecture replacing the legacy round system. Schema: 11 new models (Pipeline, Track, Stage, StageTransition, ProjectStageState, RoutingRule, Cohort, CohortProject, LiveProgressCursor, OverrideAction, AudienceVoter) + 8 new enums. Backend: 9 new routers (pipeline, stage, routing, stageFiltering, stageAssignment, cohort, live, decision, award) + 6 new services (stage-engine, routing-engine, stage-filtering, stage-assignment, stage-notifications, live-control). Frontend: Pipeline wizard (17 components), jury stage pages (7), applicant pipeline pages (3), public stage pages (2), admin pipeline pages (5), shared stage components (3), SSE route, live hook. Phase 6 refit: 23 routers/services migrated from roundId to stageId, all frontend components refitted. Deleted round.ts (985 lines), roundTemplate.ts, round-helpers.ts, round-settings.ts, round-type-settings.tsx, 10 legacy admin pages, 7 legacy jury pages, 3 legacy dialogs. Phase 7 validation: 36 tests (10 unit + 8 integration files) all passing, TypeScript 0 errors, Next.js build succeeds, 13 integrity checks, legacy symbol sweep clean, auto-seed on first Docker startup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -120,9 +120,7 @@ function EditProjectContent({ projectId }: { projectId: string }) {
|
||||
})
|
||||
|
||||
// Fetch existing tags for suggestions
|
||||
const { data: existingTags } = trpc.project.getTags.useQuery({
|
||||
roundId: project?.roundId ?? undefined,
|
||||
})
|
||||
const { data: existingTags } = trpc.project.getTags.useQuery({})
|
||||
|
||||
// Mutations
|
||||
const utils = trpc.useUtils()
|
||||
@@ -137,7 +135,7 @@ function EditProjectContent({ projectId }: { projectId: string }) {
|
||||
const deleteProject = trpc.project.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.project.list.invalidate()
|
||||
utils.round.get.invalidate()
|
||||
utils.program.get.invalidate()
|
||||
router.push('/admin/projects')
|
||||
},
|
||||
})
|
||||
@@ -202,7 +200,6 @@ function EditProjectContent({ projectId }: { projectId: string }) {
|
||||
teamName: data.teamName || null,
|
||||
description: data.description || null,
|
||||
status: data.status,
|
||||
roundId: project?.roundId ?? undefined,
|
||||
tags: data.tags,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -86,17 +86,12 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
||||
// Fetch files (flat list for backward compatibility)
|
||||
const { data: files } = trpc.file.listByProject.useQuery({ projectId })
|
||||
|
||||
// Fetch grouped files by round (if project has a roundId)
|
||||
const { data: groupedFiles } = trpc.file.listByProjectForRound.useQuery(
|
||||
{ projectId, roundId: project?.roundId || '' },
|
||||
{ enabled: !!project?.roundId }
|
||||
)
|
||||
|
||||
// Fetch available rounds for upload selector (if project has a programId)
|
||||
const { data: rounds } = trpc.round.listByProgram.useQuery(
|
||||
{ programId: project?.programId || '' },
|
||||
// Fetch available stages for upload selector (if project has a programId)
|
||||
const { data: programData } = trpc.program.get.useQuery(
|
||||
{ id: project?.programId || '' },
|
||||
{ enabled: !!project?.programId }
|
||||
)
|
||||
const availableStages = (programData?.stages as Array<{ id: string; name: string }>) || []
|
||||
|
||||
const utils = trpc.useUtils()
|
||||
|
||||
@@ -148,15 +143,15 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-1 text-sm text-muted-foreground">
|
||||
{project.roundId ? (
|
||||
{project.programId ? (
|
||||
<Link
|
||||
href={`/admin/rounds/${project.roundId}`}
|
||||
href={`/admin/programs/${project.programId}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{project.round?.name ?? 'Round'}
|
||||
{programData?.name ?? 'Program'}
|
||||
</Link>
|
||||
) : (
|
||||
<span>No round</span>
|
||||
<span>No program</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -526,9 +521,7 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{groupedFiles && groupedFiles.length > 0 ? (
|
||||
<FileViewer groupedFiles={groupedFiles} />
|
||||
) : files && files.length > 0 ? (
|
||||
{files && files.length > 0 ? (
|
||||
<FileViewer
|
||||
projectId={projectId}
|
||||
files={files.map((f) => ({
|
||||
@@ -551,13 +544,9 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
||||
<p className="text-sm font-medium mb-3">Upload New Files</p>
|
||||
<FileUpload
|
||||
projectId={projectId}
|
||||
roundId={project.roundId || undefined}
|
||||
availableRounds={rounds?.map((r: { id: string; name: string }) => ({ id: r.id, name: r.name }))}
|
||||
availableStages={availableStages?.map((s: { id: string; name: string }) => ({ id: s.id, name: s.name }))}
|
||||
onUploadComplete={() => {
|
||||
utils.file.listByProject.invalidate({ projectId })
|
||||
if (project.roundId) {
|
||||
utils.file.listByProjectForRound.invalidate({ projectId, roundId: project.roundId })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -585,7 +574,7 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href={`/admin/rounds/${project.roundId}/assignments`}>
|
||||
<Link href={`/admin/members`}>
|
||||
Manage
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -688,10 +677,10 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
||||
)}
|
||||
|
||||
{/* AI Evaluation Summary */}
|
||||
{project.roundId && stats && stats.totalEvaluations > 0 && (
|
||||
{assignments && assignments.length > 0 && stats && stats.totalEvaluations > 0 && (
|
||||
<EvaluationSummaryCard
|
||||
projectId={projectId}
|
||||
roundId={project.roundId}
|
||||
stageId={assignments[0].stageId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -30,26 +30,26 @@ function ImportPageContent() {
|
||||
const router = useRouter()
|
||||
const utils = trpc.useUtils()
|
||||
const searchParams = useSearchParams()
|
||||
const roundIdParam = searchParams.get('round')
|
||||
const stageIdParam = searchParams.get('stage')
|
||||
|
||||
const [selectedRoundId, setSelectedRoundId] = useState<string>(roundIdParam || '')
|
||||
const [selectedStageId, setSelectedStageId] = useState<string>(stageIdParam || '')
|
||||
|
||||
// Fetch active programs with rounds
|
||||
// Fetch active programs with stages
|
||||
const { data: programs, isLoading: loadingPrograms } = trpc.program.list.useQuery({
|
||||
status: 'ACTIVE',
|
||||
includeRounds: true,
|
||||
includeStages: true,
|
||||
})
|
||||
|
||||
// Get all rounds from programs
|
||||
const rounds = programs?.flatMap((p) =>
|
||||
(p.rounds || []).map((r) => ({
|
||||
...r,
|
||||
// Get all stages from programs
|
||||
const stages = programs?.flatMap((p) =>
|
||||
((p.stages ?? []) as Array<{ id: string; name: string }>).map((s: { id: string; name: string }) => ({
|
||||
...s,
|
||||
programId: p.id,
|
||||
programName: `${p.year} Edition`,
|
||||
}))
|
||||
) || []
|
||||
|
||||
const selectedRound = rounds.find((r) => r.id === selectedRoundId)
|
||||
const selectedStage = stages.find((s: { id: string }) => s.id === selectedStageId)
|
||||
|
||||
if (loadingPrograms) {
|
||||
return <ImportPageSkeleton />
|
||||
@@ -70,44 +70,44 @@ function ImportPageContent() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Import Projects</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Import projects from a CSV file into a round
|
||||
Import projects from a CSV file into a stage
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Round selection */}
|
||||
{!selectedRoundId && (
|
||||
{/* Stage selection */}
|
||||
{!selectedStageId && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Select Round</CardTitle>
|
||||
<CardTitle>Select Stage</CardTitle>
|
||||
<CardDescription>
|
||||
Choose the round you want to import projects into
|
||||
Choose the stage you want to import projects into
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{rounds.length === 0 ? (
|
||||
{stages.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<AlertCircle className="h-12 w-12 text-muted-foreground/50" />
|
||||
<p className="mt-2 font-medium">No Active Rounds</p>
|
||||
<p className="mt-2 font-medium">No Active Stages</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Create a round first before importing projects
|
||||
Create a stage first before importing projects
|
||||
</p>
|
||||
<Button asChild className="mt-4">
|
||||
<Link href="/admin/rounds/new">Create Round</Link>
|
||||
<Link href="/admin/rounds/new-pipeline">Create Pipeline</Link>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Select value={selectedRoundId} onValueChange={setSelectedRoundId}>
|
||||
<Select value={selectedStageId} onValueChange={setSelectedStageId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a round" />
|
||||
<SelectValue placeholder="Select a stage" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{rounds.map((round) => (
|
||||
<SelectItem key={round.id} value={round.id}>
|
||||
{stages.map((stage) => (
|
||||
<SelectItem key={stage.id} value={stage.id}>
|
||||
<div className="flex flex-col">
|
||||
<span>{round.name}</span>
|
||||
<span>{stage.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{round.programName}
|
||||
{stage.programName}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
@@ -117,11 +117,11 @@ function ImportPageContent() {
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (selectedRoundId) {
|
||||
router.push(`/admin/projects/import?round=${selectedRoundId}`)
|
||||
if (selectedStageId) {
|
||||
router.push(`/admin/projects/import?stage=${selectedStageId}`)
|
||||
}
|
||||
}}
|
||||
disabled={!selectedRoundId}
|
||||
disabled={!selectedStageId}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
@@ -132,14 +132,14 @@ function ImportPageContent() {
|
||||
)}
|
||||
|
||||
{/* Import form */}
|
||||
{selectedRoundId && selectedRound && (
|
||||
{selectedStageId && selectedStage && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<FileSpreadsheet className="h-8 w-8 text-muted-foreground" />
|
||||
<div>
|
||||
<p className="font-medium">Importing into: {selectedRound.name}</p>
|
||||
<p className="font-medium">Importing into: {selectedStage.name}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{selectedRound.programName}
|
||||
{selectedStage.programName}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -147,11 +147,11 @@ function ImportPageContent() {
|
||||
size="sm"
|
||||
className="ml-auto"
|
||||
onClick={() => {
|
||||
setSelectedRoundId('')
|
||||
setSelectedStageId('')
|
||||
router.push('/admin/projects/import')
|
||||
}}
|
||||
>
|
||||
Change Round
|
||||
Change Stage
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -172,32 +172,31 @@ function ImportPageContent() {
|
||||
</TabsList>
|
||||
<TabsContent value="csv" className="mt-4">
|
||||
<CSVImportForm
|
||||
programId={selectedRound.programId}
|
||||
roundId={selectedRoundId}
|
||||
roundName={selectedRound.name}
|
||||
programId={selectedStage.programId}
|
||||
stageName={selectedStage.name}
|
||||
onSuccess={() => {
|
||||
utils.project.list.invalidate()
|
||||
utils.round.get.invalidate()
|
||||
utils.program.get.invalidate()
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="notion" className="mt-4">
|
||||
<NotionImportForm
|
||||
roundId={selectedRoundId}
|
||||
roundName={selectedRound.name}
|
||||
programId={selectedStage.programId}
|
||||
stageName={selectedStage.name}
|
||||
onSuccess={() => {
|
||||
utils.project.list.invalidate()
|
||||
utils.round.get.invalidate()
|
||||
utils.program.get.invalidate()
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="typeform" className="mt-4">
|
||||
<TypeformImportForm
|
||||
roundId={selectedRoundId}
|
||||
roundName={selectedRound.name}
|
||||
programId={selectedStage.programId}
|
||||
stageName={selectedStage.name}
|
||||
onSuccess={() => {
|
||||
utils.project.list.invalidate()
|
||||
utils.round.get.invalidate()
|
||||
utils.program.get.invalidate()
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
@@ -85,11 +85,11 @@ const ROLE_SORT_ORDER: Record<string, number> = { LEAD: 0, MEMBER: 1, ADVISOR: 2
|
||||
function NewProjectPageContent() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const roundIdParam = searchParams.get('round')
|
||||
const stageIdParam = searchParams.get('stage')
|
||||
const programIdParam = searchParams.get('program')
|
||||
|
||||
const [selectedProgramId, setSelectedProgramId] = useState<string>(programIdParam || '')
|
||||
const [selectedRoundId, setSelectedRoundId] = useState<string>(roundIdParam || '')
|
||||
const [selectedStageId, setSelectedStageId] = useState<string>(stageIdParam || '')
|
||||
|
||||
// Form state
|
||||
const [title, setTitle] = useState('')
|
||||
@@ -113,7 +113,7 @@ function NewProjectPageContent() {
|
||||
// Fetch programs
|
||||
const { data: programs, isLoading: loadingPrograms } = trpc.program.list.useQuery({
|
||||
status: 'ACTIVE',
|
||||
includeRounds: true,
|
||||
includeStages: true,
|
||||
})
|
||||
|
||||
// Fetch wizard config for selected program (dropdown options)
|
||||
@@ -128,7 +128,7 @@ function NewProjectPageContent() {
|
||||
onSuccess: () => {
|
||||
toast.success('Project created successfully')
|
||||
utils.project.list.invalidate()
|
||||
utils.round.get.invalidate()
|
||||
utils.program.get.invalidate()
|
||||
router.push('/admin/projects')
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -136,9 +136,9 @@ function NewProjectPageContent() {
|
||||
},
|
||||
})
|
||||
|
||||
// Get rounds for selected program
|
||||
// Get stages for selected program
|
||||
const selectedProgram = programs?.find((p) => p.id === selectedProgramId)
|
||||
const rounds = selectedProgram?.rounds || []
|
||||
const stages = (selectedProgram?.stages || []) as Array<{ id: string; name: string }>
|
||||
|
||||
// Get dropdown options from wizard config
|
||||
const categoryOptions = wizardConfig?.competitionCategories || []
|
||||
@@ -216,7 +216,6 @@ function NewProjectPageContent() {
|
||||
|
||||
createProject.mutate({
|
||||
programId: selectedProgramId,
|
||||
roundId: selectedRoundId || undefined,
|
||||
title: title.trim(),
|
||||
teamName: teamName.trim() || undefined,
|
||||
description: description.trim() || undefined,
|
||||
@@ -264,12 +263,12 @@ function NewProjectPageContent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Program & Round selection */}
|
||||
{/* Program & Stage selection */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Program & Round</CardTitle>
|
||||
<CardTitle>Program & Stage</CardTitle>
|
||||
<CardDescription>
|
||||
Select the program for this project. Round assignment is optional.
|
||||
Select the program for this project. Stage assignment is optional.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -287,7 +286,7 @@ function NewProjectPageContent() {
|
||||
<Label>Program *</Label>
|
||||
<Select value={selectedProgramId} onValueChange={(v) => {
|
||||
setSelectedProgramId(v)
|
||||
setSelectedRoundId('') // Reset round on program change
|
||||
setSelectedStageId('') // Reset stage on program change
|
||||
}}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a program" />
|
||||
@@ -303,16 +302,16 @@ function NewProjectPageContent() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Round (optional)</Label>
|
||||
<Select value={selectedRoundId || '__none__'} onValueChange={(v) => setSelectedRoundId(v === '__none__' ? '' : v)} disabled={!selectedProgramId}>
|
||||
<Label>Stage (optional)</Label>
|
||||
<Select value={selectedStageId || '__none__'} onValueChange={(v) => setSelectedStageId(v === '__none__' ? '' : v)} disabled={!selectedProgramId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="No round assigned" />
|
||||
<SelectValue placeholder="No stage assigned" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="__none__">No round assigned</SelectItem>
|
||||
{rounds.map((r: { id: string; name: string }) => (
|
||||
<SelectItem key={r.id} value={r.id}>
|
||||
{r.name}
|
||||
<SelectItem value="__none__">No stage assigned</SelectItem>
|
||||
{stages.map((s) => (
|
||||
<SelectItem key={s.id} value={s.id}>
|
||||
{s.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -96,6 +96,7 @@ import { CountryFlagImg } from '@/components/ui/country-select'
|
||||
import {
|
||||
ProjectFiltersBar,
|
||||
type ProjectFilters,
|
||||
type FilterOptions,
|
||||
} from './project-filters'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
|
||||
@@ -121,7 +122,7 @@ function parseFiltersFromParams(
|
||||
statuses: searchParams.get('status')
|
||||
? searchParams.get('status')!.split(',')
|
||||
: [],
|
||||
roundId: searchParams.get('round') || '',
|
||||
stageId: searchParams.get('stage') || '',
|
||||
competitionCategory: searchParams.get('category') || '',
|
||||
oceanIssue: searchParams.get('issue') || '',
|
||||
country: searchParams.get('country') || '',
|
||||
@@ -155,7 +156,7 @@ function filtersToParams(
|
||||
if (filters.search) params.set('q', filters.search)
|
||||
if (filters.statuses.length > 0)
|
||||
params.set('status', filters.statuses.join(','))
|
||||
if (filters.roundId) params.set('round', filters.roundId)
|
||||
if (filters.stageId) params.set('stage', filters.stageId)
|
||||
if (filters.competitionCategory)
|
||||
params.set('category', filters.competitionCategory)
|
||||
if (filters.oceanIssue) params.set('issue', filters.oceanIssue)
|
||||
@@ -180,7 +181,7 @@ export default function ProjectsPage() {
|
||||
const [filters, setFilters] = useState<ProjectFilters>({
|
||||
search: parsed.search,
|
||||
statuses: parsed.statuses,
|
||||
roundId: parsed.roundId,
|
||||
stageId: parsed.stageId,
|
||||
competitionCategory: parsed.competitionCategory,
|
||||
oceanIssue: parsed.oceanIssue,
|
||||
country: parsed.country,
|
||||
@@ -251,7 +252,7 @@ export default function ProjectsPage() {
|
||||
| 'REJECTED'
|
||||
>)
|
||||
: undefined,
|
||||
roundId: filters.roundId || undefined,
|
||||
stageId: filters.stageId || undefined,
|
||||
competitionCategory:
|
||||
(filters.competitionCategory as 'STARTUP' | 'BUSINESS_CONCEPT') ||
|
||||
undefined,
|
||||
@@ -283,14 +284,14 @@ export default function ProjectsPage() {
|
||||
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||
const [projectToDelete, setProjectToDelete] = useState<{ id: string; title: string } | null>(null)
|
||||
// Assign to round dialog state
|
||||
// Assign to stage dialog state
|
||||
const [assignDialogOpen, setAssignDialogOpen] = useState(false)
|
||||
const [projectToAssign, setProjectToAssign] = useState<{ id: string; title: string } | null>(null)
|
||||
const [assignRoundId, setAssignRoundId] = useState('')
|
||||
const [assignStageId, setAssignStageId] = useState('')
|
||||
|
||||
const [aiTagDialogOpen, setAiTagDialogOpen] = useState(false)
|
||||
const [taggingScope, setTaggingScope] = useState<'round' | 'program'>('round')
|
||||
const [selectedRoundForTagging, setSelectedRoundForTagging] = useState<string>('')
|
||||
const [taggingScope, setTaggingScope] = useState<'stage' | 'program'>('stage')
|
||||
const [selectedStageForTagging, setSelectedStageForTagging] = useState<string>('')
|
||||
const [selectedProgramForTagging, setSelectedProgramForTagging] = useState<string>('')
|
||||
const [activeTaggingJobId, setActiveTaggingJobId] = useState<string | null>(null)
|
||||
|
||||
@@ -350,8 +351,14 @@ export default function ProjectsPage() {
|
||||
: null
|
||||
|
||||
const handleStartTagging = () => {
|
||||
if (taggingScope === 'round' && selectedRoundForTagging) {
|
||||
startTaggingJob.mutate({ roundId: selectedRoundForTagging })
|
||||
if (taggingScope === 'stage' && selectedStageForTagging) {
|
||||
// Router only accepts programId; resolve from the selected stage's parent program
|
||||
const parentProgram = programs?.find((p) =>
|
||||
((p.stages ?? []) as Array<{ id: string }>)?.some((s: { id: string }) => s.id === selectedStageForTagging)
|
||||
)
|
||||
if (parentProgram) {
|
||||
startTaggingJob.mutate({ programId: parentProgram.id })
|
||||
}
|
||||
} else if (taggingScope === 'program' && selectedProgramForTagging) {
|
||||
startTaggingJob.mutate({ programId: selectedProgramForTagging })
|
||||
}
|
||||
@@ -361,20 +368,19 @@ export default function ProjectsPage() {
|
||||
if (!taggingInProgress) {
|
||||
setAiTagDialogOpen(false)
|
||||
setActiveTaggingJobId(null)
|
||||
setSelectedRoundForTagging('')
|
||||
setSelectedStageForTagging('')
|
||||
setSelectedProgramForTagging('')
|
||||
}
|
||||
}
|
||||
|
||||
// Get selected program's rounds
|
||||
// Get selected program's stages (flattened from pipelines -> tracks -> stages)
|
||||
const selectedProgram = programs?.find(p => p.id === selectedProgramForTagging)
|
||||
const programRounds = filterOptions?.rounds?.filter(r => r.program?.id === selectedProgramForTagging) ?? []
|
||||
const programStages = selectedProgram?.stages ?? []
|
||||
|
||||
// Calculate stats for display
|
||||
const selectedRound = filterOptions?.rounds?.find(r => r.id === selectedRoundForTagging)
|
||||
const displayProgram = taggingScope === 'program'
|
||||
? selectedProgram
|
||||
: (selectedRound ? programs?.find(p => p.id === selectedRound.program?.id) : null)
|
||||
: (selectedStageForTagging ? programs?.find(p => (p.stages as Array<{ id: string }>)?.some(s => s.id === selectedStageForTagging)) : null)
|
||||
|
||||
// Calculate progress percentage
|
||||
const taggingProgressPercent = jobStatus && jobStatus.totalProjects > 0
|
||||
@@ -387,7 +393,7 @@ export default function ProjectsPage() {
|
||||
const [bulkStatus, setBulkStatus] = useState<string>('')
|
||||
const [bulkConfirmOpen, setBulkConfirmOpen] = useState(false)
|
||||
const [bulkAction, setBulkAction] = useState<'status' | 'assign' | 'delete'>('status')
|
||||
const [bulkAssignRoundId, setBulkAssignRoundId] = useState('')
|
||||
const [bulkAssignStageId, setBulkAssignStageId] = useState('')
|
||||
const [bulkAssignDialogOpen, setBulkAssignDialogOpen] = useState(false)
|
||||
const [bulkDeleteConfirmOpen, setBulkDeleteConfirmOpen] = useState(false)
|
||||
|
||||
@@ -406,7 +412,7 @@ export default function ProjectsPage() {
|
||||
| 'REJECTED'
|
||||
>)
|
||||
: undefined,
|
||||
roundId: filters.roundId || undefined,
|
||||
stageId: filters.stageId || undefined,
|
||||
competitionCategory:
|
||||
(filters.competitionCategory as 'STARTUP' | 'BUSINESS_CONCEPT') ||
|
||||
undefined,
|
||||
@@ -446,12 +452,12 @@ export default function ProjectsPage() {
|
||||
},
|
||||
})
|
||||
|
||||
const bulkAssignToRound = trpc.projectPool.assignToRound.useMutation({
|
||||
const bulkAssignToStage = trpc.projectPool.assignToStage.useMutation({
|
||||
onSuccess: (result) => {
|
||||
toast.success(`${result.assignedCount} project${result.assignedCount !== 1 ? 's' : ''} assigned to ${result.roundName}`)
|
||||
toast.success(`${result.assignedCount} project${result.assignedCount !== 1 ? 's' : ''} assigned to stage`)
|
||||
setSelectedIds(new Set())
|
||||
setAllMatchingSelected(false)
|
||||
setBulkAssignRoundId('')
|
||||
setBulkAssignStageId('')
|
||||
setBulkAssignDialogOpen(false)
|
||||
utils.project.list.invalidate()
|
||||
},
|
||||
@@ -526,10 +532,9 @@ export default function ProjectsPage() {
|
||||
}
|
||||
|
||||
const handleBulkConfirm = () => {
|
||||
if (!bulkStatus || selectedIds.size === 0 || !filters.roundId) return
|
||||
if (!bulkStatus || selectedIds.size === 0) return
|
||||
bulkUpdateStatus.mutate({
|
||||
ids: Array.from(selectedIds),
|
||||
roundId: filters.roundId,
|
||||
status: bulkStatus as 'SUBMITTED' | 'ELIGIBLE' | 'ASSIGNED' | 'SEMIFINALIST' | 'FINALIST' | 'REJECTED',
|
||||
})
|
||||
}
|
||||
@@ -542,13 +547,13 @@ export default function ProjectsPage() {
|
||||
? data.projects.some((p) => selectedIds.has(p.id)) && !allVisibleSelected
|
||||
: false
|
||||
|
||||
const assignToRound = trpc.projectPool.assignToRound.useMutation({
|
||||
const assignToStage = trpc.projectPool.assignToStage.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success('Project assigned to round')
|
||||
toast.success('Project assigned to stage')
|
||||
utils.project.list.invalidate()
|
||||
setAssignDialogOpen(false)
|
||||
setProjectToAssign(null)
|
||||
setAssignRoundId('')
|
||||
setAssignStageId('')
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message || 'Failed to assign project')
|
||||
@@ -579,7 +584,7 @@ export default function ProjectsPage() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Projects</h1>
|
||||
<p className="text-muted-foreground">
|
||||
{data ? `${data.total} projects across all rounds` : 'Manage submitted projects across all rounds'}
|
||||
{data ? `${data.total} projects across all stages` : 'Manage submitted projects across all stages'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -632,7 +637,17 @@ export default function ProjectsPage() {
|
||||
{/* Filters */}
|
||||
<ProjectFiltersBar
|
||||
filters={filters}
|
||||
filterOptions={filterOptions}
|
||||
filterOptions={filterOptions ? {
|
||||
...filterOptions,
|
||||
stages: programs?.flatMap(p =>
|
||||
(p.stages as Array<{ id: string; name: string }>)?.map(s => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
programName: p.name,
|
||||
programYear: p.year,
|
||||
})) ?? []
|
||||
) ?? [],
|
||||
} satisfies FilterOptions : undefined}
|
||||
onChange={handleFiltersChange}
|
||||
/>
|
||||
|
||||
@@ -755,7 +770,7 @@ export default function ProjectsPage() {
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{filters.search ||
|
||||
filters.statuses.length > 0 ||
|
||||
filters.roundId ||
|
||||
filters.stageId ||
|
||||
filters.competitionCategory ||
|
||||
filters.oceanIssue ||
|
||||
filters.country
|
||||
@@ -799,7 +814,7 @@ export default function ProjectsPage() {
|
||||
</TableHead>
|
||||
<TableHead className="min-w-[280px]">Project</TableHead>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Round</TableHead>
|
||||
<TableHead>Stage</TableHead>
|
||||
<TableHead>Tags</TableHead>
|
||||
<TableHead>Assignments</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
@@ -871,8 +886,8 @@ export default function ProjectsPage() {
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
{project.round ? (
|
||||
<p>{project.round.name}</p>
|
||||
{project.program ? (
|
||||
<p>{project.program.name}</p>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-xs text-amber-600 border-amber-300 bg-amber-50">
|
||||
Unassigned
|
||||
@@ -880,7 +895,7 @@ export default function ProjectsPage() {
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{project.round?.program?.name}
|
||||
{project.program?.year}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -939,7 +954,7 @@ export default function ProjectsPage() {
|
||||
Edit
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{!project.round && (
|
||||
{project._count.assignments === 0 && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
@@ -948,7 +963,7 @@ export default function ProjectsPage() {
|
||||
}}
|
||||
>
|
||||
<FolderOpen className="mr-2 h-4 w-4" />
|
||||
Assign to Round
|
||||
Assign to Stage
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
@@ -1000,8 +1015,8 @@ export default function ProjectsPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Round</span>
|
||||
<span>{project.round?.name ?? 'Unassigned'}</span>
|
||||
<span className="text-muted-foreground">Stage</span>
|
||||
<span>{project.program?.name ?? 'Unassigned'}</span>
|
||||
</div>
|
||||
{project.competitionCategory && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
@@ -1079,7 +1094,7 @@ export default function ProjectsPage() {
|
||||
Edit
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{!project.round && (
|
||||
{project._count.assignments === 0 && (
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
@@ -1088,7 +1103,7 @@ export default function ProjectsPage() {
|
||||
}}
|
||||
>
|
||||
<FolderOpen className="mr-2 h-4 w-4" />
|
||||
Assign to Round
|
||||
Assign to Stage
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
@@ -1138,10 +1153,10 @@ export default function ProjectsPage() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Round</span>
|
||||
<span className="text-muted-foreground">Program</span>
|
||||
<span className="text-right">
|
||||
{project.round ? (
|
||||
<>{project.round.name}</>
|
||||
{project.program ? (
|
||||
<>{project.program.name}</>
|
||||
) : (
|
||||
<Badge variant="outline" className="text-xs text-amber-600 border-amber-300 bg-amber-50">
|
||||
Unassigned
|
||||
@@ -1207,17 +1222,17 @@ export default function ProjectsPage() {
|
||||
{selectedIds.size} selected
|
||||
</Badge>
|
||||
<div className="flex flex-wrap gap-2 flex-1">
|
||||
{/* Assign to Round */}
|
||||
{/* Assign to Stage */}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setBulkAssignDialogOpen(true)}
|
||||
>
|
||||
<ArrowRightCircle className="mr-1.5 h-4 w-4" />
|
||||
Assign to Round
|
||||
Assign to Stage
|
||||
</Button>
|
||||
{/* Change Status (only when filtered by round) */}
|
||||
{filters.roundId && (
|
||||
{/* Change Status (only when filtered by stage) */}
|
||||
{filters.stageId && (
|
||||
<>
|
||||
<Select value={bulkStatus} onValueChange={setBulkStatus}>
|
||||
<SelectTrigger className="w-[160px] h-9 text-sm">
|
||||
@@ -1332,30 +1347,30 @@ export default function ProjectsPage() {
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* Assign to Round Dialog */}
|
||||
{/* Assign to Stage Dialog */}
|
||||
<Dialog open={assignDialogOpen} onOpenChange={(open) => {
|
||||
setAssignDialogOpen(open)
|
||||
if (!open) { setProjectToAssign(null); setAssignRoundId('') }
|
||||
if (!open) { setProjectToAssign(null); setAssignStageId('') }
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Assign to Round</DialogTitle>
|
||||
<DialogTitle>Assign to Stage</DialogTitle>
|
||||
<DialogDescription>
|
||||
Assign "{projectToAssign?.title}" to a round.
|
||||
Assign "{projectToAssign?.title}" to a stage.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Select Round</Label>
|
||||
<Select value={assignRoundId} onValueChange={setAssignRoundId}>
|
||||
<Label>Select Stage</Label>
|
||||
<Select value={assignStageId} onValueChange={setAssignStageId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Choose a round..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{programs?.flatMap((p) =>
|
||||
(p.rounds || []).map((r: { id: string; name: string }) => (
|
||||
<SelectItem key={r.id} value={r.id}>
|
||||
{p.name} {p.year} - {r.name}
|
||||
((p.stages || []) as Array<{ id: string; name: string }>).map((s) => (
|
||||
<SelectItem key={s.id} value={s.id}>
|
||||
{p.name} {p.year} - {s.name}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
@@ -1369,46 +1384,46 @@ export default function ProjectsPage() {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (projectToAssign && assignRoundId) {
|
||||
assignToRound.mutate({
|
||||
if (projectToAssign && assignStageId) {
|
||||
assignToStage.mutate({
|
||||
projectIds: [projectToAssign.id],
|
||||
roundId: assignRoundId,
|
||||
stageId: assignStageId,
|
||||
})
|
||||
}
|
||||
}}
|
||||
disabled={!assignRoundId || assignToRound.isPending}
|
||||
disabled={!assignStageId || assignToStage.isPending}
|
||||
>
|
||||
{assignToRound.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{assignToStage.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Assign
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Bulk Assign to Round Dialog */}
|
||||
{/* Bulk Assign to Stage Dialog */}
|
||||
<Dialog open={bulkAssignDialogOpen} onOpenChange={(open) => {
|
||||
setBulkAssignDialogOpen(open)
|
||||
if (!open) setBulkAssignRoundId('')
|
||||
if (!open) setBulkAssignStageId('')
|
||||
}}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Assign to Round</DialogTitle>
|
||||
<DialogTitle>Assign to Stage</DialogTitle>
|
||||
<DialogDescription>
|
||||
Assign {selectedIds.size} selected project{selectedIds.size !== 1 ? 's' : ''} to a round. Projects will have their status set to "Assigned".
|
||||
Assign {selectedIds.size} selected project{selectedIds.size !== 1 ? 's' : ''} to a stage. Projects will have their status set to "Assigned".
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Select Round</Label>
|
||||
<Select value={bulkAssignRoundId} onValueChange={setBulkAssignRoundId}>
|
||||
<Label>Select Stage</Label>
|
||||
<Select value={bulkAssignStageId} onValueChange={setBulkAssignStageId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Choose a round..." />
|
||||
<SelectValue placeholder="Choose a stage..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{programs?.flatMap((p) =>
|
||||
(p.rounds || []).map((r: { id: string; name: string }) => (
|
||||
<SelectItem key={r.id} value={r.id}>
|
||||
{p.name} {p.year} - {r.name}
|
||||
((p.stages || []) as Array<{ id: string; name: string }>).map((s) => (
|
||||
<SelectItem key={s.id} value={s.id}>
|
||||
{p.name} {p.year} - {s.name}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
@@ -1422,16 +1437,16 @@ export default function ProjectsPage() {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (bulkAssignRoundId && selectedIds.size > 0) {
|
||||
bulkAssignToRound.mutate({
|
||||
if (bulkAssignStageId && selectedIds.size > 0) {
|
||||
bulkAssignToStage.mutate({
|
||||
projectIds: Array.from(selectedIds),
|
||||
roundId: bulkAssignRoundId,
|
||||
stageId: bulkAssignStageId,
|
||||
})
|
||||
}
|
||||
}}
|
||||
disabled={!bulkAssignRoundId || bulkAssignToRound.isPending}
|
||||
disabled={!bulkAssignStageId || bulkAssignToStage.isPending}
|
||||
>
|
||||
{bulkAssignToRound.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{bulkAssignToStage.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Assign {selectedIds.size} Project{selectedIds.size !== 1 ? 's' : ''}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1608,19 +1623,19 @@ export default function ProjectsPage() {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTaggingScope('round')}
|
||||
onClick={() => setTaggingScope('stage')}
|
||||
className={`flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-colors ${
|
||||
taggingScope === 'round'
|
||||
taggingScope === 'stage'
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:border-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<FolderOpen className={`h-6 w-6 ${taggingScope === 'round' ? 'text-primary' : 'text-muted-foreground'}`} />
|
||||
<span className={`text-sm font-medium ${taggingScope === 'round' ? 'text-primary' : ''}`}>
|
||||
Single Round
|
||||
<FolderOpen className={`h-6 w-6 ${taggingScope === 'stage' ? 'text-primary' : 'text-muted-foreground'}`} />
|
||||
<span className={`text-sm font-medium ${taggingScope === 'stage' ? 'text-primary' : ''}`}>
|
||||
Single Stage
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground text-center">
|
||||
Tag projects in one specific round
|
||||
Tag projects in one specific stage
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@@ -1637,7 +1652,7 @@ export default function ProjectsPage() {
|
||||
Entire Edition
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground text-center">
|
||||
Tag all projects across all rounds
|
||||
Tag all projects across all stages
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1645,22 +1660,24 @@ export default function ProjectsPage() {
|
||||
|
||||
{/* Selection */}
|
||||
<div className="space-y-2">
|
||||
{taggingScope === 'round' ? (
|
||||
{taggingScope === 'stage' ? (
|
||||
<>
|
||||
<Label htmlFor="round-select">Select Round</Label>
|
||||
<Label htmlFor="stage-select">Select Stage</Label>
|
||||
<Select
|
||||
value={selectedRoundForTagging}
|
||||
onValueChange={setSelectedRoundForTagging}
|
||||
value={selectedStageForTagging}
|
||||
onValueChange={setSelectedStageForTagging}
|
||||
>
|
||||
<SelectTrigger id="round-select">
|
||||
<SelectValue placeholder="Choose a round..." />
|
||||
<SelectTrigger id="stage-select">
|
||||
<SelectValue placeholder="Choose a stage..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{filterOptions?.rounds?.map((round) => (
|
||||
<SelectItem key={round.id} value={round.id}>
|
||||
{round.name} ({round.program?.name})
|
||||
</SelectItem>
|
||||
))}
|
||||
{programs?.flatMap(p =>
|
||||
(p.stages as Array<{ id: string; name: string }>)?.map(s => (
|
||||
<SelectItem key={s.id} value={s.id}>
|
||||
{s.name} ({p.name})
|
||||
</SelectItem>
|
||||
)) ?? []
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
@@ -1716,7 +1733,7 @@ export default function ProjectsPage() {
|
||||
onClick={handleStartTagging}
|
||||
disabled={
|
||||
taggingInProgress ||
|
||||
(taggingScope === 'round' && !selectedRoundForTagging) ||
|
||||
(taggingScope === 'stage' && !selectedStageForTagging) ||
|
||||
(taggingScope === 'program' && !selectedProgramForTagging)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function ProjectPoolPage() {
|
||||
const [selectedProgramId, setSelectedProgramId] = useState<string>('')
|
||||
const [selectedProjects, setSelectedProjects] = useState<string[]>([])
|
||||
const [assignDialogOpen, setAssignDialogOpen] = useState(false)
|
||||
const [targetRoundId, setTargetRoundId] = useState<string>('')
|
||||
const [targetStageId, setTargetStageId] = useState<string>('')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [categoryFilter, setCategoryFilter] = useState<'STARTUP' | 'BUSINESS_CONCEPT' | 'all'>('all')
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
@@ -50,20 +50,22 @@ export default function ProjectPoolPage() {
|
||||
{ enabled: !!selectedProgramId }
|
||||
)
|
||||
|
||||
const { data: rounds, isLoading: isLoadingRounds } = trpc.round.listByProgram.useQuery(
|
||||
{ programId: selectedProgramId },
|
||||
// Get stages from the selected program (program.list includes rounds/stages)
|
||||
const { data: selectedProgramData, isLoading: isLoadingStages } = trpc.program.get.useQuery(
|
||||
{ id: selectedProgramId },
|
||||
{ enabled: !!selectedProgramId }
|
||||
)
|
||||
const stages = (selectedProgramData?.stages || []) as Array<{ id: string; name: string }>
|
||||
|
||||
const utils = trpc.useUtils()
|
||||
const assignMutation = trpc.projectPool.assignToRound.useMutation({
|
||||
const assignMutation = trpc.projectPool.assignToStage.useMutation({
|
||||
onSuccess: (result) => {
|
||||
utils.project.list.invalidate()
|
||||
utils.round.get.invalidate()
|
||||
toast.success(`Assigned ${result.assignedCount} project${result.assignedCount !== 1 ? 's' : ''} to round`)
|
||||
utils.program.get.invalidate()
|
||||
toast.success(`Assigned ${result.assignedCount} project${result.assignedCount !== 1 ? 's' : ''} to stage`)
|
||||
setSelectedProjects([])
|
||||
setAssignDialogOpen(false)
|
||||
setTargetRoundId('')
|
||||
setTargetStageId('')
|
||||
refetch()
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -72,17 +74,17 @@ export default function ProjectPoolPage() {
|
||||
})
|
||||
|
||||
const handleBulkAssign = () => {
|
||||
if (selectedProjects.length === 0 || !targetRoundId) return
|
||||
if (selectedProjects.length === 0 || !targetStageId) return
|
||||
assignMutation.mutate({
|
||||
projectIds: selectedProjects,
|
||||
roundId: targetRoundId,
|
||||
stageId: targetStageId,
|
||||
})
|
||||
}
|
||||
|
||||
const handleQuickAssign = (projectId: string, roundId: string) => {
|
||||
const handleQuickAssign = (projectId: string, stageId: string) => {
|
||||
assignMutation.mutate({
|
||||
projectIds: [projectId],
|
||||
roundId,
|
||||
stageId,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -109,7 +111,7 @@ export default function ProjectPoolPage() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">Project Pool</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Assign unassigned projects to evaluation rounds
|
||||
Assign unassigned projects to evaluation stages
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -236,20 +238,20 @@ export default function ProjectPoolPage() {
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="p-3">
|
||||
{isLoadingRounds ? (
|
||||
{isLoadingStages ? (
|
||||
<Skeleton className="h-9 w-[200px]" />
|
||||
) : (
|
||||
<Select
|
||||
onValueChange={(roundId) => handleQuickAssign(project.id, roundId)}
|
||||
onValueChange={(stageId) => handleQuickAssign(project.id, stageId)}
|
||||
disabled={assignMutation.isPending}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="Assign to round..." />
|
||||
<SelectValue placeholder="Assign to stage..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{rounds?.map((round: { id: string; name: string; sortOrder: number }) => (
|
||||
<SelectItem key={round.id} value={round.id}>
|
||||
{round.name}
|
||||
{stages?.map((stage: { id: string; name: string }) => (
|
||||
<SelectItem key={stage.id} value={stage.id}>
|
||||
{stage.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -310,20 +312,20 @@ export default function ProjectPoolPage() {
|
||||
<Dialog open={assignDialogOpen} onOpenChange={setAssignDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Assign Projects to Round</DialogTitle>
|
||||
<DialogTitle>Assign Projects to Stage</DialogTitle>
|
||||
<DialogDescription>
|
||||
Assign {selectedProjects.length} selected project{selectedProjects.length > 1 ? 's' : ''} to:
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<Select value={targetRoundId} onValueChange={setTargetRoundId}>
|
||||
<Select value={targetStageId} onValueChange={setTargetStageId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select round..." />
|
||||
<SelectValue placeholder="Select stage..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{rounds?.map((round: { id: string; name: string; sortOrder: number }) => (
|
||||
<SelectItem key={round.id} value={round.id}>
|
||||
{round.name}
|
||||
{stages?.map((stage: { id: string; name: string }) => (
|
||||
<SelectItem key={stage.id} value={stage.id}>
|
||||
{stage.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -335,7 +337,7 @@ export default function ProjectPoolPage() {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleBulkAssign}
|
||||
disabled={!targetRoundId || assignMutation.isPending}
|
||||
disabled={!targetStageId || assignMutation.isPending}
|
||||
>
|
||||
{assignMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Assign
|
||||
|
||||
@@ -63,7 +63,7 @@ const ISSUE_LABELS: Record<string, string> = {
|
||||
export interface ProjectFilters {
|
||||
search: string
|
||||
statuses: string[]
|
||||
roundId: string
|
||||
stageId: string
|
||||
competitionCategory: string
|
||||
oceanIssue: string
|
||||
country: string
|
||||
@@ -72,11 +72,11 @@ export interface ProjectFilters {
|
||||
hasAssignments: boolean | undefined
|
||||
}
|
||||
|
||||
interface FilterOptions {
|
||||
rounds: Array<{ id: string; name: string; program: { name: string; year: number } }>
|
||||
export interface FilterOptions {
|
||||
countries: string[]
|
||||
categories: Array<{ value: string; count: number }>
|
||||
issues: Array<{ value: string; count: number }>
|
||||
stages?: Array<{ id: string; name: string; programName: string; programYear: number }>
|
||||
}
|
||||
|
||||
interface ProjectFiltersBarProps {
|
||||
@@ -94,7 +94,7 @@ export function ProjectFiltersBar({
|
||||
|
||||
const activeFilterCount = [
|
||||
filters.statuses.length > 0,
|
||||
filters.roundId !== '',
|
||||
filters.stageId !== '',
|
||||
filters.competitionCategory !== '',
|
||||
filters.oceanIssue !== '',
|
||||
filters.country !== '',
|
||||
@@ -114,7 +114,7 @@ export function ProjectFiltersBar({
|
||||
onChange({
|
||||
search: filters.search,
|
||||
statuses: [],
|
||||
roundId: '',
|
||||
stageId: '',
|
||||
competitionCategory: '',
|
||||
oceanIssue: '',
|
||||
country: '',
|
||||
@@ -175,21 +175,21 @@ export function ProjectFiltersBar({
|
||||
{/* Select filters grid */}
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm">Round / Edition</Label>
|
||||
<Label className="text-sm">Stage / Edition</Label>
|
||||
<Select
|
||||
value={filters.roundId || '_all'}
|
||||
value={filters.stageId || '_all'}
|
||||
onValueChange={(v) =>
|
||||
onChange({ ...filters, roundId: v === '_all' ? '' : v })
|
||||
onChange({ ...filters, stageId: v === '_all' ? '' : v })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="All rounds" />
|
||||
<SelectValue placeholder="All stages" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_all">All rounds</SelectItem>
|
||||
{filterOptions?.rounds.map((r) => (
|
||||
<SelectItem key={r.id} value={r.id}>
|
||||
{r.name} ({r.program.year} Edition)
|
||||
<SelectItem value="_all">All stages</SelectItem>
|
||||
{filterOptions?.stages?.map((s) => (
|
||||
<SelectItem key={s.id} value={s.id}>
|
||||
{s.name} ({s.programYear} {s.programName})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
Reference in New Issue
Block a user