263 lines
9.9 KiB
TypeScript
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>
|
|
)
|
|
}
|