MOPC-App/src/components/admin/rounds/config/intake-config.tsx

263 lines
9.9 KiB
TypeScript

'use client'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Plus, Trash2 } from 'lucide-react'
type IntakeConfigProps = {
config: Record<string, unknown>
onChange: (config: Record<string, unknown>) => void
}
const MIME_PRESETS = [
{ label: 'PDF', value: 'application/pdf' },
{ label: 'Word', value: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' },
{ label: 'Excel', value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
{ label: 'PowerPoint', value: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' },
{ label: 'Images', value: 'image/*' },
{ label: 'Video', value: 'video/*' },
]
const FIELD_TYPES = [
{ value: 'text', label: 'Text' },
{ value: 'textarea', label: 'Text Area' },
{ value: 'select', label: 'Dropdown' },
{ value: 'checkbox', label: 'Checkbox' },
{ value: 'date', label: 'Date' },
]
export function IntakeConfig({ config, onChange }: IntakeConfigProps) {
const update = (key: string, value: unknown) => {
onChange({ ...config, [key]: value })
}
const allowedMimeTypes = (config.allowedMimeTypes as string[]) ?? ['application/pdf']
const customFields = (config.customFields as Array<{
id: string; label: string; type: string; required: boolean; options?: string[]
}>) ?? []
const toggleMime = (mime: string) => {
const current = [...allowedMimeTypes]
const idx = current.indexOf(mime)
if (idx >= 0) {
current.splice(idx, 1)
} else {
current.push(mime)
}
update('allowedMimeTypes', current)
}
const addCustomField = () => {
update('customFields', [
...customFields,
{ id: `field-${Date.now()}`, label: '', type: 'text', required: false },
])
}
const updateCustomField = (index: number, field: typeof customFields[0]) => {
const updated = [...customFields]
updated[index] = field
update('customFields', updated)
}
const removeCustomField = (index: number) => {
update('customFields', customFields.filter((_, i) => i !== index))
}
return (
<div className="space-y-6">
{/* Basic Settings */}
<Card>
<CardHeader>
<CardTitle className="text-base">Application Settings</CardTitle>
<CardDescription>Configure how projects are submitted during intake</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label htmlFor="allowDrafts">Allow Drafts</Label>
<p className="text-xs text-muted-foreground">Let applicants save incomplete submissions</p>
</div>
<Switch
id="allowDrafts"
checked={(config.allowDrafts as boolean) ?? true}
onCheckedChange={(v) => update('allowDrafts', v)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="draftExpiryDays">Draft Expiry (days)</Label>
<p className="text-xs text-muted-foreground">Days before incomplete drafts are automatically deleted</p>
<Input
id="draftExpiryDays"
type="number"
min={1}
className="w-32"
value={(config.draftExpiryDays as number) ?? 30}
onChange={(e) => update('draftExpiryDays', parseInt(e.target.value, 10) || 30)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label htmlFor="publicFormEnabled">Public Application Form</Label>
<p className="text-xs text-muted-foreground">Allow applications without login</p>
</div>
<Switch
id="publicFormEnabled"
checked={(config.publicFormEnabled as boolean) ?? false}
onCheckedChange={(v) => update('publicFormEnabled', v)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label htmlFor="lateSubmissionNotification">Late Submission Notification</Label>
<p className="text-xs text-muted-foreground">Notify admins when submissions arrive after deadline</p>
</div>
<Switch
id="lateSubmissionNotification"
checked={(config.lateSubmissionNotification as boolean) ?? true}
onCheckedChange={(v) => update('lateSubmissionNotification', v)}
/>
</div>
</CardContent>
</Card>
{/* File Settings */}
<Card>
<CardHeader>
<CardTitle className="text-base">File Upload Settings</CardTitle>
<CardDescription>Constraints for uploaded documents</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="maxFileSizeMB">Max File Size (MB)</Label>
<Input
id="maxFileSizeMB"
type="number"
min={1}
className="w-32"
value={(config.maxFileSizeMB as number) ?? 50}
onChange={(e) => update('maxFileSizeMB', parseInt(e.target.value, 10) || 50)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxFilesPerSlot">Max Files per Slot</Label>
<Input
id="maxFilesPerSlot"
type="number"
min={1}
className="w-32"
value={(config.maxFilesPerSlot as number) ?? 1}
onChange={(e) => update('maxFilesPerSlot', parseInt(e.target.value, 10) || 1)}
/>
</div>
</div>
<div className="space-y-2">
<Label>Allowed File Types</Label>
<div className="flex flex-wrap gap-2">
{MIME_PRESETS.map((preset) => (
<Badge
key={preset.value}
variant={allowedMimeTypes.includes(preset.value) ? 'default' : 'outline'}
className="cursor-pointer select-none"
onClick={() => toggleMime(preset.value)}
>
{preset.label}
</Badge>
))}
</div>
</div>
</CardContent>
</Card>
{/* Custom Fields */}
<Card>
<CardHeader>
<CardTitle className="text-base">Custom Application Fields</CardTitle>
<CardDescription>Additional fields applicants must fill in</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{customFields.length === 0 && (
<p className="text-sm text-muted-foreground">No custom fields configured.</p>
)}
{customFields.map((field, idx) => (
<div key={field.id} className="flex items-start gap-3 rounded-lg border p-3">
<div className="flex-1 space-y-3">
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1">
<Label className="text-xs">Label</Label>
<Input
value={field.label}
placeholder="Field name"
onChange={(e) => updateCustomField(idx, { ...field, label: e.target.value })}
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Type</Label>
<Select
value={field.type}
onValueChange={(v) => updateCustomField(idx, { ...field, type: v })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{FIELD_TYPES.map((ft) => (
<SelectItem key={ft.value} value={ft.value}>{ft.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{field.type === 'select' && (
<div className="space-y-1">
<Label className="text-xs">Options (comma-separated)</Label>
<Input
value={(field.options ?? []).join(', ')}
placeholder="Option 1, Option 2, Option 3"
onChange={(e) => updateCustomField(idx, {
...field,
options: e.target.value.split(',').map((o) => o.trim()).filter(Boolean),
})}
/>
</div>
)}
<div className="flex items-center gap-2">
<Switch
checked={field.required}
onCheckedChange={(v) => updateCustomField(idx, { ...field, required: v })}
/>
<Label className="text-xs">Required</Label>
</div>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-muted-foreground hover:text-destructive"
onClick={() => removeCustomField(idx)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
<Button variant="outline" size="sm" onClick={addCustomField}>
<Plus className="h-4 w-4 mr-1" />
Add Field
</Button>
</CardContent>
</Card>
</div>
)
}