diff --git a/.gitignore b/.gitignore index 3601c4b..33aac76 100644 --- a/.gitignore +++ b/.gitignore @@ -143,3 +143,5 @@ Thumbs.db # Prisma prisma/.env +# direnv +.direnv \ No newline at end of file diff --git a/flake.nix b/flake.nix index fdcca78..f3ddb9b 100644 --- a/flake.nix +++ b/flake.nix @@ -19,7 +19,6 @@ # Core dependencies nodejs_20 nodePackages.typescript - nodePackages.tsx podman podman-compose direnv @@ -53,4 +52,4 @@ }; }; }); -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 05bbe49..3dc35d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pgt-web-app", - "version": "0.1.1", + "version": "0.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pgt-web-app", - "version": "0.1.1", + "version": "0.1.3", "dependencies": { "@prisma/client": "^5.22.0", "@radix-ui/react-accordion": "^1.2.1", diff --git a/package.json b/package.json index d6aa74f..11896b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pgt-web-app", - "version": "0.1.6", + "version": "0.1.7", "private": true, "type": "module", "scripts": { diff --git a/prisma/migrations/20241126154717_add_submission_phase/migration.sql b/prisma/migrations/20241126154717_add_submission_phase/migration.sql new file mode 100644 index 0000000..e990124 --- /dev/null +++ b/prisma/migrations/20241126154717_add_submission_phase/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +CREATE TABLE "SubmissionPhase" ( + "id" UUID NOT NULL, + "fundingRoundId" UUID NOT NULL, + "startDate" TIMESTAMPTZ(6) NOT NULL, + "endDate" TIMESTAMPTZ(6) NOT NULL, + "createdAt" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "SubmissionPhase_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "SubmissionPhase_fundingRoundId_key" ON "SubmissionPhase"("fundingRoundId"); + +-- CreateIndex +CREATE INDEX "SubmissionPhase_startDate_endDate_idx" ON "SubmissionPhase"("startDate", "endDate"); + +-- AddForeignKey +ALTER TABLE "SubmissionPhase" ADD CONSTRAINT "SubmissionPhase_fundingRoundId_fkey" FOREIGN KEY ("fundingRoundId") REFERENCES "FundingRound"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8eadf28..8905d80 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -153,6 +153,7 @@ model FundingRound { topic Topic @relation(fields: [topicId], references: [id]) proposals Proposal[] + submissionPhase SubmissionPhase? considerationPhase ConsiderationPhase? deliberationPhase DeliberationPhase? votingPhase VotingPhase? @@ -201,6 +202,19 @@ model VotingPhase { @@index([startDate, endDate]) } +model SubmissionPhase { + id String @id @default(uuid()) @db.Uuid + fundingRoundId String @unique @db.Uuid + startDate DateTime @db.Timestamptz(6) + endDate DateTime @db.Timestamptz(6) + createdAt DateTime @default(now()) @db.Timestamptz(6) + updatedAt DateTime @updatedAt @db.Timestamptz(6) + + fundingRound FundingRound @relation(fields: [fundingRoundId], references: [id], onDelete: Cascade) + + @@index([startDate, endDate]) +} + model AdminUser { id String @id @default(uuid()) @db.Uuid userId String @unique @db.Uuid diff --git a/src/app/api/admin/funding-rounds/[id]/route.ts b/src/app/api/admin/funding-rounds/[id]/route.ts index 599a068..f8aa136 100644 --- a/src/app/api/admin/funding-rounds/[id]/route.ts +++ b/src/app/api/admin/funding-rounds/[id]/route.ts @@ -6,6 +6,23 @@ import { validatePhaseDates } from "@/lib/validation"; const adminService = new AdminService(prisma); +interface DateRange { + from: string; + to: string; +} + +interface FundingRoundRequestData { + name: string; + description: string; + topicId: string; + totalBudget: number; + fundingRoundDates: DateRange; + submissionDates: DateRange; + considerationDates: DateRange; + deliberationDates: DateRange; + votingDates: DateRange; +} + interface RouteContext { params: Promise<{ id: string; @@ -58,26 +75,37 @@ export async function PUT(request: Request, context: RouteContext) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } - const data = await request.json(); + const data: FundingRoundRequestData = await request.json(); + + // Convert string dates to Date objects for validation + const fundingRoundDates = { + from: new Date(data.fundingRoundDates.from), + to: new Date(data.fundingRoundDates.to), + }; + const submissionDates = { + from: new Date(data.submissionDates.from), + to: new Date(data.submissionDates.to), + }; + const considerationDates = { + from: new Date(data.considerationDates.from), + to: new Date(data.considerationDates.to), + }; + const deliberationDates = { + from: new Date(data.deliberationDates.from), + to: new Date(data.deliberationDates.to), + }; + const votingDates = { + from: new Date(data.votingDates.from), + to: new Date(data.votingDates.to), + }; // Validate phase dates const datesValid = validatePhaseDates({ - fundingRound: { - from: new Date(data.fundingRoundDates.from), - to: new Date(data.fundingRoundDates.to), - }, - consideration: { - from: new Date(data.considerationDates.from), - to: new Date(data.considerationDates.to), - }, - deliberation: { - from: new Date(data.deliberationDates.from), - to: new Date(data.deliberationDates.to), - }, - voting: { - from: new Date(data.votingDates.from), - to: new Date(data.votingDates.to), - }, + fundingRound: fundingRoundDates, + submission: submissionDates, + consideration: considerationDates, + deliberation: deliberationDates, + voting: votingDates, }); if (!datesValid.valid) { @@ -85,10 +113,18 @@ export async function PUT(request: Request, context: RouteContext) { } const round = await adminService.updateFundingRound( - ( - await context.params - ).id, - data + (await context.params).id, + { + name: data.name, + description: data.description, + topicId: data.topicId, + totalBudget: data.totalBudget, + fundingRoundDates, + submissionDates, + considerationDates, + deliberationDates, + votingDates, + } ); return NextResponse.json(round); diff --git a/src/app/api/admin/funding-rounds/route.ts b/src/app/api/admin/funding-rounds/route.ts index 023dcd7..3661caf 100644 --- a/src/app/api/admin/funding-rounds/route.ts +++ b/src/app/api/admin/funding-rounds/route.ts @@ -4,6 +4,23 @@ import prisma from "@/lib/prisma"; import { getUserFromRequest } from "@/lib/auth"; import { validatePhaseDates } from "@/lib/validation"; +interface DateRange { + from: string; + to: string; +} + +interface FundingRoundRequestData { + name: string; + description: string; + topicId: string; + totalBudget: number; + fundingRoundDates: DateRange; + submissionDates: DateRange; + considerationDates: DateRange; + deliberationDates: DateRange; + votingDates: DateRange; +} + const adminService = new AdminService(prisma); export async function GET(req: Request) { @@ -41,26 +58,37 @@ export async function POST(req: Request) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } - const data = await req.json(); + const data: FundingRoundRequestData = await req.json(); + + // Convert string dates to Date objects for validation + const fundingRoundDates = { + from: new Date(data.fundingRoundDates.from), + to: new Date(data.fundingRoundDates.to), + }; + const submissionDates = { + from: new Date(data.submissionDates.from), + to: new Date(data.submissionDates.to), + }; + const considerationDates = { + from: new Date(data.considerationDates.from), + to: new Date(data.considerationDates.to), + }; + const deliberationDates = { + from: new Date(data.deliberationDates.from), + to: new Date(data.deliberationDates.to), + }; + const votingDates = { + from: new Date(data.votingDates.from), + to: new Date(data.votingDates.to), + }; // Validate phase dates const datesValid = validatePhaseDates({ - fundingRound: { - from: new Date(data.fundingRoundDates.from), - to: new Date(data.fundingRoundDates.to), - }, - consideration: { - from: new Date(data.considerationDates.from), - to: new Date(data.considerationDates.to), - }, - deliberation: { - from: new Date(data.deliberationDates.from), - to: new Date(data.deliberationDates.to), - }, - voting: { - from: new Date(data.votingDates.from), - to: new Date(data.votingDates.to), - }, + fundingRound: fundingRoundDates, + submission: submissionDates, + consideration: considerationDates, + deliberation: deliberationDates, + voting: votingDates, }); if (!datesValid.valid) { @@ -68,8 +96,16 @@ export async function POST(req: Request) { } const round = await adminService.createFundingRound({ - ...data, + name: data.name, + description: data.description, + topicId: data.topicId, + totalBudget: data.totalBudget, createdById: user.id, + fundingRoundDates, + submissionDates, + considerationDates, + deliberationDates, + votingDates, }); return NextResponse.json(round); } catch (error) { diff --git a/src/components/CreateEditFundingRound.tsx b/src/components/CreateEditFundingRound.tsx index 29ade0b..3a21765 100644 --- a/src/components/CreateEditFundingRound.tsx +++ b/src/components/CreateEditFundingRound.tsx @@ -44,6 +44,10 @@ interface FundingRound { totalBudget: number; startDate: string; endDate: string; + submissionPhase: { + startDate: string; + endDate: string; + }; considerationPhase: { startDate: string; endDate: string; @@ -59,12 +63,13 @@ interface FundingRound { } type DatePhase = { - key: 'fundingRoundDates' | 'considerationDates' | 'deliberationDates' | 'votingDates'; + key: 'fundingRoundDates' | 'submissionDates' | 'considerationDates' | 'deliberationDates' | 'votingDates'; label: string; }; const DATE_PHASES: DatePhase[] = [ { key: 'fundingRoundDates', label: "Funding Round Period" }, + { key: 'submissionDates', label: "Submission Phase (UTC)" }, { key: 'considerationDates', label: "Consideration Phase (UTC)" }, { key: 'deliberationDates', label: "Deliberation Phase (UTC)" }, { key: 'votingDates', label: "Voting Phase (UTC)" } @@ -87,6 +92,7 @@ export function AddEditFundingRoundComponent({ totalBudget: "", selectedTopic: null as Topic | null, fundingRoundDates: { from: null as Date | null, to: null as Date | null }, + submissionDates: { from: null as Date | null, to: null as Date | null }, considerationDates: { from: null as Date | null, to: null as Date | null }, deliberationDates: { from: null as Date | null, to: null as Date | null }, votingDates: { from: null as Date | null, to: null as Date | null }, @@ -107,6 +113,21 @@ export function AddEditFundingRoundComponent({ const roundResponse = await fetch(`/api/admin/funding-rounds/${roundId}`); if (!roundResponse.ok) throw new Error('Failed to fetch funding round'); const round: FundingRound = await roundResponse.json(); + + // Check for missing phases and show warnings + const missingPhases = []; + if (!round.submissionPhase) missingPhases.push('Submission'); + if (!round.considerationPhase) missingPhases.push('Consideration'); + if (!round.deliberationPhase) missingPhases.push('Deliberation'); + if (!round.votingPhase) missingPhases.push('Voting'); + + if (missingPhases.length > 0) { + toast({ + title: "⚠️ Warning", + description: `Missing phase data for: ${missingPhases.join(', ')}. Please set the dates manually.`, + variant: "default", + }); + } setFormData({ name: round.name, @@ -117,18 +138,22 @@ export function AddEditFundingRoundComponent({ from: new Date(round.startDate), to: new Date(round.endDate), }, - considerationDates: { + submissionDates: round.submissionPhase ? { + from: new Date(round.submissionPhase.startDate), + to: new Date(round.submissionPhase.endDate), + } : { from: null, to: null }, + considerationDates: round.considerationPhase ? { from: new Date(round.considerationPhase.startDate), to: new Date(round.considerationPhase.endDate), - }, - deliberationDates: { + } : { from: null, to: null }, + deliberationDates: round.deliberationPhase ? { from: new Date(round.deliberationPhase.startDate), to: new Date(round.deliberationPhase.endDate), - }, - votingDates: { + } : { from: null, to: null }, + votingDates: round.votingPhase ? { from: new Date(round.votingPhase.startDate), to: new Date(round.votingPhase.endDate), - }, + } : { from: null, to: null }, }); } } catch (error) { @@ -201,6 +226,7 @@ export function AddEditFundingRoundComponent({ // Check if all dates are set const dateFields = [ 'fundingRoundDates', + 'submissionDates', 'considerationDates', 'deliberationDates', 'votingDates', @@ -221,6 +247,8 @@ export function AddEditFundingRoundComponent({ const allDatesPresent = formData.fundingRoundDates.from && formData.fundingRoundDates.to && + formData.submissionDates.from && + formData.submissionDates.to && formData.considerationDates.from && formData.considerationDates.to && formData.deliberationDates.from && @@ -243,6 +271,10 @@ export function AddEditFundingRoundComponent({ from: formData.fundingRoundDates.from as Date, to: formData.fundingRoundDates.to as Date, }, + submission: { + from: formData.submissionDates.from as Date, + to: formData.submissionDates.to as Date, + }, consideration: { from: formData.considerationDates.from as Date, to: formData.considerationDates.to as Date, @@ -293,6 +325,10 @@ export function AddEditFundingRoundComponent({ from: formData.fundingRoundDates.from!.toISOString(), to: formData.fundingRoundDates.to!.toISOString(), }, + submissionDates: { + from: formData.submissionDates.from!.toISOString(), + to: formData.submissionDates.to!.toISOString(), + }, considerationDates: { from: formData.considerationDates.from!.toISOString(), to: formData.considerationDates.to!.toISOString(), diff --git a/src/components/FundingRoundStatus.tsx b/src/components/FundingRoundStatus.tsx index 80c9350..f5120e8 100644 --- a/src/components/FundingRoundStatus.tsx +++ b/src/components/FundingRoundStatus.tsx @@ -24,6 +24,10 @@ interface FundingRound { endDate: string; totalBudget: string; proposals: Proposal[]; + submissionPhase: { + startDate: string; + endDate: string; + }; considerationPhase: { startDate: string; endDate: string; @@ -38,7 +42,7 @@ interface FundingRound { }; } -type Phase = 'submit' | 'consider' | 'deliberate' | 'vote'; +type Phase = 'submission' | 'consider' | 'deliberate' | 'vote'; interface Props { onRoundSelect?: (round: { id: string; name: string } | null) => void; @@ -97,7 +101,7 @@ export function FundingRoundStatus({ onRoundSelect }: Props) { now <= new Date(round.votingPhase.endDate)) { return 'vote'; } - return 'submit'; + return 'submission'; }; const getTimeRemainingWithEmoji = (date: Date): { text: string; emoji: string } => { @@ -166,8 +170,8 @@ export function FundingRoundStatus({ onRoundSelect }: Props) { ; } - const currentPhase = selectedRound ? getCurrentPhase(selectedRound) : 'submit' as Phase; - const phases: Phase[] = ['submit', 'consider', 'deliberate', 'vote']; + const currentPhase = selectedRound ? getCurrentPhase(selectedRound) : 'submission' as Phase; + const phases: Phase[] = ['submission', 'consider', 'deliberate', 'vote']; return (