mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
get rid of useeffect
This commit is contained in:
parent
c0954e50d8
commit
a7eef6db85
4 changed files with 136 additions and 92 deletions
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useState, useSyncExternalStore } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
|
|
@ -26,10 +26,126 @@ interface Props {
|
|||
isDomnaUser: boolean;
|
||||
}
|
||||
|
||||
interface Snapshot {
|
||||
data: TaskData | null;
|
||||
uploadStatus: UploadStatus | null;
|
||||
fetchError: boolean;
|
||||
finalizeError: string | null;
|
||||
}
|
||||
|
||||
const TERMINAL_STATUSES = new Set(["complete", "completed", "failed", "failure", "error"]);
|
||||
const FAILED_STATUSES = new Set(["failed", "failure", "error"]);
|
||||
const FINAL_UPLOAD_STATUSES = new Set(["complete", "needs_review"]);
|
||||
|
||||
function createProgressStore(args: {
|
||||
taskId: string;
|
||||
portfolioId: string;
|
||||
uploadId: string;
|
||||
onComplete: () => void;
|
||||
}) {
|
||||
let snapshot: Snapshot = {
|
||||
data: null,
|
||||
uploadStatus: null,
|
||||
fetchError: false,
|
||||
finalizeError: null,
|
||||
};
|
||||
const listeners = new Set<() => void>();
|
||||
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||
let combineFired = false;
|
||||
let finalizeFired = false;
|
||||
let refreshed = false;
|
||||
|
||||
function emit(patch: Partial<Snapshot>) {
|
||||
snapshot = { ...snapshot, ...patch };
|
||||
listeners.forEach((l) => l());
|
||||
}
|
||||
|
||||
async function fireFinalize() {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/portfolio/${args.portfolioId}/bulk-uploads/${args.uploadId}/finalize`,
|
||||
{ method: "POST" }
|
||||
);
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
const msg =
|
||||
body?.detail || body?.error || `Finalize failed (${res.status})`;
|
||||
emit({ finalizeError: msg });
|
||||
finalizeFired = false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to trigger finalize:", err);
|
||||
emit({ finalizeError: err instanceof Error ? err.message : "Network error" });
|
||||
finalizeFired = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${args.taskId}/summary`);
|
||||
if (!res.ok) { emit({ fetchError: true }); return; }
|
||||
const json: TaskData = await res.json();
|
||||
emit({ data: json });
|
||||
const status = json.status.toLowerCase();
|
||||
|
||||
if (TERMINAL_STATUSES.has(status)) {
|
||||
if (!FAILED_STATUSES.has(status) && !combineFired) {
|
||||
combineFired = true;
|
||||
fetch(`/api/portfolio/${args.portfolioId}/bulk-uploads/${args.uploadId}/combine`, {
|
||||
method: "POST",
|
||||
}).catch((err) => console.error("Failed to trigger combiner:", err));
|
||||
}
|
||||
|
||||
const uploadRes = await fetch(
|
||||
`/api/portfolio/${args.portfolioId}/bulk-uploads/${args.uploadId}`
|
||||
);
|
||||
if (uploadRes.ok) {
|
||||
const upload: UploadStatus = await uploadRes.json();
|
||||
emit({ uploadStatus: upload });
|
||||
|
||||
if (upload.status === "awaiting_review" && !finalizeFired) {
|
||||
finalizeFired = true;
|
||||
fireFinalize();
|
||||
}
|
||||
|
||||
if (FINAL_UPLOAD_STATUSES.has(upload.status) && !refreshed) {
|
||||
refreshed = true;
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
args.onComplete();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
emit({ fetchError: true });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe(listener: () => void) {
|
||||
listeners.add(listener);
|
||||
if (listeners.size === 1 && intervalId === null && !refreshed) {
|
||||
poll();
|
||||
intervalId = setInterval(poll, 3000);
|
||||
}
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
if (listeners.size === 0 && intervalId !== null) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
};
|
||||
},
|
||||
getSnapshot: () => snapshot,
|
||||
retryFinalize() {
|
||||
emit({ finalizeError: null });
|
||||
finalizeFired = true;
|
||||
fireFinalize();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function OnboardingProgress({
|
||||
taskId,
|
||||
portfolioSlug,
|
||||
|
|
@ -38,88 +154,16 @@ export default function OnboardingProgress({
|
|||
isDomnaUser,
|
||||
}: Props) {
|
||||
const router = useRouter();
|
||||
const [data, setData] = useState<TaskData | null>(null);
|
||||
const [uploadStatus, setUploadStatus] = useState<UploadStatus | null>(null);
|
||||
const [fetchError, setFetchError] = useState(false);
|
||||
const [finalizeError, setFinalizeError] = useState<string | null>(null);
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const combineFiredRef = useRef(false);
|
||||
const finalizeFiredRef = useRef(false);
|
||||
const refreshedRef = useRef(false);
|
||||
|
||||
async function fireFinalize() {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/portfolio/${portfolioId}/bulk-uploads/${uploadId}/finalize`,
|
||||
{ method: "POST" }
|
||||
);
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
const msg =
|
||||
body?.detail || body?.error || `Finalize failed (${res.status})`;
|
||||
setFinalizeError(msg);
|
||||
finalizeFiredRef.current = false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to trigger finalize:", err);
|
||||
setFinalizeError(err instanceof Error ? err.message : "Network error");
|
||||
finalizeFiredRef.current = false;
|
||||
}
|
||||
}
|
||||
|
||||
function retryFinalize() {
|
||||
setFinalizeError(null);
|
||||
finalizeFiredRef.current = true;
|
||||
fireFinalize();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function poll() {
|
||||
try {
|
||||
const res = await fetch(`/api/tasks/${taskId}/summary`);
|
||||
if (!res.ok) { setFetchError(true); return; }
|
||||
const json: TaskData = await res.json();
|
||||
setData(json);
|
||||
const status = json.status.toLowerCase();
|
||||
|
||||
if (TERMINAL_STATUSES.has(status)) {
|
||||
if (!FAILED_STATUSES.has(status) && !combineFiredRef.current) {
|
||||
combineFiredRef.current = true;
|
||||
fetch(`/api/portfolio/${portfolioId}/bulk-uploads/${uploadId}/combine`, {
|
||||
method: "POST",
|
||||
}).catch((err) => console.error("Failed to trigger combiner:", err));
|
||||
}
|
||||
|
||||
const uploadRes = await fetch(
|
||||
`/api/portfolio/${portfolioId}/bulk-uploads/${uploadId}`
|
||||
);
|
||||
if (uploadRes.ok) {
|
||||
const upload: UploadStatus = await uploadRes.json();
|
||||
setUploadStatus(upload);
|
||||
|
||||
if (upload.status === "awaiting_review" && !finalizeFiredRef.current) {
|
||||
finalizeFiredRef.current = true;
|
||||
fireFinalize();
|
||||
}
|
||||
|
||||
if (FINAL_UPLOAD_STATUSES.has(upload.status) && !refreshedRef.current) {
|
||||
refreshedRef.current = true;
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
router.refresh();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setFetchError(true);
|
||||
}
|
||||
}
|
||||
|
||||
poll();
|
||||
intervalRef.current = setInterval(poll, 3000);
|
||||
return () => { if (intervalRef.current) clearInterval(intervalRef.current); };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [taskId, portfolioId, portfolioSlug, uploadId, router]);
|
||||
const [store] = useState(() =>
|
||||
createProgressStore({
|
||||
taskId,
|
||||
portfolioId,
|
||||
uploadId,
|
||||
onComplete: () => router.refresh(),
|
||||
})
|
||||
);
|
||||
const snap = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
||||
const { data, uploadStatus, fetchError, finalizeError } = snap;
|
||||
|
||||
if (fetchError) return null;
|
||||
if (!data) {
|
||||
|
|
@ -191,7 +235,7 @@ export default function OnboardingProgress({
|
|||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={retryFinalize}
|
||||
onClick={() => store.retryFinalize()}
|
||||
className="shrink-0 px-2 py-1 rounded-md bg-red-600 text-white font-semibold hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Retry
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ interface Props {
|
|||
filename: string;
|
||||
}
|
||||
|
||||
export default function StartOnboardingButton({ portfolioId, uploadId, filename }: Props) {
|
||||
export default function StartAddressMatchingButton({ portfolioId, uploadId, filename }: Props) {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -39,8 +39,8 @@ export default function StartOnboardingButton({ portfolioId, uploadId, filename
|
|||
|
||||
const { taskId, subTaskId } = await taskRes.json();
|
||||
|
||||
const onboardRes = await fetch(
|
||||
`/api/portfolio/${portfolioId}/bulk-uploads/${uploadId}/onboard`,
|
||||
const matchRes = await fetch(
|
||||
`/api/portfolio/${portfolioId}/bulk-uploads/${uploadId}/start-address-matching`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
|
@ -48,9 +48,9 @@ export default function StartOnboardingButton({ portfolioId, uploadId, filename
|
|||
}
|
||||
);
|
||||
|
||||
if (!onboardRes.ok) {
|
||||
const data = await onboardRes.json().catch(() => ({}));
|
||||
throw new Error(data.error ?? "Failed to start onboarding");
|
||||
if (!matchRes.ok) {
|
||||
const data = await matchRes.json().catch(() => ({}));
|
||||
throw new Error(data.error ?? "Failed to start address matching");
|
||||
}
|
||||
|
||||
router.refresh();
|
||||
|
|
@ -15,7 +15,7 @@ import {
|
|||
ExclamationCircleIcon,
|
||||
ArrowPathIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import StartOnboardingButton from "./StartOnboardingButton";
|
||||
import StartAddressMatchingButton from "./StartAddressMatchingButton";
|
||||
import OnboardingProgress from "./OnboardingProgress";
|
||||
|
||||
function formatDate(date: Date) {
|
||||
|
|
@ -166,7 +166,7 @@ export default async function BulkUploadDetailPage(props: {
|
|||
Edit column mapping
|
||||
<ArrowRightIcon className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
<StartOnboardingButton
|
||||
<StartAddressMatchingButton
|
||||
portfolioId={upload.portfolioId}
|
||||
uploadId={uploadId}
|
||||
filename={upload.filename}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue