"use client"; import { useSearchParams, useRouter, usePathname } from "next/navigation"; import { useCallback, useMemo } from "react"; import { ComponentGroup } from "./component-group"; import { ReportFilters } from "./report-filters"; import { ReviewedSection } from "./reviewed-section"; import type { Finding, FindingComment, FindingReview, FindingStatus, DesignGraph, Collaborator } from "@/lib/types"; import { groupBy, getFindingKey } from "@/lib/utils"; interface FindingsListProps { findings: Finding[]; graph: DesignGraph; onViewReference: (finding: Finding) => void; projectId: string; isReviewed: (key: string) => boolean; toggleReviewed: (key: string) => void; comments?: Record; collaborators?: Collaborator[]; currentUserId?: string; currentUserName?: string; onCommentAdded?: (comment: FindingComment) => void; onCommentDeleted?: (commentId: string, findingId: string) => void; onReportFinding?: (finding: Finding) => void; reportedFindingIds?: Set; reviews?: Record; onReviewSaved?: (findingId: string, review: FindingReview) => void; } export function FindingsList({ findings, graph, onViewReference, projectId, isReviewed, toggleReviewed, comments, collaborators, currentUserId, currentUserName, onCommentAdded, onCommentDeleted, onReportFinding, reportedFindingIds, reviews, onReviewSaved }: FindingsListProps) { const searchParams = useSearchParams(); const router = useRouter(); const pathname = usePathname(); const findingKeyMap = useMemo(() => { const map = new Map(); findings.forEach((f, i) => map.set(f, getFindingKey(f, i))); return map; }, [findings]); const statusParam = searchParams.get("status"); const componentParam = searchParams.get("component"); const reviewParam = searchParams.get("review"); const searchParam = searchParams.get("q") ?? ""; const domainParam = searchParams.get("domain"); const statusFilters = useMemo(() => { if (!statusParam) return new Set(["ERROR", "WARNING", "INFO"]); return new Set(statusParam.split(",") as FindingStatus[]); }, [statusParam]); const updateParams = useCallback( (updates: Record) => { const params = new URLSearchParams(searchParams.toString()); for (const [key, value] of Object.entries(updates)) { if (value === null || value === "") params.delete(key); else params.set(key, value); } router.replace(`${pathname}?${params.toString()}`, { scroll: false }); }, [searchParams, router, pathname] ); const toggleStatus = useCallback( (status: FindingStatus) => { const next = new Set(statusFilters); if (next.has(status)) next.delete(status); else next.add(status); const isDefault = next.size === 3 && next.has("ERROR") && next.has("WARNING") && next.has("INFO"); const value = isDefault ? null : Array.from(next).join(","); updateParams({ status: value }); }, [statusFilters, updateParams] ); const matchesFilters = useCallback( (f: Finding) => { if (!statusFilters.has(f.status)) return false; if (componentParam && componentParam !== "all" && f.designator !== componentParam) return false; const q = searchParam.toLowerCase(); if (q && !f.finding.toLowerCase().includes(q) && !(f.why ?? "").toLowerCase().includes(q)) return false; if (reviewParam === "open") { const st = f.finding_id ? reviews?.[f.finding_id]?.state : undefined; if (st && st !== "open") return false; } if (domainParam === "layout") { const layout = (f.finding_id || "").startsWith("PCB-") || f.source === "pcb_review" || (f.rule_id || "").startsWith("PE-PLC") || (f.rule_id || "").startsWith("PE-LAY") || (f.rule_id || "").startsWith("PE-SI") || (f.rule_id || "").startsWith("PE-DRT"); if (!layout) return false; } else if (domainParam === "schema") { const layout = (f.finding_id || "").startsWith("PCB-"); if (layout) return false; } return true; }, [statusFilters, componentParam, searchParam, reviewParam, reviews, domainParam] ); const filtered = useMemo(() => { return findings.filter((f) => { if (!matchesFilters(f)) return false; const key = findingKeyMap.get(f)!; return !isReviewed(key); }); }, [findings, matchesFilters, findingKeyMap, isReviewed]); const reviewedFindings = useMemo(() => { return findings.filter((f) => { if (!matchesFilters(f)) return false; const key = findingKeyMap.get(f)!; return isReviewed(key); }); }, [findings, matchesFilters, findingKeyMap, isReviewed]); const grouped = useMemo(() => groupBy(filtered, (f) => f.designator), [filtered]); const designators = useMemo(() => { const all = [...new Set(findings.map((f) => f.designator))]; const byDesignator = groupBy(findings, (f) => f.designator); const worst = (d: string): number => { const group = byDesignator[d] ?? []; if (group.some((f) => f.status === "ERROR")) return 0; if (group.some((f) => f.status === "WARNING")) return 1; return 2; }; return all.sort((a, b) => worst(a) - worst(b) || a.localeCompare(b)); }, [findings]); return (
updateParams({ component: v === "all" ? null : v })} search={searchParam} onSearchChange={(v) => updateParams({ q: v || null })} needsReview={reviewParam === "open"} onToggleNeedsReview={() => updateParams({ review: reviewParam === "open" ? null : "open" }) } domain={domainParam ?? "all"} onDomainChange={(v) => updateParams({ domain: v })} designators={designators} />
{designators .filter((d) => grouped[d]) .map((d) => ( ))} {filtered.length === 0 && reviewedFindings.length === 0 && findings.length > 0 && (

No findings match your filters.

)}
{reviewedFindings.length > 0 && ( )}
); }