Remove dynamic form builder and complete RoundProject→roundId migration
Major cleanup and schema migration: - Remove unused dynamic form builder system (ApplicationForm, ApplicationFormField, etc.) - Complete migration from RoundProject junction table to direct Project.roundId - Add sortOrder and entryNotificationType fields to Round model - Add country field to User model for mentor matching - Enhance onboarding with profile photo and country selection steps - Fix all TypeScript errors related to roundProjects references - Remove unused libraries (@radix-ui/react-toast, embla-carousel-react, vaul) Files removed: - admin/forms/* pages and related components - admin/onboarding/* pages - applicationForm.ts and onboarding.ts routers - Dynamic form builder Prisma models and enums Schema changes: - Removed ApplicationForm, ApplicationFormField, OnboardingStep, ApplicationFormSubmission, SubmissionFile models - Removed FormFieldType and SpecialFieldType enums - Added Round.sortOrder, Round.entryNotificationType - Added User.country Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,12 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible'
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
@@ -42,12 +48,19 @@ import {
|
||||
Plus,
|
||||
FileSpreadsheet,
|
||||
UserPlus,
|
||||
FolderKanban,
|
||||
ChevronDown,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
type Step = 'input' | 'preview' | 'sending' | 'complete'
|
||||
type Role = 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER'
|
||||
|
||||
interface Assignment {
|
||||
projectId: string
|
||||
roundId: string
|
||||
}
|
||||
|
||||
interface MemberRow {
|
||||
id: string
|
||||
name: string
|
||||
@@ -55,6 +68,7 @@ interface MemberRow {
|
||||
role: Role
|
||||
expertiseTags: string[]
|
||||
tagInput: string
|
||||
assignments: Assignment[]
|
||||
}
|
||||
|
||||
interface ParsedUser {
|
||||
@@ -62,6 +76,7 @@ interface ParsedUser {
|
||||
name?: string
|
||||
role: Role
|
||||
expertiseTags?: string[]
|
||||
assignments?: Assignment[]
|
||||
isValid: boolean
|
||||
error?: string
|
||||
isDuplicate?: boolean
|
||||
@@ -81,7 +96,7 @@ function nextRowId(): string {
|
||||
}
|
||||
|
||||
function createEmptyRow(role: Role = 'JURY_MEMBER'): MemberRow {
|
||||
return { id: nextRowId(), name: '', email: '', role, expertiseTags: [], tagInput: '' }
|
||||
return { id: nextRowId(), name: '', email: '', role, expertiseTags: [], tagInput: '', assignments: [] }
|
||||
}
|
||||
|
||||
// Common expertise tags for suggestions
|
||||
@@ -115,8 +130,12 @@ export default function MemberInvitePage() {
|
||||
const [result, setResult] = useState<{
|
||||
created: number
|
||||
skipped: number
|
||||
assignmentsCreated?: number
|
||||
} | null>(null)
|
||||
|
||||
// Pre-assignment state
|
||||
const [selectedRoundId, setSelectedRoundId] = useState<string>('')
|
||||
|
||||
const utils = trpc.useUtils()
|
||||
const bulkCreate = trpc.user.bulkCreate.useMutation({
|
||||
onSuccess: () => {
|
||||
@@ -125,6 +144,33 @@ export default function MemberInvitePage() {
|
||||
},
|
||||
})
|
||||
|
||||
// Fetch programs with rounds for pre-assignment
|
||||
const { data: programsData } = trpc.program.list.useQuery({
|
||||
status: 'ACTIVE',
|
||||
includeRounds: true,
|
||||
})
|
||||
// Flatten all rounds from all programs
|
||||
const rounds = useMemo(() => {
|
||||
if (!programsData) return []
|
||||
type ProgramWithRounds = typeof programsData[number] & {
|
||||
rounds?: Array<{ id: string; name: string }>
|
||||
}
|
||||
return (programsData as ProgramWithRounds[]).flatMap((program) =>
|
||||
(program.rounds || []).map((round) => ({
|
||||
id: round.id,
|
||||
name: round.name,
|
||||
programName: `${program.name} ${program.year}`,
|
||||
}))
|
||||
)
|
||||
}, [programsData])
|
||||
|
||||
// Fetch projects for selected round
|
||||
const { data: projectsData, isLoading: projectsLoading } = trpc.project.list.useQuery(
|
||||
{ roundId: selectedRoundId, perPage: 200 },
|
||||
{ enabled: !!selectedRoundId }
|
||||
)
|
||||
const projects = projectsData?.projects || []
|
||||
|
||||
// --- Manual entry helpers ---
|
||||
const updateRow = (id: string, field: keyof MemberRow, value: string | string[]) => {
|
||||
setRows((prev) =>
|
||||
@@ -176,6 +222,28 @@ export default function MemberInvitePage() {
|
||||
).slice(0, 5)
|
||||
}
|
||||
|
||||
// Per-row project assignment management
|
||||
const toggleProjectAssignment = (rowId: string, projectId: string) => {
|
||||
if (!selectedRoundId) return
|
||||
setRows((prev) =>
|
||||
prev.map((r) => {
|
||||
if (r.id !== rowId) return r
|
||||
const existing = r.assignments.find((a) => a.projectId === projectId)
|
||||
if (existing) {
|
||||
return { ...r, assignments: r.assignments.filter((a) => a.projectId !== projectId) }
|
||||
} else {
|
||||
return { ...r, assignments: [...r.assignments, { projectId, roundId: selectedRoundId }] }
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const clearRowAssignments = (rowId: string) => {
|
||||
setRows((prev) =>
|
||||
prev.map((r) => (r.id === rowId ? { ...r, assignments: [] } : r))
|
||||
)
|
||||
}
|
||||
|
||||
// --- CSV helpers ---
|
||||
const handleCSVUpload = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -252,6 +320,7 @@ export default function MemberInvitePage() {
|
||||
name: r.name.trim() || undefined,
|
||||
role: r.role,
|
||||
expertiseTags: r.expertiseTags.length > 0 ? r.expertiseTags : undefined,
|
||||
assignments: r.assignments.length > 0 ? r.assignments : undefined,
|
||||
isValid: isValidFormat && !isDuplicate,
|
||||
isDuplicate,
|
||||
error: !isValidFormat
|
||||
@@ -298,6 +367,7 @@ export default function MemberInvitePage() {
|
||||
name: u.name,
|
||||
role: u.role,
|
||||
expertiseTags: u.expertiseTags,
|
||||
assignments: u.assignments,
|
||||
})),
|
||||
})
|
||||
setSendProgress(100)
|
||||
@@ -362,6 +432,35 @@ export default function MemberInvitePage() {
|
||||
|
||||
{inputMethod === 'manual' ? (
|
||||
<div className="space-y-4">
|
||||
{/* Round selector for pre-assignments */}
|
||||
<div className="rounded-lg border border-dashed p-4 bg-muted/30">
|
||||
<div className="flex items-center gap-3">
|
||||
<FolderKanban className="h-5 w-5 text-muted-foreground shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<Label className="text-sm font-medium">Pre-assign Projects (Optional)</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Select a round to assign projects to jury members before they onboard
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedRoundId || 'none'}
|
||||
onValueChange={(v) => setSelectedRoundId(v === 'none' ? '' : v)}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder="Select round" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">No pre-assignment</SelectItem>
|
||||
{rounds.map((round) => (
|
||||
<SelectItem key={round.id} value={round.id}>
|
||||
{round.programName} - {round.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Member cards */}
|
||||
{rows.map((row, index) => (
|
||||
<div
|
||||
@@ -500,6 +599,81 @@ export default function MemberInvitePage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Per-member project pre-assignment (only for jury members) */}
|
||||
{row.role === 'JURY_MEMBER' && selectedRoundId && (
|
||||
<Collapsible className="space-y-2">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-between text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<FolderKanban className="h-4 w-4" />
|
||||
Pre-assign Projects
|
||||
{row.assignments.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-1">
|
||||
{row.assignments.length}
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-2">
|
||||
{projectsLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
<span className="text-sm text-muted-foreground">Loading projects...</span>
|
||||
</div>
|
||||
) : projects.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground py-2">
|
||||
No projects in this round
|
||||
</p>
|
||||
) : (
|
||||
<div className="max-h-48 overflow-y-auto space-y-1 border rounded-lg p-2">
|
||||
{projects.map((project) => {
|
||||
const isAssigned = row.assignments.some(
|
||||
(a) => a.projectId === project.id
|
||||
)
|
||||
return (
|
||||
<label
|
||||
key={project.id}
|
||||
className={cn(
|
||||
'flex items-center gap-2 p-2 rounded-md cursor-pointer hover:bg-muted',
|
||||
isAssigned && 'bg-primary/5'
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isAssigned}
|
||||
onCheckedChange={() =>
|
||||
toggleProjectAssignment(row.id, project.id)
|
||||
}
|
||||
/>
|
||||
<span className="text-sm truncate flex-1">
|
||||
{project.title}
|
||||
</span>
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{row.assignments.length > 0 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => clearRowAssignments(row.id)}
|
||||
className="text-xs text-muted-foreground"
|
||||
>
|
||||
Clear all assignments
|
||||
</Button>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -722,6 +896,9 @@ export default function MemberInvitePage() {
|
||||
{result?.skipped
|
||||
? ` ${result.skipped} skipped (already exist).`
|
||||
: ''}
|
||||
{result?.assignmentsCreated && result.assignmentsCreated > 0
|
||||
? ` ${result.assignmentsCreated} project assignment${result.assignmentsCreated !== 1 ? 's' : ''} pre-assigned.`
|
||||
: ''}
|
||||
</p>
|
||||
<div className="mt-6 flex gap-3">
|
||||
<Button variant="outline" asChild>
|
||||
|
||||
Reference in New Issue
Block a user