feat: applicant onboarding, bulk invite, team management enhancements
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m50s

- Add nationality/institution fields to User model with migration
- Applicant onboarding wizard (name, photo, nationality, country, institution, bio, project logo, preferences)
- Project logo upload from applicant context with team membership verification
- APPLICANT redirects in set-password, onboarding, and auth layout
- Mask evaluation round names as "Evaluation Round 1/2/..." for applicants
- Extend inviteTeamMember with nationality/country/institution/sendInvite fields
- Admin getApplicants query with search/filter/pagination
- Admin bulkInviteApplicants mutation with token generation and emails
- Applicants tab on Members page with bulk select and floating invite bar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 10:11:11 +01:00
parent 68aa393559
commit 49e706f2cf
11 changed files with 1509 additions and 8 deletions

View File

@@ -46,6 +46,8 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import { CountrySelect } from '@/components/ui/country-select'
import { Checkbox as CheckboxPrimitive } from '@/components/ui/checkbox'
import {
Users,
UserPlus,
@@ -64,6 +66,10 @@ const inviteSchema = z.object({
email: z.string().email('Invalid email address'),
role: z.enum(['MEMBER', 'ADVISOR']),
title: z.string().optional(),
nationality: z.string().optional(),
country: z.string().optional(),
institution: z.string().optional(),
sendInvite: z.boolean().default(true),
})
type InviteFormData = z.infer<typeof inviteSchema>
@@ -129,6 +135,10 @@ export default function ApplicantTeamPage() {
email: '',
role: 'MEMBER',
title: '',
nationality: '',
country: '',
institution: '',
sendInvite: true,
},
})
@@ -280,6 +290,42 @@ export default function ApplicantTeamPage() {
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Nationality</Label>
<CountrySelect
value={form.watch('nationality') || ''}
onChange={(v) => form.setValue('nationality', v)}
placeholder="Select nationality"
/>
</div>
<div className="space-y-2">
<Label>Country of Residence</Label>
<CountrySelect
value={form.watch('country') || ''}
onChange={(v) => form.setValue('country', v)}
placeholder="Select country"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="institution">Institution (optional)</Label>
<Input
id="institution"
placeholder="e.g., Ocean Research Institute"
{...form.register('institution')}
/>
</div>
<div className="flex items-center gap-2">
<CheckboxPrimitive
id="sendInvite"
checked={form.watch('sendInvite')}
onCheckedChange={(checked) => form.setValue('sendInvite', !!checked)}
/>
<Label htmlFor="sendInvite" className="text-sm font-normal cursor-pointer">
Send platform invite email
</Label>
</div>
<div className="rounded-lg bg-muted/50 border p-3 text-sm">
<p className="font-medium mb-1">What invited members can do:</p>
<ul className="list-disc list-inside space-y-1 text-muted-foreground">