mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
added onboaridng journey flow for address2uprn
This commit is contained in:
parent
33d1b42d91
commit
8fef7ab26e
6 changed files with 121 additions and 20 deletions
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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<number>`count(case when lower(${subTasks.status}) in ('completed', 'complete') then 1 end)::int`,
|
||||
failedSubtasks: sql<number>`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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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<Record<string, string>>(),
|
||||
taskId: uuid("task_id"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.notNull()
|
||||
|
|
|
|||
|
|
@ -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: {
|
|||
<div className="flex-1">
|
||||
<p className="text-sm font-semibold text-gray-900 mb-1">{config.title}</p>
|
||||
<p className="text-sm text-gray-500 leading-relaxed">{config.body}</p>
|
||||
{config.cta && (
|
||||
|
||||
{statusKey === "ready_for_processing" && (
|
||||
<Link
|
||||
href={`/portfolio/${slug}/bulk-upload/${uploadId}/map-columns`}
|
||||
className="mt-4 inline-flex items-center gap-2 px-5 py-2 rounded-xl bg-gradient-to-br from-[#14163d] to-[#15173e] text-white text-sm font-bold hover:opacity-90 transition-opacity"
|
||||
|
|
@ -128,6 +131,28 @@ export default async function BulkUploadDetailPage(props: {
|
|||
<ArrowRightIcon className="h-4 w-4" />
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{statusKey === "mapping_complete" && (
|
||||
<div className="mt-4 flex flex-col gap-3">
|
||||
<Link
|
||||
href={`/portfolio/${slug}/bulk-upload/${uploadId}/map-columns`}
|
||||
className="inline-flex items-center gap-1.5 text-sm text-gray-400 hover:text-gray-700 transition-colors"
|
||||
>
|
||||
Edit column mapping
|
||||
<ArrowRightIcon className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
<StartOnboardingButton
|
||||
portfolioId={upload.portfolioId}
|
||||
uploadId={uploadId}
|
||||
filename={upload.filename}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(statusKey === "processing" || statusKey === "complete" || statusKey === "failed") &&
|
||||
upload.taskId && (
|
||||
<OnboardingProgress taskId={upload.taskId} portfolioSlug={slug} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue