From 8fef7ab26eee1cda29188c0ee8164aaa8ab34783 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Fri, 17 Apr 2026 13:49:33 +0000 Subject: [PATCH] added onboaridng journey flow for address2uprn --- .devcontainer/devcontainer.json | 2 +- src/app/api/tasks/[taskId]/route.ts | 44 +++++++++------ src/app/api/tasks/route.ts | 54 +++++++++++++++++++ src/app/db/migrations/meta/_journal.json | 7 +++ src/app/db/schema/bulk_address_uploads.ts | 1 + .../bulk-upload/[uploadId]/page.tsx | 33 ++++++++++-- 6 files changed, 121 insertions(+), 20 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d8c62f0..36d8b4d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,7 +9,7 @@ "appPort": ["3000:3000"], # For devcontainer shell "mounts": [ // Optional, just makes getting from Downloads (local env) easier - // "source=${localEnv:HOME},target=/workspaces/home,type=bind" + "source=${localEnv:HOME},target=/workspaces/home,type=bind" ], "customizations": { "vscode": { diff --git a/src/app/api/tasks/[taskId]/route.ts b/src/app/api/tasks/[taskId]/route.ts index 8c43f4f..9eaa1ed 100644 --- a/src/app/api/tasks/[taskId]/route.ts +++ b/src/app/api/tasks/[taskId]/route.ts @@ -1,26 +1,40 @@ import { db } from "@/app/db/db"; +import { tasks } from "@/app/db/schema/tasks/tasks"; import { subTasks } from "@/app/db/schema/tasks/subtask"; -import { eq } from "drizzle-orm"; +import { eq, count, sql } from "drizzle-orm"; import { NextRequest, NextResponse } from "next/server"; export async function GET( - request: NextRequest, + _request: NextRequest, { params }: { params: Promise<{ taskId: string }> } ) { - try { - const { taskId } = await params; - const taskSubTasks = await db - .select() - .from(subTasks) - .where(eq(subTasks.taskId, taskId)) - .orderBy(subTasks.updatedAt); + const { taskId } = await params; - return NextResponse.json(taskSubTasks); + try { + const [row] = await db + .select({ + id: tasks.id, + taskSource: tasks.taskSource, + status: tasks.status, + service: tasks.service, + jobStarted: tasks.jobStarted, + jobCompleted: tasks.jobCompleted, + updatedAt: tasks.updatedAt, + totalSubtasks: count(subTasks.id), + completedSubtasks: sql`count(case when lower(${subTasks.status}) in ('completed', 'complete') then 1 end)::int`, + failedSubtasks: sql`count(case when lower(${subTasks.status}) in ('failed', 'failure', 'error') then 1 end)::int`, + }) + .from(tasks) + .leftJoin(subTasks, eq(subTasks.taskId, tasks.id)) + .where(eq(tasks.id, taskId)) + .groupBy(tasks.id) + .limit(1); + + if (!row) return NextResponse.json({ error: "Not found" }, { status: 404 }); + + return NextResponse.json(row); } catch (error) { - console.error("Error fetching subtasks:", error); - return NextResponse.json( - { error: "Failed to fetch subtasks" }, - { status: 500 } - ); + console.error("Error fetching task:", error); + return NextResponse.json({ error: "Failed to fetch task" }, { status: 500 }); } } diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index d11a239..dc6e830 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -1,7 +1,61 @@ import { db } from "@/app/db/db"; import { tasks } from "@/app/db/schema/tasks/tasks"; +import { subTasks } from "@/app/db/schema/tasks/subtask"; import { desc, count } from "drizzle-orm"; import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { AuthOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { z } from "zod"; + +const CreateTaskSchema = z.object({ + taskSource: z.string().min(1), + service: z.string().optional(), + source: z.literal("portfolio_id").optional(), + sourceId: z.string().optional(), + inputs: z.record(z.unknown()).optional(), +}); + +export async function POST(request: NextRequest) { + const session = await getServerSession(AuthOptions); + if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + + let body; + try { + body = CreateTaskSchema.parse(await request.json()); + } catch { + return NextResponse.json({ error: "Invalid input" }, { status: 400 }); + } + + try { + const now = new Date(); + + const [task] = await db + .insert(tasks) + .values({ + taskSource: body.taskSource, + service: body.service, + source: body.source, + sourceId: body.sourceId, + status: "waiting", + jobStarted: now, + }) + .returning(); + + const [subTask] = await db + .insert(subTasks) + .values({ + taskId: task.id, + status: "waiting", + inputs: body.inputs ? JSON.stringify(body.inputs) : null, + }) + .returning(); + + return NextResponse.json({ taskId: task.id, subTaskId: subTask.id }, { status: 201 }); + } catch (error) { + console.error("Failed to create task:", error); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } +} export async function GET(request: NextRequest) { try { diff --git a/src/app/db/migrations/meta/_journal.json b/src/app/db/migrations/meta/_journal.json index 721ae1c..83a1494 100644 --- a/src/app/db/migrations/meta/_journal.json +++ b/src/app/db/migrations/meta/_journal.json @@ -1205,6 +1205,13 @@ "when": 1776361262258, "tag": "0171_chunky_wallow", "breakpoints": true + }, + { + "idx": 172, + "version": "7", + "when": 1776900000000, + "tag": "0172_bulk_upload_task_id", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/db/schema/bulk_address_uploads.ts b/src/app/db/schema/bulk_address_uploads.ts index 082ca8b..1bacb10 100644 --- a/src/app/db/schema/bulk_address_uploads.ts +++ b/src/app/db/schema/bulk_address_uploads.ts @@ -11,6 +11,7 @@ export const bulkAddressUploads = pgTable("bulk_address_uploads", { status: text("status").notNull().default("ready_for_processing"), sourceHeaders: text("source_headers").array().notNull().default(sql`'{}'`), columnMapping: jsonb("column_mapping").$type>(), + taskId: uuid("task_id"), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }) .notNull() diff --git a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/page.tsx b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/page.tsx index 3955a57..0c35f76 100644 --- a/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/page.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/bulk-upload/[uploadId]/page.tsx @@ -15,6 +15,8 @@ import { ExclamationCircleIcon, ArrowPathIcon, } from "@heroicons/react/24/outline"; +import StartOnboardingButton from "./StartOnboardingButton"; +import OnboardingProgress from "./OnboardingProgress"; function formatDate(date: Date) { return new Intl.DateTimeFormat("en-GB", { @@ -39,9 +41,9 @@ const STATUS_CONFIG = { icon: CheckCircleIcon, iconBg: "bg-blue-50", iconColor: "text-blue-500", - title: "Queued for processing", - body: "Column mapping saved. Your file is queued and will be processed shortly.", - cta: false, + title: "Mapping complete", + body: "Column mapping saved. Start onboarding to begin matching your addresses to UPRNs.", + cta: true, }, processing: { icon: ArrowPathIcon, @@ -119,7 +121,8 @@ export default async function BulkUploadDetailPage(props: {

{config.title}

{config.body}

- {config.cta && ( + + {statusKey === "ready_for_processing" && ( )} + + {statusKey === "mapping_complete" && ( +
+ + Edit column mapping + + + +
+ )} + + {(statusKey === "processing" || statusKey === "complete" || statusKey === "failed") && + upload.taskId && ( + + )}