diff --git a/docs/gdpr/ai-data-processing.docx b/docs/gdpr/ai-data-processing.docx new file mode 100644 index 0000000..dd6ef6e Binary files /dev/null and b/docs/gdpr/ai-data-processing.docx differ diff --git a/docs/gdpr/platform-gdpr-compliance.docx b/docs/gdpr/platform-gdpr-compliance.docx new file mode 100644 index 0000000..3950e89 Binary files /dev/null and b/docs/gdpr/platform-gdpr-compliance.docx differ diff --git a/prisma/migrations/20260203000000_add_ai_usage_log/migration.sql b/prisma/migrations/20260203000000_add_ai_usage_log/migration.sql new file mode 100644 index 0000000..2f1ea53 --- /dev/null +++ b/prisma/migrations/20260203000000_add_ai_usage_log/migration.sql @@ -0,0 +1,33 @@ +-- CreateTable +CREATE TABLE "AIUsageLog" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "userId" TEXT, + "action" TEXT NOT NULL, + "entityType" TEXT, + "entityId" TEXT, + "model" TEXT NOT NULL, + "promptTokens" INTEGER NOT NULL, + "completionTokens" INTEGER NOT NULL, + "totalTokens" INTEGER NOT NULL, + "estimatedCostUsd" DECIMAL(10,6), + "batchSize" INTEGER, + "itemsProcessed" INTEGER, + "status" TEXT NOT NULL, + "errorMessage" TEXT, + "detailsJson" JSONB, + + CONSTRAINT "AIUsageLog_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "AIUsageLog_userId_idx" ON "AIUsageLog"("userId"); + +-- CreateIndex +CREATE INDEX "AIUsageLog_action_idx" ON "AIUsageLog"("action"); + +-- CreateIndex +CREATE INDEX "AIUsageLog_createdAt_idx" ON "AIUsageLog"("createdAt"); + +-- CreateIndex +CREATE INDEX "AIUsageLog_model_idx" ON "AIUsageLog"("model"); diff --git a/src/app/(admin)/admin/awards/[id]/edit/page.tsx b/src/app/(admin)/admin/awards/[id]/edit/page.tsx new file mode 100644 index 0000000..0bfc5b7 --- /dev/null +++ b/src/app/(admin)/admin/awards/[id]/edit/page.tsx @@ -0,0 +1,232 @@ +'use client' + +import { use, useState, useEffect } from 'react' +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { trpc } from '@/lib/trpc/client' +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Switch } from '@/components/ui/switch' +import { Skeleton } from '@/components/ui/skeleton' +import { toast } from 'sonner' +import { ArrowLeft, Save, Loader2 } from 'lucide-react' + +export default function EditAwardPage({ + params, +}: { + params: Promise<{ id: string }> +}) { + const { id: awardId } = use(params) + const router = useRouter() + + const { data: award, isLoading } = trpc.specialAward.get.useQuery({ id: awardId }) + const updateAward = trpc.specialAward.update.useMutation() + + const [name, setName] = useState('') + const [description, setDescription] = useState('') + const [criteriaText, setCriteriaText] = useState('') + const [scoringMode, setScoringMode] = useState<'PICK_WINNER' | 'RANKED' | 'SCORED'>('PICK_WINNER') + const [useAiEligibility, setUseAiEligibility] = useState(true) + const [maxRankedPicks, setMaxRankedPicks] = useState('3') + + // Load existing values when award data arrives + useEffect(() => { + if (award) { + setName(award.name) + setDescription(award.description || '') + setCriteriaText(award.criteriaText || '') + setScoringMode(award.scoringMode as 'PICK_WINNER' | 'RANKED' | 'SCORED') + setUseAiEligibility(award.useAiEligibility) + setMaxRankedPicks(String(award.maxRankedPicks || 3)) + } + }, [award]) + + const handleSubmit = async () => { + if (!name.trim()) return + try { + await updateAward.mutateAsync({ + id: awardId, + name: name.trim(), + description: description.trim() || undefined, + criteriaText: criteriaText.trim() || undefined, + useAiEligibility, + scoringMode, + maxRankedPicks: scoringMode === 'RANKED' ? parseInt(maxRankedPicks) : undefined, + }) + toast.success('Award updated') + router.push(`/admin/awards/${awardId}`) + } catch (error) { + toast.error( + error instanceof Error ? error.message : 'Failed to update award' + ) + } + } + + if (isLoading) { + return ( +
+ Update award settings and eligibility criteria +
++ Use AI to automatically evaluate project eligibility based on the criteria above. + Turn off for awards decided by feeling or manual selection. +
+