diff --git a/prisma/seed-candidatures.ts b/prisma/seed-candidatures.ts
index d8f76fd..e0a22bc 100644
--- a/prisma/seed-candidatures.ts
+++ b/prisma/seed-candidatures.ts
@@ -346,7 +346,7 @@ async function main() {
email,
name: row['Full name']?.trim() || 'Unknown',
role: 'APPLICANT',
- status: 'ACTIVE',
+ status: 'INVITED',
phoneNumber: row['Téléphone']?.trim() || null,
},
})
diff --git a/src/app/(admin)/admin/awards/[id]/edit/page.tsx b/src/app/(admin)/admin/awards/[id]/edit/page.tsx
index 079644a..5826cb0 100644
--- a/src/app/(admin)/admin/awards/[id]/edit/page.tsx
+++ b/src/app/(admin)/admin/awards/[id]/edit/page.tsx
@@ -35,8 +35,14 @@ export default function EditAwardPage({
const { id: awardId } = use(params)
const router = useRouter()
+ const utils = trpc.useUtils()
const { data: award, isLoading } = trpc.specialAward.get.useQuery({ id: awardId })
- const updateAward = trpc.specialAward.update.useMutation()
+ const updateAward = trpc.specialAward.update.useMutation({
+ onSuccess: () => {
+ utils.specialAward.get.invalidate({ id: awardId })
+ utils.specialAward.list.invalidate()
+ },
+ })
const [name, setName] = useState('')
const [description, setDescription] = useState('')
diff --git a/src/app/(admin)/admin/awards/[id]/page.tsx b/src/app/(admin)/admin/awards/[id]/page.tsx
index c9cbc56..889a0bc 100644
--- a/src/app/(admin)/admin/awards/[id]/page.tsx
+++ b/src/app/(admin)/admin/awards/[id]/page.tsx
@@ -141,28 +141,39 @@ export default function AwardDetailPage({
const { id: awardId } = use(params)
const router = useRouter()
+ // State declarations (before queries that depend on them)
+ const [isPollingJob, setIsPollingJob] = useState(false)
+ const pollingIntervalRef = useRef | null>(null)
+ const [selectedJurorId, setSelectedJurorId] = useState('')
+ const [includeSubmitted, setIncludeSubmitted] = useState(true)
+ const [addProjectDialogOpen, setAddProjectDialogOpen] = useState(false)
+ const [projectSearchQuery, setProjectSearchQuery] = useState('')
+ const [expandedRows, setExpandedRows] = useState>(new Set())
+ const [activeTab, setActiveTab] = useState('eligibility')
+
+ // Core queries
const { data: award, isLoading, refetch } =
trpc.specialAward.get.useQuery({ id: awardId })
const { data: eligibilityData, refetch: refetchEligibility } =
trpc.specialAward.listEligible.useQuery({
awardId,
page: 1,
- perPage: 500,
+ perPage: 100,
})
const { data: jurors, refetch: refetchJurors } =
trpc.specialAward.listJurors.useQuery({ awardId })
const { data: voteResults } =
trpc.specialAward.getVoteResults.useQuery({ awardId })
- const { data: allUsers } = trpc.user.list.useQuery({ role: 'JURY_MEMBER', page: 1, perPage: 100 })
- // Fetch all projects in the program for manual eligibility addition
- const { data: allProjects } = trpc.project.list.useQuery(
- { programId: award?.programId ?? '', perPage: 500 },
- { enabled: !!award?.programId }
+ // Deferred queries - only load when needed
+ const { data: allUsers } = trpc.user.list.useQuery(
+ { role: 'JURY_MEMBER', page: 1, perPage: 100 },
+ { enabled: activeTab === 'jurors' }
+ )
+ const { data: allProjects } = trpc.project.list.useQuery(
+ { programId: award?.programId ?? '', perPage: 200 },
+ { enabled: !!award?.programId && addProjectDialogOpen }
)
-
- const [isPollingJob, setIsPollingJob] = useState(false)
- const pollingIntervalRef = useRef | null>(null)
// Eligibility job polling
const { data: jobStatus, refetch: refetchJobStatus } =
@@ -208,19 +219,34 @@ export default function AwardDetailPage({
}
}, [award?.eligibilityJobStatus])
- const updateStatus = trpc.specialAward.updateStatus.useMutation()
- const runEligibility = trpc.specialAward.runEligibility.useMutation()
- const setEligibility = trpc.specialAward.setEligibility.useMutation()
- const addJuror = trpc.specialAward.addJuror.useMutation()
- const removeJuror = trpc.specialAward.removeJuror.useMutation()
- const setWinner = trpc.specialAward.setWinner.useMutation()
- const deleteAward = trpc.specialAward.delete.useMutation()
-
- const [selectedJurorId, setSelectedJurorId] = useState('')
- const [includeSubmitted, setIncludeSubmitted] = useState(true)
- const [addProjectDialogOpen, setAddProjectDialogOpen] = useState(false)
- const [projectSearchQuery, setProjectSearchQuery] = useState('')
- const [expandedRows, setExpandedRows] = useState>(new Set())
+ const utils = trpc.useUtils()
+ const invalidateAward = () => {
+ utils.specialAward.get.invalidate({ id: awardId })
+ utils.specialAward.listEligible.invalidate({ awardId })
+ utils.specialAward.listJurors.invalidate({ awardId })
+ utils.specialAward.getVoteResults.invalidate({ awardId })
+ }
+ const updateStatus = trpc.specialAward.updateStatus.useMutation({
+ onSuccess: invalidateAward,
+ })
+ const runEligibility = trpc.specialAward.runEligibility.useMutation({
+ onSuccess: invalidateAward,
+ })
+ const setEligibility = trpc.specialAward.setEligibility.useMutation({
+ onSuccess: () => utils.specialAward.listEligible.invalidate({ awardId }),
+ })
+ const addJuror = trpc.specialAward.addJuror.useMutation({
+ onSuccess: () => utils.specialAward.listJurors.invalidate({ awardId }),
+ })
+ const removeJuror = trpc.specialAward.removeJuror.useMutation({
+ onSuccess: () => utils.specialAward.listJurors.invalidate({ awardId }),
+ })
+ const setWinner = trpc.specialAward.setWinner.useMutation({
+ onSuccess: invalidateAward,
+ })
+ const deleteAward = trpc.specialAward.delete.useMutation({
+ onSuccess: () => utils.specialAward.list.invalidate(),
+ })
const handleStatusChange = async (
status: 'DRAFT' | 'NOMINATIONS_OPEN' | 'VOTING_OPEN' | 'CLOSED' | 'ARCHIVED'
@@ -569,7 +595,7 @@ export default function AwardDetailPage({
{/* Tabs */}
-
+
diff --git a/src/app/(admin)/admin/awards/new/page.tsx b/src/app/(admin)/admin/awards/new/page.tsx
index 6576227..fb6b099 100644
--- a/src/app/(admin)/admin/awards/new/page.tsx
+++ b/src/app/(admin)/admin/awards/new/page.tsx
@@ -38,8 +38,11 @@ export default function CreateAwardPage() {
const [maxRankedPicks, setMaxRankedPicks] = useState('3')
const [programId, setProgramId] = useState('')
+ const utils = trpc.useUtils()
const { data: programs } = trpc.program.list.useQuery()
- const createAward = trpc.specialAward.create.useMutation()
+ const createAward = trpc.specialAward.create.useMutation({
+ onSuccess: () => utils.specialAward.list.invalidate(),
+ })
const handleSubmit = async () => {
if (!name.trim() || !programId) return
diff --git a/src/app/(admin)/admin/learning/[id]/page.tsx b/src/app/(admin)/admin/learning/[id]/page.tsx
index 856647c..b4fd7ec 100644
--- a/src/app/(admin)/admin/learning/[id]/page.tsx
+++ b/src/app/(admin)/admin/learning/[id]/page.tsx
@@ -97,8 +97,16 @@ export default function EditLearningResourcePage() {
// API
const { data: programs } = trpc.program.list.useQuery({ status: 'ACTIVE' })
- const updateResource = trpc.learningResource.update.useMutation()
- const deleteResource = trpc.learningResource.delete.useMutation()
+ const utils = trpc.useUtils()
+ const updateResource = trpc.learningResource.update.useMutation({
+ onSuccess: () => {
+ utils.learningResource.get.invalidate({ id: resourceId })
+ utils.learningResource.list.invalidate()
+ },
+ })
+ const deleteResource = trpc.learningResource.delete.useMutation({
+ onSuccess: () => utils.learningResource.list.invalidate(),
+ })
const getUploadUrl = trpc.learningResource.getUploadUrl.useMutation()
// Populate form when resource loads
diff --git a/src/app/(admin)/admin/learning/new/page.tsx b/src/app/(admin)/admin/learning/new/page.tsx
index d5148e6..a77bed4 100644
--- a/src/app/(admin)/admin/learning/new/page.tsx
+++ b/src/app/(admin)/admin/learning/new/page.tsx
@@ -68,7 +68,10 @@ export default function NewLearningResourcePage() {
const { data: programs } = trpc.program.list.useQuery({ status: 'ACTIVE' })
const [programId, setProgramId] = useState(null)
- const createResource = trpc.learningResource.create.useMutation()
+ const utils = trpc.useUtils()
+ const createResource = trpc.learningResource.create.useMutation({
+ onSuccess: () => utils.learningResource.list.invalidate(),
+ })
const getUploadUrl = trpc.learningResource.getUploadUrl.useMutation()
// Handle file upload for BlockNote
diff --git a/src/app/(admin)/admin/page.tsx b/src/app/(admin)/admin/page.tsx
index 245ae18..521780d 100644
--- a/src/app/(admin)/admin/page.tsx
+++ b/src/app/(admin)/admin/page.tsx
@@ -131,11 +131,11 @@ async function DashboardStats({ editionId, sessionName }: DashboardStatsProps) {
where: { programId: editionId },
}),
prisma.project.count({
- where: { round: { programId: editionId } },
+ where: { programId: editionId },
}),
prisma.project.count({
where: {
- round: { programId: editionId },
+ programId: editionId,
createdAt: { gte: sevenDaysAgo },
},
}),
@@ -186,7 +186,7 @@ async function DashboardStats({ editionId, sessionName }: DashboardStatsProps) {
},
}),
prisma.project.findMany({
- where: { round: { programId: editionId } },
+ where: { programId: editionId },
orderBy: { createdAt: 'desc' },
take: 8,
select: {
@@ -205,12 +205,12 @@ async function DashboardStats({ editionId, sessionName }: DashboardStatsProps) {
}),
prisma.project.groupBy({
by: ['competitionCategory'],
- where: { round: { programId: editionId } },
+ where: { programId: editionId },
_count: true,
}),
prisma.project.groupBy({
by: ['oceanIssue'],
- where: { round: { programId: editionId } },
+ where: { programId: editionId },
_count: true,
}),
// Recent activity feed (scoped to last 7 days for performance)
@@ -243,7 +243,8 @@ async function DashboardStats({ editionId, sessionName }: DashboardStatsProps) {
// Projects without assignments in active rounds
prisma.project.count({
where: {
- round: { programId: editionId, status: 'ACTIVE' },
+ programId: editionId,
+ round: { status: 'ACTIVE' },
assignments: { none: {} },
},
}),
diff --git a/src/app/(admin)/admin/partners/[id]/page.tsx b/src/app/(admin)/admin/partners/[id]/page.tsx
index 6aca19f..46b2c4f 100644
--- a/src/app/(admin)/admin/partners/[id]/page.tsx
+++ b/src/app/(admin)/admin/partners/[id]/page.tsx
@@ -68,8 +68,11 @@ export default function EditPartnerPage() {
}
}, [partner])
+ const utils = trpc.useUtils()
const updatePartner = trpc.partner.update.useMutation({
onSuccess: () => {
+ utils.partner.list.invalidate()
+ utils.partner.get.invalidate()
toast.success('Partner updated successfully')
router.push('/admin/partners')
},
@@ -81,6 +84,7 @@ export default function EditPartnerPage() {
const deletePartner = trpc.partner.delete.useMutation({
onSuccess: () => {
+ utils.partner.list.invalidate()
toast.success('Partner deleted successfully')
router.push('/admin/partners')
},
diff --git a/src/app/(admin)/admin/partners/new/page.tsx b/src/app/(admin)/admin/partners/new/page.tsx
index b33db17..06b9401 100644
--- a/src/app/(admin)/admin/partners/new/page.tsx
+++ b/src/app/(admin)/admin/partners/new/page.tsx
@@ -31,8 +31,10 @@ export default function NewPartnerPage() {
const [partnerType, setPartnerType] = useState('PARTNER')
const [visibility, setVisibility] = useState('ADMIN_ONLY')
+ const utils = trpc.useUtils()
const createPartner = trpc.partner.create.useMutation({
onSuccess: () => {
+ utils.partner.list.invalidate()
toast.success('Partner created successfully')
router.push('/admin/partners')
},
diff --git a/src/app/(admin)/admin/programs/[id]/apply-settings/page.tsx b/src/app/(admin)/admin/programs/[id]/apply-settings/page.tsx
index 0903790..f115a71 100644
--- a/src/app/(admin)/admin/programs/[id]/apply-settings/page.tsx
+++ b/src/app/(admin)/admin/programs/[id]/apply-settings/page.tsx
@@ -254,8 +254,10 @@ export default function ApplySettingsPage() {
)
// --- Mutations ---
+ const utils = trpc.useUtils()
const createTemplate = trpc.wizardTemplate.create.useMutation({
onSuccess: () => {
+ utils.wizardTemplate.list.invalidate()
toast.success('Template saved')
setSaveTemplateOpen(false)
setSaveTemplateName('')
@@ -264,6 +266,7 @@ export default function ApplySettingsPage() {
})
const updateConfig = trpc.program.updateWizardConfig.useMutation({
onSuccess: () => {
+ utils.program.get.invalidate({ id: programId })
toast.success('Settings saved successfully')
setIsDirty(false)
},
diff --git a/src/app/(admin)/admin/programs/[id]/edit/page.tsx b/src/app/(admin)/admin/programs/[id]/edit/page.tsx
index 21af297..726c0de 100644
--- a/src/app/(admin)/admin/programs/[id]/edit/page.tsx
+++ b/src/app/(admin)/admin/programs/[id]/edit/page.tsx
@@ -66,8 +66,11 @@ export default function EditProgramPage() {
}
}, [program])
+ const utils = trpc.useUtils()
const updateProgram = trpc.program.update.useMutation({
onSuccess: () => {
+ utils.program.list.invalidate()
+ utils.program.get.invalidate({ id })
toast.success('Program updated successfully')
router.push(`/admin/programs/${id}`)
},
@@ -79,6 +82,7 @@ export default function EditProgramPage() {
const deleteProgram = trpc.program.delete.useMutation({
onSuccess: () => {
+ utils.program.list.invalidate()
toast.success('Program deleted successfully')
router.push('/admin/programs')
},
diff --git a/src/app/(admin)/admin/programs/new/page.tsx b/src/app/(admin)/admin/programs/new/page.tsx
index a7946dc..94bbff2 100644
--- a/src/app/(admin)/admin/programs/new/page.tsx
+++ b/src/app/(admin)/admin/programs/new/page.tsx
@@ -22,8 +22,10 @@ export default function NewProgramPage() {
const router = useRouter()
const [isSubmitting, setIsSubmitting] = useState(false)
+ const utils = trpc.useUtils()
const createProgram = trpc.program.create.useMutation({
onSuccess: () => {
+ utils.program.list.invalidate()
toast.success('Program created successfully')
router.push('/admin/programs')
},
diff --git a/src/app/(admin)/admin/projects/page.tsx b/src/app/(admin)/admin/projects/page.tsx
index e5dae26..c509c10 100644
--- a/src/app/(admin)/admin/projects/page.tsx
+++ b/src/app/(admin)/admin/projects/page.tsx
@@ -69,6 +69,9 @@ import {
FolderOpen,
X,
AlertTriangle,
+ ArrowRightCircle,
+ LayoutGrid,
+ LayoutList,
} from 'lucide-react'
import {
Select,
@@ -180,6 +183,7 @@ export default function ProjectsPage() {
const [page, setPage] = useState(parsed.page)
const [perPage, setPerPage] = useState(parsed.perPage || 20)
const [searchInput, setSearchInput] = useState(parsed.search)
+ const [viewMode, setViewMode] = useState<'table' | 'card'>('table')
// Fetch display settings
const { data: displaySettings } = trpc.settings.getMultiple.useQuery({
@@ -373,6 +377,10 @@ export default function ProjectsPage() {
const [selectedIds, setSelectedIds] = useState>(new Set())
const [bulkStatus, setBulkStatus] = useState('')
const [bulkConfirmOpen, setBulkConfirmOpen] = useState(false)
+ const [bulkAction, setBulkAction] = useState<'status' | 'assign' | 'delete'>('status')
+ const [bulkAssignRoundId, setBulkAssignRoundId] = useState('')
+ const [bulkAssignDialogOpen, setBulkAssignDialogOpen] = useState(false)
+ const [bulkDeleteConfirmOpen, setBulkDeleteConfirmOpen] = useState(false)
const bulkUpdateStatus = trpc.project.bulkUpdateStatus.useMutation({
onSuccess: (result) => {
@@ -387,6 +395,31 @@ export default function ProjectsPage() {
},
})
+ const bulkAssignToRound = trpc.projectPool.assignToRound.useMutation({
+ onSuccess: (result) => {
+ toast.success(`${result.assignedCount} project${result.assignedCount !== 1 ? 's' : ''} assigned to ${result.roundName}`)
+ setSelectedIds(new Set())
+ setBulkAssignRoundId('')
+ setBulkAssignDialogOpen(false)
+ utils.project.list.invalidate()
+ },
+ onError: (error) => {
+ toast.error(error.message || 'Failed to assign projects')
+ },
+ })
+
+ const bulkDeleteProjects = trpc.project.bulkDelete.useMutation({
+ onSuccess: (result) => {
+ toast.success(`${result.deleted} project${result.deleted !== 1 ? 's' : ''} deleted`)
+ setSelectedIds(new Set())
+ setBulkDeleteConfirmOpen(false)
+ utils.project.list.invalidate()
+ },
+ onError: (error) => {
+ toast.error(error.message || 'Failed to delete projects')
+ },
+ })
+
const handleToggleSelect = (id: string) => {
setSelectedIds((prev) => {
const next = new Set(prev)
@@ -481,12 +514,6 @@ export default function ProjectsPage() {
-