1import { c as ua, d as He, r as d, e as Ve, J as ke, K as se, y as ne, j as e, s as De, t as Oe, v as Ie, w as Pe, x as Ee, B as $, ag as Z, cs as Ca, aX as Ae, a9 as qe, z as $t, L as ie, a5 as pe, A as je, aF as sa, ct as wa, cu as Sa, be as ot, a7 as ha, cv as Ma, cw as Ct, cx as ht, cy as st, aU as wt, aG as xa, I as ye, S as St, f as Mt, g as Tt, h as Rt, l as be, M as At, C as ga, a4 as fa, bk as pa, V as We, a$ as Ta, ay as Ra, b0 as Fa, o as Ce, p as we, q as Se, T as ba, bv as $a, bJ as Aa, N as ra } from "./index-B9miRpwC.js"; 2import { T as Dt, a as Ot, b as Ke, c as I, d as It, e as P } from "./table-D-NY6-3m.js"; 3import { A as xt, a as gt, b as ft, c as pt, d as bt, e as jt, f as yt, g as kt } from "./alert-dialog-Cv39x3F5.js"; 4import { C as Da, a as Oa, b as Ia } from "./collapsible-lrkyHkBq.js"; 5import { r as Pa, G as Ea, c as La, e as Ba, s as Ua, f as lt, a as za, C as _a } from "./resolveRearCenterConvention-Ck2Uhhpy.js"; 6import { C as ja } from "./chevrons-right-qnSTlkCF.js"; 7import { H as Ft } from "./history-CEPm62B8.js"; 8import { G as Va, C as Ga } from "./GeometryReportCard-Bn1sTy_a.js"; 9import { R as Pt } from "./refresh-cw-D2_WynW5.js"; 10import { P as qa } from "./play-BHXr4qu-.js"; 11import { L as ya } from "./list-checks-iqZBxtyj.js"; 12import { U as Ge } from "./upload-u0DU9Zyt.js"; 13import { E as ia } from "./eye-3Ma80GoX.js"; 14import { E as na } from "./eye-off-DOl5DV4h.js"; 15import { P as la } from "./pencil-CWIGhVLH.js"; 16import { O as Ka } from "./octagon-alert-DIrZyqAr.js"; 17import { B as Ha } from "./ban-DEw-6ye0.js"; 18import { S as ka } from "./save-fTnHtNzX.js"; 19import "./progress-DFvIGKNt.js"; 20import "./topTubeCalculator-fCM7LY55.js"; 21import "./link-BVyy0oqf.js"; 22import "./package-CQTVcVCl.js"; 23import "./calculator-B2vnHHEJ.js"; 24import "./chevron-left-B-jr5clS.js"; 25import "./file-down-S4Ft3bwC.js"; 26import "./info-CWBPgXHY.js"; 27import "./git-merge-F2YjpL8M.js"; 28import "./minus-NuscadqZ.js"; 29import "./trending-up-Cbn6FDrL.js"; 30/** 31* @license lucide-react v0.453.0 - ISC 32* 33* This source code is licensed under the ISC license. 34* See the LICENSE file in the root directory of this source tree. 35*/ 36const Wa = ua("ChartNoAxesColumn", [["line", { x1: "18", x2: "18", y1: "20", y2: "10", key: "1xfpm4" }], ["line", { x1: "12", x2: "12", y1: "20", y2: "4", key: "be30l9" }], ["line", { x1: "6", x2: "6", y1: "20", y2: "14", key: "1r4le6" }]]); 37/** 38* @license lucide-react v0.453.0 - ISC 39* 40* This source code is licensed under the ISC license. 41* See the LICENSE file in the root directory of this source tree. 42*/ 43const dt = ua("ScanSearch", [["path", { d: "M3 7V5a2 2 0 0 1 2-2h2", key: "aa7l1z" }], ["path", { d: "M17 3h2a2 2 0 0 1 2 2v2", key: "4qcy5o" }], ["path", { d: "M21 17v2a2 2 0 0 1-2 2h-2", key: "6vwrx8" }], ["path", { d: "M7 21H5a2 2 0 0 1-2-2v-2", key: "ioqczr" }], ["circle", { cx: "12", cy: "12", r: "3", key: "1v7zrd" }], ["path", { d: "m16 16-1.9-1.9", key: "1dq9hf" }]]); 44function Nt(t, m) { 45 return (t ?? "").localeCompare(m ?? "", void 0, { sensitivity: "base", numeric: !0 }); 46} 47function Ya(t, m) { 48 const h = t.modelYear ?? Number.NEGATIVE_INFINITY, T = m.modelYear ?? Number.NEGATIVE_INFINITY; 49 if (h !== T) return T - h; 50 const A = Nt(t.brand, m.brand); 51 if (A !== 0) return A; 52 const N = Nt(t.name, m.name); 53 return N !== 0 ? N : Nt(t.id, m.id); 54} 55function oa(t, m = 1) { 56 return t === null ? "\u2014" : t.toFixed(m); 57} 58function Qa(t) { 59 return t.currentValue === null && t.extractedValue !== null ? "NEW" : t.deltaMm === null ? "\u2014" : `${t.extractedValue > t.currentValue ? "+" : "-"}${t.deltaMm.toFixed(1)}`; 60} 61function da(t) { 62 return t === "headTubeAngle" || t === "seatTubeAngle"; 63} 64function Na({ open: t, onOpenChange: m, modelId: h, modelName: T, hasImage: A }) { 65 const { toast: N } = He(), [f, V] = d.useState(!0), [S, R] = d.useState({}), [K, Q] = d.useState(!1), L = d.useRef(null), { data: F, isLoading: X, refetch: H } = Ve({ queryKey: ["/api/admin/competitor-database/models", h, "reaudit", "latest"], queryFn: async () => { 66 const l = await fetch(`/api/admin/competitor-database/models/${h}/reaudit/latest`, { credentials: "include" }); 67 if (!l.ok) throw new Error("Failed to fetch reaudit run"); 68 return l.json(); 69 }, enabled: t, staleTime: 0 }), k = F?.run ?? null, x = F?.diffs ?? [], n = F?.labelViolations ?? [], o = x.map((l) => ({ ...l, decision: S[l.id] ?? l.decision })), u = f ? o.filter((l) => l.flagged) : o, r = u.reduce((l, j) =>
69 (l[j.bikeSize] || (l[j.bikeSize] = []), l[j.bikeSize].push(j), l), {}), p = ke({ mutationFn: () => se("POST", `/api/admin/competitor-database/models/${h}/reaudit`), onSuccess: () => { 70 R({}), H(), N({ title: "Re-audit complete", description: "Diffs are ready for review." }); 71 }, onError: (l) => { 72 N({ title: "Re-audit failed", description: l?.message ?? "Unknown error", variant: "destructive" }); 73 } }), w = d.useCallback(async (l, j) => { 74 if (!k || l.appliedAt) return; 75 const g = S[l.id] ?? l.decision; 76 R((v) => ({ ...v, [l.id]: j })); 77 try { 78 await se("PATCH", `/api/admin/competitor-database/models/${h}/reaudit/${k.id}/decisions`, { decisions: [{ diffId: l.id, decision: j }] }); 79 } catch { 80 R((v) => ({ ...v, [l.id]: g })), N({ title: "Error", description: "Failed to save decision.", variant: "destructive" }); 81 } 82 }, [k, S, h, N]), B = d.useCallback((l) => { 83 const g = (S[l.id] ?? l.decision) === "accepted" ? "pending" : "accepted"; 84 return w(l, g); 85 }, [S, w]), q = d.useCallback((l) => { 86 const g = (S[l.id] ?? l.decision) === "rejected" ? "pending" : "rejected"; 87 return w(l, g); 88 }, [S, w]), M = d.useCallback(async () => { 89 if (!k) return; 90 const j = o.filter((v) => v.flagged && !v.appliedAt).map((v) => ({ diffId: v.id, decision: "accepted" })), g = {}; 91 for (const v of j) g[v.diffId] = "accepted"; 92 R((v) => ({ ...v, ...g })); 93 try { 94 await se("PATCH", `/api/admin/competitor-database/models/${h}/reaudit/${k.id}/decisions`, { decisions: j }), N({ title: "Accepted", description: `${j.length} flagged diffs accepted.` }); 95 } catch { 96 R((v) => { 97 const a = { ...v }; 98 for (const D of j) delete a[D.diffId]; 99 return a; 100 }), N({ title: "Error", description: "Failed to accept diffs.", variant: "destructive" }); 101 } 102 }, [k, o, h, N]), E = d.useCallback(async () => { 103 if (!k) return; 104 const j = o.filter((v) => v.flagged && !v.appliedAt).map((v) => ({ diffId: v.id, decision: "rejected" })), g = {}; 105 for (const v of j) g[v.diffId] = "rejected"; 106 R((v) => ({ ...v, ...g })); 107 try { 108 await se("PATCH", `/api/admin/competitor-database/models/${h}/reaudit/${k.id}/decisions`, { decisions: j }), N({ title: "Declined", description: `${j.length} flagged diffs marked N/A.` }); 109 } catch { 110 R((v) => { 111 const a = { ...v }; 112 for (const D of j) delete a[D.diffId]; 113 return a; 114 }), N({ title: "Error", description: "Failed to decline diffs.", variant: "destructive" }); 115 } 116 }, [k, o, h, N]), U = ke({ mutationFn: async () => (await se("POST", `/api/admin/competitor-database/models/${h}/reaudit/${k.id}/apply`)).json(), onSuccess: (l) => { 117 R({}), H(), ne.invalidateQueries({ predicate: (g) => typeof g.queryKey[0] == "string" && g.queryKey[0].includes("competitor-database") }); 118 const j = l.reprocessed ? "Geometry recalculated automatically." : "Note: geometry recalculation could not run."; 119 N({ title: `${l.applied} field${l.applied !== 1 ? "s" : ""} applied`, description: `${j}${l.skipped > 0 ? ` (${l.skipped} skipped)` : ""}` }); 120 }, onError: (l) => { 121 N({ title: "Apply failed", description: l?.message ?? "Unknown error", variant: "destructive" }); 122 } }), Y = o.filter((l) => l.decision === "accepted" && !l.appliedAt).length, ce = o.filter((l) => l.appliedAt).length, C = o.filter((l) => l.flagged).length, ee = d.useCallback(async (l) => { 123 Q(!0); 124 try { 125 const j = new FormData(); 126 j.append("file", l); 127 const g = await fetch(`/api/admin/competitor-database/models/${h}/geometry-image`, { method: "POST", credentials: "include", body: j }); 128 if (!g.ok) throw new Error((await g.json()).error ?? "Upload failed"); 129 N({ title: "Chart image replaced", description: "Running re-audit with the new image\u2026" }), await se("POST", `/api/admin/competitor-database/models/${h}/reaudit`), R({}), H(), N({ title: "Re-audit complete", description: "Diffs updated from the new chart image." }); 130 } catch (j) { 131 N({ title: "Image replace failed", description: j?.message ?? "Unknown error", variant: "destructive" }); 132 } finally { 133 Q(!1), L.current && (L.current.value = ""); 134 } 135 }, [h, H, N]); 136 return e.jsxs(De, { open: t, onOpenChange: m, children: [e.jsx("input", { ref: L, type: "file", accept: "image/*", className: "hidden", onChange: (l) => { 137 var g; 138 const j = (g = l.target.files) == null ? void 0 : g[0]; 139 j && ee(j); 140 } }), e.jsxs(Oe, { className: "max-w-6xl max-h-[90vh] flex flex-col gap-0 p-0", children: [e.jsxs(Ie, { className: "px-6 pt-6 pb-4 border-b border-border", children: [e.jsxs(Pe, { className: "flex items-center gap-2", children: [e.jsx(dt, { className: "w-5 h-5" }), "Vision Re-Audit \u2014 ", T] }), e.jsx(Ee, { children: "Re-extract geometry from the stored chart image, compare against current DB values, and selectively approve corrections." })] }), e.jsxs("div", { className: "flex items-center justify-between px-6 py-3 border-b border-border gap-4 flex-wrap", children: [e.jsxs("div", { className: "flex items-center gap-3", children: [e.jsxs($, { size: "sm", variant: "outline", onClick: () => p.mutate(), disabled: p.isPending || K || !A, "data-testid": `button-run-reaudit-${h}`, children: [p.isPending ? e.jsx(Z, { className: "w-4 h-4 mr-2 animate-spin" }) : e.jsx(dt, { className: "w-4 h-4 mr-2" }), p.isPending ? "Running\u2026" : k ? "Re-Run" : "Run Re-Audit"] }), e.jsxs($, { size: "sm", variant: "outline", onClick: () => { 141 var l; 142 return (l = L.current) == null ? void 0 : l.click(); 143 }, disabled: p.isPending || K, "data-testid": `button-replace-image-${h}`, title: "Upload a new chart image, then automatically re-run the re-audit", children: [K ? e.jsx(Z, { className: "w-4 h-4 mr-2 animate-spin" }) : e.jsx(Ca, { className: "w-4 h-4 mr-2" }), K ? "Uploading\u2026" : "Replace Image"] }), !A && e.jsx("span", { className: "text-xs text-muted-foreground", children: 'No chart image \u2014 upload one via "Replace Image"' }), k && e.jsx("span", { className: "text-xs text-muted-foreground", children: k.status === "completed" ? `Last run: ${new Date(k.createdAt).toLocaleDateString()} \u2014 ${k.totalDiffs ?? 0} diffs, ${k.flaggedDiffs ?? 0} flagged` : k.status === "failed" ? `Failed: ${k.errorMsg ?? "unknown error"}` : "Running\u2026" })] }), e.jsxs("div", { className: "flex items-center gap-3", children: [C > 0 && e.jsxs($, { size: "sm", variant: "outline", onClick: M, "data-testid": "button-accept-all-flagged", children: [e.jsx(Ae, { className: "w-4 h-4 mr-2 text-amber-500" }), "Accept All Flagged (", C, ")"] }), C > 0 && e.jsxs($, { size: "sm", variant: "outline", onClick: E, "data-testid": "button-decline-
143all-flagged", children: [e.jsx(qe, { className: "w-4 h-4 mr-2 text-muted-foreground" }), "Decline All Flagged (", C, ")"] }), e.jsxs("div", { className: "flex items-center gap-2", children: [e.jsx($t, { id: "flagged-only", checked: f, onCheckedChange: V, "data-testid": "switch-flagged-only" }), e.jsx(ie, { htmlFor: "flagged-only", className: "text-sm whitespace-nowrap", children: "Flagged only" })] }), e.jsxs($, { size: "sm", onClick: () => U.mutate(), disabled: Y === 0 || U.isPending, "data-testid": "button-apply-accepted", children: [U.isPending && e.jsx(Z, { className: "w-4 h-4 mr-2 animate-spin" }), !U.isPending && e.jsx(ja, { className: "w-4 h-4 mr-2" }), U.isPending ? "Applying & Recalculating\u2026" : `Apply Accepted (${Y})`] })] })] }), k && k.status === "completed" && e.jsxs("div", { className: "flex items-center gap-2 px-6 py-2 border-b border-border flex-wrap", children: [e.jsxs(pe, { variant: "outline", children: [k.totalDiffs ?? 0, " diffs total"] }), n.length > 0 && e.jsxs(pe, { className: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300 border-red-300 dark:border-red-700", children: [e.jsx(je, { className: "w-3 h-3 mr-1" }), n.length, " label ", n.length === 1 ? "problem" : "problems"] }), C > 0 && e.jsxs(pe, { className: "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300 border-amber-300 dark:border-amber-700", children: [e.jsx(je, { className: "w-3 h-3 mr-1" }), C, " flagged"] }), Y > 0 && e.jsxs(pe, { className: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300 border-emerald-300 dark:border-emerald-700", children: [e.jsx(Ae, { className: "w-3 h-3 mr-1" }), Y, " accepted"] }), ce > 0 && e.jsxs(pe, { className: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 border-blue-300 dark:border-blue-700", children: [ce, " applied"] })] }), n.length > 0 && e.jsxs("div", { className: "px-6 py-4 border-b border-border bg-red-50/60 dark:bg-red-950/20", children: [e.jsxs("div", { className: "flex items-start gap-2 mb-3", children: [e.jsx(je, { className: "w-4 h-4 text-red-600 dark:text-red-400 mt-0.5 shrink-0" }), e.jsxs("div", { children: [e.jsxs("h3", { className: "text-sm font-semibold text-red-700 dark:text-red-300", children: ["Size label integrity problems (", n.length, ")"] }), e.jsx("p", { className: "text-xs text-red-600/80 dark:text-red-400/80 mt-0.5", children: "These structural problems with the frame size labels are invisible to the geometry diff engine. They must be corrected directly in the database before this model can be re-published." })] })] }), e.jsx("div", { className: "flex flex-col gap-2", children: n.map((l, j) => e.jsxs("div", { className: "rounded-md border border-red-200 dark:border-red-800 bg-white dark:bg-red-950/40 px-3 py-2", children: [e.jsxs("div", { className: "flex items-center gap-2 mb-0.5", children: [e.jsx(pe, { className: l.type === "pinarello_label_mismatch" ? "text-[10px] bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300 border-red-300" : l.type === "duplicate_label" ? "text-[10px] bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300 border-orange-300" : "text-[10px] bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300 border-amber-300", children: l.type === "pinarello_label_mismatch" ? "Label \u2260 CC" : l.type === "duplicate_label" ? "Duplicate" : "Non-monotonic" }), e.jsxs("span", { className: "text-xs font-mono font-semibold text-foreground", children: ['Size "', l.bikeSize, '"'] }), l.suggestedValue && e.jsxs("span", { className: "text-xs text-muted-foreground", children: ["\u2192 should be", " ", e.jsx("span", { className: "font-mono font-semibold text-emerald-700 dark:text-emerald-400", children: l.suggestedValue })] })] }), e.jsx("p", { className: "text-xs text-muted-foreground", children: l.detail })] }, `${l.type}-${l.sizeId}-${j}`)) })] }), e.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: X ? e.jsxs("div", { className: "flex items-center justify-center py-16 text-muted-foreground", children: [e.jsx(Z, { className: "w-5 h-5 animate-spin mr-2" }), "Loading\u2026"] }) : k ? k.status === "failed" ? e.jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-destructive gap-3", children: [e.jsx(je, { className: "w-8 h-8" }), e.jsx("p", { className: "text-sm", children: k.errorMsg ?? "Re-audit failed" })] }) : u.length === 0 ? e.jsx("div", { className: "flex items-center justify-center py-16 text-muted-foreground text-sm", children: f ? "No flagged diffs." : "No diffs recorded for this run." }) : e.jsxs(Dt, { children: [e.jsx(Ot, { children: e.jsxs(Ke, { children: [e.jsx(I, { className: "w-20", children: "Size" }), e.jsx(I, { className: "w-36", children: "Field" }), e.jsx(I, { className: "text-right w-24", children: "Current" }), e.jsx(I, { className: "text-right w-24", children: "Extracted" }), e.jsx(I, { className: "text-right w-20", children: "Delta" }), e.jsx(I, { className: "w-16 text-center", children: "Flag" }), e.jsx(I, { className: "w-28 text-center", children: "Accept / N/A" })] }) }), e.jsx(It, { children: Object.entries(r).map(([l, j]) => j.map((g, v) => { 144 const a = !!g.appliedAt, D = g.decision === "accepted", z = g.decision === "rejected", Me = da(g.fieldKey) ? 2 : 1, xe = da(g.fieldKey) ? "\xB0" : "mm"; 145 return e.jsxs(Ke, { "data-testid": `diff-row-${g.id}`, className: a ? "opacity-50" : D ? "bg-emerald-50/40 dark:bg-emerald-950/20" : z ? "opacity-50" : g.flagged ? "bg-amber-50/40 dark:bg-amber-950/20" : "", children: [e.jsx(P, { className: "font-mono text-sm", children: v === 0 ? l : "" }), e.jsx(P, { className: "text-sm", children: g.fieldLabel ?? g.fieldKey }), e.jsxs(P, { className: "text-right font-mono text-sm text-muted-foreground", children: [oa(g.currentValue, Me), g.currentValue !== null ? xe : ""] }), e.jsxs(P, { className: "text-right font-mono text-sm", children: [oa(g.extractedValue, Me), g.extractedValue !== null ? xe : ""] }), e.jsxs(P, { className: `text-right font-mono text-sm font-medium ${g.flagged ? "text-amber-600 dar
145k:text-amber-400" : "text-muted-foreground"}`, children: [Qa(g), g.deltaMm !== null ? xe : ""] }), e.jsxs(P, { className: "text-center", children: [g.flagged && e.jsx(je, { className: "w-4 h-4 text-amber-500 inline-block" }), g.consistencyImpact === "degrades" && e.jsx("span", { className: "block text-[10px] font-semibold text-red-500 dark:text-red-400 leading-none mt-0.5", title: "Accepting this change would worsen geometric consistency \u2014 auto-de-flagged", children: "\u2191worse" }), g.consistencyImpact === "improves" && e.jsx("span", { className: "block text-[10px] font-semibold text-emerald-600 dark:text-emerald-400 leading-none mt-0.5", title: "Accepting this change would improve geometric consistency", children: "\u2193better" })] }), e.jsx(P, { className: "text-center", children: a ? e.jsx(pe, { className: "text-xs bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 border-blue-300", children: "Applied" }) : e.jsxs("div", { className: "flex items-center justify-center gap-1.5", children: [e.jsx("button", { onClick: () => B(g), "data-testid": `button-accept-${g.id}`, title: D ? "Un-accept" : "Accept this change", className: `w-7 h-7 rounded-md border transition-colors flex items-center justify-center ${D ? "bg-emerald-500 border-emerald-600 text-white" : "border-border bg-background hover:bg-muted"}`, children: D && e.jsx(Ae, { className: "w-4 h-4" }) }), e.jsx("button", { onClick: () => q(g), "data-testid": `button-dismiss-${g.id}`, title: z ? "Un-dismiss (mark pending)" : "Dismiss \u2014 measurement N/A or not applicable", className: `w-7 h-7 rounded-md border transition-colors flex items-center justify-center ${z ? "bg-muted border-border text-muted-foreground" : "border-border bg-background hover:bg-muted"}`, children: e.jsx(qe, { className: `w-3.5 h-3.5 ${z ? "text-muted-foreground" : "text-muted-foreground/40"}` }) })] }) })] }, g.id); 146 })) })] }) : e.jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-muted-foreground gap-3", children: [e.jsx(dt, { className: "w-10 h-10 opacity-30" }), e.jsx("p", { className: "text-sm", children: 'No re-audit run found. Click "Run Re-Audit" to start.' })] }) })] })] }); 147} 148const Ja = { reaudit_apply: { label: "Re-Audit Apply", className: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300 border-emerald-300" }, admin_patch: { label: "Admin Edit", className: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 border-blue-300" }, direct_fix: { label: "Direct Fix", className: "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300 border-amber-300" }, startup_backfill: { label: "Startup Backfill", className: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300 border-purple-300" }, system_write: { label: "System Write", className: "bg-muted text-muted-foreground border-muted" } }; 149function ca(t) { 150 if (t === null) return "\u2014"; 151 const m = Number(t); 152 return !isNaN(m) && t.trim() !== "" ? Number.isInteger(m) ? String(m) : m.toFixed(2).replace(/\.?0+$/, "") : t; 153} 154function Xa(t, m) { 155 if (t === null || m === null) return null; 156 const h = Number(t), T = Number(m); 157 return isNaN(h) || isNaN(T) ? null : T - h; 158} 159function Za(t) { 160 return new Date(t).toLocaleString("en-US", { month: "short", day: "
160numeric", year: "numeric", hour: "2-digit", minute: "2-digit" }); 161} 162function es(t, m = 24) { 163 return t.length > m ? `${t.slice(0, m)}\u2026` : t; 164} 165function ts({ open: t, onOpenChange: m, modelId: h, modelName: T }) { 166 const { data: A, isLoading: N } = Ve({ queryKey: ["/api/admin/competitor-database/models", h, "change-history"], queryFn: async () => { 167 const f = await fetch(`/api/admin/competitor-database/models/${h}/change-history`, { credentials: "include" }); 168 if (!f.ok) throw new Error("Failed to fetch change history"); 169 return f.json(); 170 }, enabled: t, staleTime: 3e4 }); 171 return e.jsx(De, { open: t, onOpenChange: m, children: e.jsxs(Oe, { className: "max-w-5xl max-h-[90vh] flex flex-col gap-0 p-0", children: [e.jsxs(Ie, { className: "px-6 pt-6 pb-4 border-b border-border", children: [e.jsxs(Pe, { className: "flex items-center gap-2", children: [e.jsx(Ft, { className: "w-5 h-5" }), "Change History \u2014 ", T] }), e.jsx(Ee, { children: "Append-only audit trail of every write to this model's geometry data, regardless of the path that triggered it." })] }), e.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: N ? e.jsxs("div", { className: "flex items-center justify-center py-16 text-muted-foreground", children: [e.jsx(Z, { className: "w-5 h-5 animate-spin mr-2" }), "Loading\u2026"] }) : !A || A.length === 0 ? e.jsxs("div", { className: "flex flex-col items-center justify-center py-16 text-muted-foreground gap-3", children: [e.jsx(Ft, { className: "w-10 h-10 opacity-30" }), e.jsx("p", { className: "text-sm", children: "No changes recorded yet." })] }) : e.jsxs(Dt, { children: [e.jsx(Ot, { children: e.jsxs(Ke, { children: [e.jsx(I, { className: "w-44", children: "Date" }), e.jsx(I, { className: "w-16", children: "Size" }), e.jsx(I, { className: "w-44", children: "Field" }), e.jsx(I, { className: "text-right w-24", children: "Before" }), e.jsx(I, { className: "text-right w-28", children: "After" }), e.jsx(I, { className: "w-36", children: "Source" }), e.jsx(I, { className: "w-36", children: "By" }), e.jsx(I, { children: "Note" })] }) }), e.jsx(It, { children: A.map((f) => { 172 const V = Ja[f.change_source] ?? { label: f.change_source, className: "bg-muted text-muted-foreground border-muted" }, S = Xa(f.old_value, f.new_value); 173 return e.jsxs(Ke, { children: [e.jsx(P, { className: "text-xs text-muted-foreground whitespace-nowrap", children: Za(f.created_at) }), e.jsx(P, { className: "font-mono text-sm", children: f.bike_size ?? "\u2014" }), e.jsx(P, { className: "font-mono text-xs", children: f.field_name }), e.jsx(P, { className: "text-right text-sm tabular-nums text-muted-foreground", children: ca(f.old_value) }), e.jsx(P, { className: "text-right text-sm tabular-nums font-medium", children: e.jsxs("span", { className: S != null && Math.abs(S) > 0 ? S > 0 ? "text-emerald-600 dark:text-emerald-400" : "text-red-600 dark:text-red-400" : "", children: [ca(f.new_value), S != null && Math.abs(S) > 0 && e.jsxs("span", { className: "ml-1 text-xs opacity-60", children: ["(", S > 0 ? "+" : "", Math.abs(S) < 1 ? S.toFixed(2) : S.toFixed(1), ")"] })] }) }), e.jsx(P, { children: e.jsx(pe, { variant: "outline", className: `text-xs ${V.className}`, children: V.label }) }), e.jsx(P, { className: "text-xs text-muted-foreground font-mono", children: f.changed_by === "system" ? "system" : es(f.changed_by) }), e.jsx(P, { className: "text-xs text-muted-foreground max-w-xs truncate", children: f.note ?? "" })] }, f.id); 174 }) })] }) }), e.jsx("div", { className: "px-6 py-3 border-t border-border text-xs text-muted-foreground", children: "Showing up to 200 most recent entries. This log is append-only and covers all writes to any admin-editable field." })] }) }); 175} 176const as = { green: "#10b981", yellow: "#f59e0b", red: "#ef4444", purple: "#a855f7" }, ss = { green: "Acceptable (0\u20132mm)", yellow: "Minor Variance (2\u20135mm)", red: "Significant Variance (>5mm)", purple: "Incomplete \u2014 manufacturer did not publish required dimensions" }; 177function ma({ severity: t, size: m = "sm" }) { 178 if (!t) return null; 179 const h = m === "md" ? 16 : 14, T = as[t], A = t === "green" ? Ae : t === "yellow" ? je : t === "red" ? Ka : Ha; 180 return e.jsx("span", { className: "inline-flex flex-shrink-0", "data-testid": `indicator-consistency-${t}`, title: ss[t], children: e.jsx(A, { size: h, color: T, strokeWidth: 2 }) }); 181} 182function rs(t) { 183 if (t.engineUsedFrontTireSize != null) return Math.round(t.engineUsedFrontTireSize); 184 if (t.actualTrail == null || t.headTubeAngle == null || t.forkOffset == null) return null; 185 const m = t.headTubeAngle * Math.PI / 180, h = (t.actualTrail * Math.sin(m) + t.forkOffset) / Math.cos(m); 186 return Math.round(h); 187} 188function is(t, m) { 189 if (t.actualTrail == null || t.headTubeAngle == null || t.forkOffset == null) return null; 190 const h = t.headTubeAngle * Math.PI / 180; 191 return ((t.actualTrail * Math.sin(h) + t.forkOffset) / Math.cos(h) * Math.cos(h) - m) / Math.sin(h); 192} 193function _s() { 194 var Bt; 195 const { toast: t }
195 = He(), [m, h] = d.useState(""), [T, A] = d.useState(null), [N, f] = d.useState(!1), [V, S] = d.useState("year"), [R, K] = d.useState("models"), [Q, L] = d.useState(/* @__PURE__ */ new Set()), [F, X] = d.useState(null), [H, k] = d.useState(!1), [x, n] = d.useState(!1), [o, u] = d.useState(null), p = (() => { 196 const s = new URLSearchParams(); 197 T && s.set("brand", T), m && s.set("search", m), s.set("includeUnpublished", "true"), N && s.set("status", "draft"); 198 const b = s.toString(); 199 return b ? `/api/admin/competitor-database/models?${b}` : "/api/admin/competitor-database/models"; 200 })(), { data: w = [] } = Ve({ queryKey: ["/api/competitor-database/brands"] }), { data: B = [], isLoading: q } = Ve({ queryKey: [p] }), [M, E] = d.useState(null), U = ke({ mutationFn: async ({ modelId: s, overrideReason: b }) => se("POST", `/api/admin/competitor-database/models/${s}/toggle-status`, b ? { overrideReason: b } : void 0), onSuccess: () => { 201 ne.invalidateQueries({ predicate: (s) => typeof s.queryKey[0] == "string" && s.queryKey[0].includes("competitor-database") }), t({ title: "Status Updated", description: "Model status has been toggled." }); 202 }, onError: (s, b) => { 203 const y = s.message.match(/^\d+: ([\s\S]+)$/); 204 if (y) try { 205 const _ = JSON.parse(y[1]); 206 if (_.requiresOverride && _.advisoryReasons && _.advisoryReasons.length > 0) { 207 E({ modelId: b.modelId, reasons: _.advisoryReasons, overrideReason: "" }); 208 return; 209 } 210 if (_.hardBlockReasons && _.hardBlockReasons.length > 0) { 211 t({ title: "Cannot Publish", description: _.hardBlockReasons.join(" "), variant: "destructive" }); 212 return; 213 } 214 } catch { 215 } 216 t({ title: "Error", description: "Failed to update status.", variant: "destructive" }); 217 } }), Y = ke({ mutationFn: async (s) => se("DELETE", `/api/admin/competitor-database/models/${s}`), onSuccess: () => { 218 ne.invalidateQueries({ predicate: (s) => typeof s.queryKey[0] == "string" && s.queryKey[0].includes("competitor-database") }), t({ title: "Model Deleted", description: "Model and all sizes have been deleted." }), X(null); 219 }, onError: () => { 220 t({ title: "Error", description: "Failed to delete model.", variant: "destructive" }); 221 } }), ce = ke({ mutationFn: async (s) => se("POST", "/api/admin/competitor-database/import", s), onSuccess: () => { 222 ne.invalidateQueries({ predicate: (s) => typeof s.queryKey[0] == "string" && s.queryKey[0].includes("competitor-database") }); 223 } }), [C, ee] = d.useState(null), [l, j] = d.useState(!1); 224 d.useEffect(() => { 225 se("GET", "/api/admin/competitor-database/models/batch-reprocess/status").then((s) => s.json()).then((s) => { 226 s != null && s.running && (ee({ running: !0, total: s.total || 0, completed: s.completed || 0, failed: s.failed || 0 }), g()); 227 }).catch(() => { 228 }); 229 }, []); 230 const g = d.useCallback(() => { 231 let s = 0; 232 const b = 120, y = setInterval(async () => { 233 s++;
234 try { 235 const J = await (await se("GET", "/api/admin/competitor-database/models/batch-reprocess/status")).json(); 236 ee(J), J.running ? s >= b && (clearInterval(y), ee(null)) : (clearInterval(y), ee(null), ne.invalidateQueries({ predicate: (Re) => typeof Re.queryKey[0] == "string" && Re.queryKey[0].includes("competitor-database") }), t({ title: "Recalculation Complete", description: `${J.completed} model${J.completed !== 1 ? "s" : ""} recalculated${J.failed > 0 ? `, ${J.failed} with errors` : "."}` })); 237 } catch { 238 s >= b && clearInterval(y); 239 } 240 }, 5e3); 241 }, []), v = ke({ mutationFn: async () => { 242 var y; 243 const s = await fetch("/api/admin/competitor-database/models/batch-reprocess", { method: "POST", headers: { "X-Requested-With": "XMLHttpRequest" }, credentials: "include" }), b = await s.json(); 244 if (s.status === 409 && (y = b.state) != null && y.running) return { status: "already_running", ...b.state }; 245 if (!s.ok) throw new Error(b.error || "Failed to start recalculation"); 246 return b; 247 }, onSuccess: (s) => { 248 if (n(!1), s.status === "already_running") { 249 ee({ running: !0, total: s.total || 0, completed: s.completed || 0, failed: s.failed || 0 }), t({ title: "Already Recalculating", description: "A recalculation is already running. Monitoring progress\u2026" }), g(); 250 return; 251 } 252 s.status === "started" ? (ee({ running: !0, total: s.total, completed: 0, failed: 0 }), t({ title: "Recalculation Started", description: `Processing ${s.total} models in the background. The list will update automatically when complete.` }), g()) : (ne.invalidateQueries({ predicate: (b) => typeof b.queryKey[0] == "string" && b.queryKey[0].includes("competitor-database") }), t({ title: "Recalculation Complete", description: `${s.total} models recalculated.` })); 253 }, onError: (s) => { 254 t({ title: "Error", description: s?.message || "Failed to start recalculation.", variant: "destructive" }), n(!1); 255 } }), [a, D] = d.useState(null), [z, Me] = d.useState(null), [xe, Le] = d.useState(!1), [Ue, ge] = d.useState(!1), [Ne, c] = d.useState("all"), [O, G] = d.useState(!1), [me, le] = d.useState(/* @__PURE__ */ new Set()), { data: W = [] } = Ve({ queryKey: ["/api/admin/competitor-database/models?includeUnpublished=true"], enabled: O }), Be = W.filter((s) => s.geometryImageUrl), Ye = d.useRef(t); 256 Ye.current = t; 257 const Te = d.useCallback(async () => { 258 try { 259 const b = await (await se("GET", "/api/admin/competitor-database/reaudit/batch/status")).json(); 260 return b && (D(b), Me(b)), b; 261 } catch { 262 return null; 263 } 264 }, []), Qe = d.useCallback(() => { 265 let s = !0, b = 0; 266 const y = 300, _ = async () => { 267 if (!s) return; 268 b++; 269 const J = await Te(); 270 if (s) { 271 if (!J || !J.running) { 272 if (s = !1, J && !J.running) { 273 const Re = J.results.filter((mt) => mt.flaggedDiffs > 0).length; 274 Ye.current({ title: "Vision Re-Audit Complete", description: Re > 0 ? `${Re} model${Re !== 1 ? "s" : ""} have flagged diffs needing review.` : `${J.completed} models checked \u2014 no flagged diffs found.` }); 275 } 276 return; 277 } 278 if (b >= y) { 279 s = !1; 280 return; 281 } 282 setTimeout(_, 3e3); 283 } 284 }; 285 return _(), () => { 286 s = !1; 287 }; 288 }, [Te]), Je = "ridedna_reaudit_dismissed_at"; 289 d.useEffect(() => { 290 let s; 291 return (async () => { 292 const y = await Te(); 293 if (y != null && y.running) s = Qe(); 294 else if (y && !y.running) try { 295 const _ = localStorage.getItem(Je); 296 if (_ && Number(_) === y.startedAt) { 297 D(null); 298 return; 299 } 300 } catch { 301 } 302 })(), () => s?.(); 303 }, [Te, Qe]); 304 const ct = d.useCallback(() => { 305 if (a != null && a.running) { 306 Le(!0); 307 return; 308 } 309 if (a) try { 310 localStorage.setItem(Je, String(a.startedAt)); 311 } catch { 312 } 313 D(null); 314 }, [a]); 315 d.useEffect(() => { 316 xe && a && !a.running && Le(!1); 317 }, [a, xe]); 318 const rt = d.useCallback(() => { 319 if (C != null && C.running) { 320 j(!0); 321 return; 322 } 323 ee(null); 324 }, [C]); 325 d.useEffect(() => { 326 l && C && !C.running && j(!1); 327 }, [C, l]); 328 const it = d.useCallback(async () => { 329 try { 330 localStorage.removeItem(Je); 331 } catch { 332 } 333 await Te(); 334 }, [Te]), ue = ke({ mutationFn: async ({ mode: s, modelIds: b }) => { 335 const y = { mode: s }; 336 s === "selected" && b && (y.modelIds = b); 337 const _ = await se("POST", "/api/admin/competitor-database/reaudit/batch", y); 338 if (!_.ok) { 339 const J = await _.json().catch(() => ({})); 340 throw new Error(J?.error ?? `HTTP ${_.status}`); 341 } 342 return _.json(); 343 }, onSuccess: (s) => { 344 ge(!1), G(!1), le(/* @__PURE__ */ new Set()), s.state && D(s.state), s.queued > 0 ? Qe() : t({ title: "Nothing to process", description: s.message ?? "No models matched the selected run mode." }); 345 }, onError: (s) => { 346 t({ title: "Failed to start re-audit", description: s?.message ?? "Unknown error", variant: "destructive" }), ge(!1); 347 } }), ze = (s) => { 348 L((b) => { 349 const y = new Set(b); 350 return y.has(s) ? y.delete(s) : y.add(s), y; 351 }); 352 }, va = (s) => { 353 h(""), A(null), f(!1), L((b) => new Set(b).add(s)), K("models"), u(s); 354 }; 355 d.useEffect(() => { 356 if (!o || R !== "models" || !B.find((y) => y.id === o)) return; 357 const b = document.querySelector(`[data-testid="card-admin-model-${o}"]`); 358 b && (b.scrollIntoView({ behavior: "smooth", block: "start" }), u(null)); 359 }, [o, R, B]); 360 const Et = d.useCallback(async (s, b) => { 361 const y = s[0], _ = y?.brand, J = y?.modelName, Re = y?.modelYear, mt = y?.bikeType; 362 if (!_ || !J) { 363 t({ title: "Missing Information", description: "Please enter brand and model name in the upload dialog.", variant: "destructive" }); 364 return; 365 } 366 const Fe = s.map((i) => { 367 var ve, Xe, Ze, et, tt, nt, de, at, _t, Vt, Gt, qt, Kt, Ht, Wt, Yt, Qt, Jt, Xt, Zt; 368 const te = (i.geometry.wheelSize || "700c").toLowerCase().trim().replace(/[\s-]*(inch(?:es)?|in\.?)\s*$/, "").trim(), re = te.includes("650b") || te.includes("27.5") || te === "584" || te.includes("584mm"), he = te === "26" || te === "26er" || te === "559" ? 559 : re ? 584 : 622, $e = Pa({ chainstayMm: i.geometry.chainstayMm ?? null, ...i.geometry.rearCenterMm !== void 0 ? { rearCenterMm: i.geometry.rearCenterMm } : {}, wheelbaseMm: i.geometry.wheelbaseMm ?? null, frontCenterMm: i.geometry.frontCenterMm ?? null, bbDropMm: i.geometry.bbDropMm ?? null, sizeLabel: i.selectedSize }), ae = $e.csLength0, oe = $e.horizontalRC;
369 return { bikeSize: i.selectedSize, rimDiameter: he, headTubeAngle: i.geometry.headTubeAngleDeg, seatTubeAngle: i.geometry.seatTubeAngleDeg, headTubeLength: i.geometry.headTubeLengthMm, csLength0: ae, rearCenter: oe ?? void 0, csIsHorizontalSource: $e.interpretation === "horizontal", bbDrop0: ((ve = i.geometrySources) == null ? void 0 : ve.forkA2CMm) === "not-published" ? null : i.geometry.bbDropMm != null ? Math.abs(i.geometry.bbDropMm) : null, forkLength: i.geometry.forkA2CMm == null || ((Xe = i.geometrySources) == null ? void 0 : Xe.forkA2CMm) === "chart" || ((Ze = i.geometrySources) == null ? void 0 : Ze.forkA2CMm) === "user" ? i.geometry.forkA2CMm : Math.round(i.geometry.forkA2CMm), forkLengthSource: ((et = i.geometrySources) == null ? void 0 : et.forkA2CMm) === "chart" ? "chart" : ((tt = i.geometrySources) == null ? void 0 : tt.forkA2CMm) === "user" ? "manual" : ((nt = i.geometrySources) == null ? void 0 : nt.forkA2CMm) === "calculated" ? "calculated" : ((de = i.geometrySources) == null ? void 0 : de.forkA2CMm) === "not-published" ? "not-published" : void 0, forkOffset: ((at = i.geometrySources) == null ? void 0 : at.forkA2CMm) === "not-published" ? null : (() => { 370 var ta, aa; 371 const ut = i.geometry.forkOffsetMm ?? i.geometry.forkRakeMm ?? null, ea = ((ta = i.geometrySources) == null ? void 0 : ta.forkOffsetMm) ?? ((aa = i.geometrySources) == null ? void 0 : aa.forkRakeMm); 372 return ut == null || ea === "chart" || ea === "user" ? ut : Math.round(ut); 373 })(), forkOffsetSource: ((_t = i.geometrySources) == null ? void 0 : _t.forkA2CMm) === "not-published" ? "not-published" : ((Vt = i.geometrySources) == null ? void 0 : Vt.forkOffsetMm) === "chart" || ((Gt = i.geometrySources) == null ? void 0 : Gt.forkRakeMm) === "chart" ? "chart" : ((qt = i.geometrySources) == null ? void 0 : qt.forkOffsetMm) === "user" || ((Kt = i.geometrySources) == null ? void 0 : Kt.forkRakeMm) === "user" ? "manual" : ((Ht = i.geometrySources) == null ? void 0 : Ht.forkOffsetMm) === "calculated" || ((Wt = i.geometrySources) == null ? void 0 : Wt.forkRakeMm) === "calculated" || i.geometry.forkOffsetMm != null || i.geometry.forkRakeMm != null ? "calculated" : void 0, isFlipChip: i.geometry.forkOffsetMm2 != null || i.geometry.forkRakeMm2 != null || i.geometry.forkA2CMm2 != null, forkOffset2: i.geometry.forkOffsetMm2 ?? i.geometry.forkRakeMm2 ?? void 0, forkLength2: i.geometry.forkA2CMm2 ?? void 0, frontCenter: i.geometry.frontCenterMm, stackRef: ((Yt = i.geometrySources) == null ? void 0 : Yt.stackMm) === "chart" ? i.geometry.stackMm : void 0, reachRef: ((Qt = i.geometrySources) == null ? void 0 : Qt.reachMm) === "chart" ? i.geometry.reachMm : void 0, wheelbaseRef: i.geometry.wheelbaseMm, frontCenterRef: ((Jt = i.geometrySources) == null ? void 0 : Jt.frontCenterMm) === "chart" ? i.geometry.frontCenterMm : void 0, standoverRef: i.geometry.standoverMm, stack: ((Xt = i.geometrySources) == null ? void 0 : Xt.stackMm) === "chart" ? i.geometry.stackMm : void 0, reach: ((Zt = i.geometrySources) == null ? void 0 : Zt.reachMm) === "chart" ? i.geometry.reachMm : void 0, wheelbase: i.geometry.wheelbaseMm, standoverHeight: i.geometry.standoverMm, seatTubeC2C: i.geometry.seatTubeC2CMm, seatTubeC2T: i.geometry.seatTubeC2TMm, effectiveTopTube: i.geometry.effectiveTopTubeMm, actualTopTube: i.geometry.actualTopTubeMm, bbHeight: i.geometry.bbHeightMm, maxRearTireSize: i.geometry.maxRearTireClearanceMm, forkMaxTireClearance: i.geometry.maxTireClearanceMm, specificationTireMm: i.geometry.specificationTireMm, manufacturerTrail: i.geometry.trailMm, effectiveStack: i.geometry.effectiveStackMm, effectiveReach: i.geometry.effectiveReachMm, riderHeightMinCm: i.geometry.riderHeightMinCm, riderHeightMaxCm: i.geometry.riderHeightMaxCm, inseamMinCm: i.geometry.inseamMinCm, inseamMaxCm: i.geometry.inseamMaxCm, standoverClearanceCm: i.geometry.standoverClearanceCm, torsoLengthMinCm: i.geometry.torsoLengthMinCm, torsoLengthMaxCm: i.geometry.torsoLengthMaxCm, armLengthMinCm: i.geometry.armLengthMinCm, armLengthMaxCm: i.geometry.armLengthMaxCm, fieldsFoundInChart: i.fieldsFoundInChart ?? void 0 }; 374 }), Ut = Fe.filter((i) =>
374 i.seatTubeC2T != null).length, zt = Fe.filter((i) => i.seatTubeC2C != null && i.seatTubeC2T == null).length; 375 Ut > 0 && zt > 0 && (`${Ut}${zt}`, Fe.forEach((i) => { 376 i.seatTubeC2C != null && i.seatTubeC2T == null && (i.seatTubeC2T = i.seatTubeC2C, i.seatTubeC2C = void 0, `${i.bikeSize}${i.seatTubeC2T}`); 377 })); 378 const _e = Fe.map((i) => i.forkLength).filter((i) => i != null); 379 if (_e.length > 1) { 380 const i = _e.map((ae) => Math.round(ae)), te = /* @__PURE__ */ new Map(); 381 i.forEach((ae) => te.set(ae, (te.get(ae) || 0) + 1)); 382 const re = [...te.keys()].sort((ae, oe) => ae - oe); 383 let fe = [], he = 0; 384 for (const ae of re) { 385 const oe = i.filter((ve) => ve >= ae && ve <= ae + 5); 386 oe.length > he && (he = oe.length, fe = oe); 387 } 388 const $e = Math.ceil(_e.length * 0.6); 389 if (he >= $e && he >= 2) { 390 const ae = /* @__PURE__ */ new Map(); 391 fe.forEach((de) => ae.set(de, (ae.get(de) || 0) + 1)); 392 let oe = fe[0], ve = 0; 393 ae.forEach((de, at) => { 394 (de > ve || de === ve && at < oe) && (oe = at, ve = de); 395 }); 396 const Xe = oe, Ze = i.filter((de) => de < oe - 5 || de > oe + 5), et = Math.min(...i), tt = Math.max(...i), nt = tt - et; 397 `${_e.length}${et}${tt}${nt}${he}${_e.length}${Xe}${ve}${Ze.length > 0 ? Ze.join(", ") : "none"}`, Fe.forEach((de) => { 398 de.forkLength != null && (de.forkLength = Xe); 399 }); 400 } else { 401 const ae = Math.min(...i), oe = Math.max(...i); 402 `${he}${_e.length}${$e}${ae}${oe}`; 403 } 404 } 405 `${Fe.length}${_}${J}`, Fe.map((i) => ({ size: i.bikeSize, forkLength: i.forkLength, forkOffset: i.forkOffset })); 406 try { 407 const i = y?.manufacturerUrl, te = await ce.mutateAsync({ model: { brand: _, name: J, modelYear: Re ? parseInt(Re, 10) : void 0, bikeType: mt || void 0, sourceUrl: i || void 0 }, sizes: Fe }); 408 let re = null; 409 try { 410 re = await te.json(); 411 } catch { 412 } 413 if (b && re != null && re.id) try { 414 const fe = new FormData(); 415 fe.append("file", b); 416 const he = await fetch(`/api/admin/competitor-database/models/${re.id}/geometry-image`, { method: "POST", body: fe }); 417 he.ok ? (`${re.id}`, ne.invalidateQueries({ predicate: ($e) => typeof $e.queryKey[0] == "string" && $e.queryKey[0].includes("competitor-database") })) : console.warn("[CompetitorBatchImport] Failed to save geometry chart image:", await he.text()); 418 } catch (fe) { 419 console.warn("[CompetitorBatchImport] Error uploading geometry chart image:", fe); 420 } 421 return re || { success: !0, sizesImported: Fe.length }; 422 } catch (i) { 423 console.error("[CompetitorBatchImport] Import failed:", i); 424 const te = i instanceof Error ? i.message : "Unknown error"; 425 let re = te; 426 try { 427 const fe = te.indexOf("{"); 428 if (fe !== -1) { 429 const he = JSON.parse(te.slice(fe)); 430 re = he.message || he.error || te; 431 } 432 } catch { 433 } 434 t({ title: "Import Failed", description: re.length > 200 ? re.slice(0, 200) + "\u2026" : re, variant: "destructive" }); 435 return; 436 } 437 }, [ce, t, ne]), Lt = B.filter((s) => !(N && s.status !== "draft")).sort((s, b) => { 438 if (V === "year") return Ya(s, b); 439 if (V === "date") return new Date(b.createdAt).getTime() - new Date(s.createdAt).getTime(); 440 if (V === "type") { 441 const y = (s.bikeType ?? "zzz").toLowerCase(), _ = (b.bikeType ?? "zzz").toLowerCase(); 442 return y !== _ ? y.localeCompare(_) : `${s.brand} ${s.name}`.localeCompare(`${b.brand} ${b.name}`); 443 } 444 return `${s.brand} ${s.name}`.localeCompare(`${b.brand} ${b.name}`); 445 }); 446 return e.jsxs("div", { className: "h-full flex flex-col", children: [e.jsxs("div", { className: "px-6 py-4 border-b border-border", children: [e.jsxs("div", { className: "flex items-center justify-between gap-4 mb-4", children: [e.jsxs("div", { className: "flex items-center gap-2", children: [e.jsx(sa, { className: "w-6 h-6 text-primary" }), e.jsx("h1", { className: "text-2xl font-bold", children: "Geometry Database Admin" })] }), e.jsxs("div", { className: "flex items-center gap-2", children: [e.jsxs($, { variant: "outline", onClick: () => n(!0), disabled: v.isPending || !!(C != null && C.running), "data-testid": "button-recalculate-all", children: [e.jsx(Pt, { className: `w-4 h-4 mr-2 ${v.isPending || C != null && C.running ? "animate-spin" : ""}` }), C != null && C.running ? `Recalculating\u2026 ${C.completed}/${C.total}` : v.isPending ? "Starting\u2026" : "Recalculate All"] }), e.jsxs(wa, { children: [e.jsx(Sa, { asChild: !0, children: e.jsxs($, { variant: "outline", disabled: ue.isPending || a?.running, "data-testid": "button-reaudit-menu", children: [ue.isPending || a != null && a.running ? e.jsx(Z, { className: "w-4 h-4 mr-2 animate-spin" }) : e.jsx(ot, { className: "w-4 h-4 mr-2" }), a != null && a.running ? `Re-Auditing\u2026 ${a.completed}/${a.total}` : ue.isPending ? "Starting\u2026" : "Re-Audit", e.jsx(ha, { className: "w-3.5 h-3.5 ml-1.5 opacity-60" })] }) }), e.jsxs(Ma, { align: "end", children: [z && e.jsxs(e.Fragment, { children: [e.jsx("div", { className: "px-2 py-1.5 flex items-center gap-1.5 text-xs text-muted-foreground select-none", children: z.running ? e.jsxs(e.Fragment, { children: [e.jsx(Z, { className: "w-3 h-3 animate-spin shrink-0" }), e.jsxs("span", { children: ["In progress: ", z.completed, "/", z.total, " models"] })] }) : e.jsxs(e.Fragment, { children: [e.jsx(Ct, { className: "w-3 h-3 shrink-0" }), e.jsxs("span", { children: ["Last run: ", z.completed, "/", z.total, " models complete"] })] }) }), e.jsx(ht, {})] }), e.jsxs(st, { "data-testid": "menu-reaudit-view-results", onClick: it, children: [e.jsx(Ct, { className: "w-4 h-4 mr-2" }), z != null && z.running ? "View Progress" : "View Last Results"] }), e.jsx(ht, {}), z && !z.running && z.completed > 0 && z.completed < z.total && e.jsxs(st, { "data-testid": "menu-reaudit-resume", onClick: () => { 447 c("all"), ge(!0); 448 }, className: "font-medium", children: [e.jsx(qa, { className: "w-4 h-4 mr-2 text-primary" }), "Resume", e.jsxs("span", { className: "ml-2 text-xs text-muted-foreground", children: ["(", z.total - z.completed, " remaining)"] })] }), e.jsxs(st, { "data-testid": "menu-reaudit-all", onClick: () => { 449 c("all"), ge(!0); 450 }, children: [e.jsx(ot, { className: "w-4 h-4 mr-2" }), "Run All", e.jsx("span", { className: "ml-2 text-xs text-muted-foreground", children: "(skip today's)" })] }), e.jsxs(st, { "data-testid": "menu-reaudit-exceptions", onClick: () => { 451 c("exceptions"), ge(!0); 452 }, children: [e.jsx(wt, { className: "w-4 h-4 mr-2" }), "Run Last Exceptions"] }), e.jsx(ht, {}), e.jsxs(st, { "data-testid": "menu-reaudit-selected", onClick: () => { 453 c("selected"), G(!0); 454 }, children: [e.jsx(ya, { className: "w-4 h-4 mr-2" }), "Run Selected\u2026"] })] })] }), e.jsxs($, { onClick: () => k(!0), "data-testid": "button-import-model", children: [e.jsx(Ge, { className: "w-4 h-4 mr-2" }), "Import Model"] })] })] }), e.jsxs("div", { className: "flex items-center gap-1 mt-2 mb-3",
454children: [e.jsxs("button", { className: `flex items-center gap-1.5 px-3 h-8 rounded-md text-sm font-medium transition-colors ${R === "models" ? "bg-primary text-white" : "text-muted-foreground hover:text-foreground hover:bg-muted"}`, onClick: () => K("models"), "data-testid": "tab-models", children: [e.jsx(sa, { className: "w-3.5 h-3.5" }), "Models"] }), e.jsxs("button", { className: `flex items-center gap-1.5 px-3 h-8 rounded-md text-sm font-medium transition-colors ${R === "report-card" ? "bg-primary text-white" : "text-muted-foreground hover:text-foreground hover:bg-muted"}`, onClick: () => K("report-card"), "data-testid": "tab-report-card", children: [e.jsx(Wa, { className: "w-3.5 h-3.5" }), "Report Card"] })] }), R === "models" && e.jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [e.jsxs("div", { className: "relative flex-1 min-w-[200px] max-w-sm", children: [e.jsx(xa, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" }), e.jsx(ye, { placeholder: "Search brands or models...", value: m, onChange: (s) => h(s.target.value), className: "pl-10", "data-testid": "input-admin-search" })] }), e.jsxs(St, { value: T || "__all__", onValueChange: (s) => A(s === "__all__" ? null : s), children: [e.jsx(Mt, { className: "w-[160px]", "data-testid": "select-admin-brand", children: e.jsx(Tt, { placeholder: "All Brands" }) }), e.jsxs(Rt, { children: [e.jsx(be, { value: "__all__", children: "All Brands" }), w.map((s) => e.jsx(be, { value: s, children: s }, s))] })] }), e.jsxs(St, { value: V, onValueChange: (s) => S(s), children: [e.jsx(Mt, { className: "w-[160px]", "data-testid": "select-sort-by", children: e.jsx(Tt, { placeholder: "Sort by\u2026" }) }), e.jsxs(Rt, { children: [e.jsx(be, { value: "year", children: "Sort: Newest Model Year" }), e.jsx(be, { value: "brand", children: "Sort: Brand" }), e.jsx(be, { value: "date", children: "Sort: Date Created" }), e.jsx(be, { value: "type", children: "Sort: Bike Type" })] })] }), e.jsxs("div", { className: "flex items-center gap-2", children: [e.jsx($t, { checked: N, onCheckedChange: f, id: "draft-filter", "data-testid": "switch-draft-only" }), e.jsx(ie, { htmlFor: "draft-filter", className: "text-sm", children: "Draft Only" })] })] })] }), R === "models" ? e.jsx(At, { className: "flex-1", children: e.jsx("div", { className: "p-6 space-y-4", children: q ? e.jsx("div", { className: "flex items-center justify-center py-12", children: e.jsx(Z, { className: "w-8 h-8 animate-spin text-muted-foreground" }) }) : Lt.length === 0 ? e.jsx(ga, { children: e.jsx(fa, { className: "py-12 text-center text-muted-foreground", children: e.jsx("p", { children: "No competitor models found. Import your first model to get started." }) }) }) : Lt.map((s) => e.jsx(cs, { model: s, isExpanded: Q.has(s.id), onToggleExpand: () => ze(s.id), onToggleStatus: () => U.mutate({ modelId: s.id }), onDelete: () => X(s.id), isTogglingStatus: U.isPending }, s.id)) }) }) : e.jsx(Va, { onNavigateToModel: va }), H && e.jsx(Ea, { open: H, onOpenChange: k, onGeometryParsed: (s) => Et([s]), onBatchImport: Et, destination: "competitor-database" }), e.jsx(xt, { open: x, onOpenChange: n, children: e.jsxs(gt, { children: [e.jsxs(ft, { children: [e.jsx(pt, { children: "Recalculate All Models?" }), e.jsx(bt, { children: "This will re-run geometry calculations and AI analysis for every model in the database. It may take several minutes. The page will refresh automatically when complete." })] }), e.jsxs(jt, { children: [e.jsx(yt, { disabled: v.isPending, children: "Cancel" }), e.jsx(kt, { onClick: () => v.mutate(), disabled: v.isPending, "data-testid": "button-confirm-recalculate-all", children: v.isPending ? e.jsxs(e.Fragment, { children: [e.jsx(Z, { className: "w-4 h-4 mr-2 animate-spin" }), "Recalculating\u2026"] }) : "Recalculate All" })] })] }) }), e.jsx(xt, { open: Ue, onOpenChange: ge, children: e.jsxs(gt, { children: [e.jsxs(ft, { children: [e.jsx(pt, { children: Ne === "exceptions" ? "Re-Audit Models With Discrepancies?" : "Vision Re-Audit All Models?" }), e.jsx(bt, { children: Ne === "exceptions" ? "This will re-run the vision parser on any model whose most recent audit found flagged geometry differences \u2014 or whose run failed outright. Use this to verify whether extraction fixes resolved those discrepancies." : "This will re-run the vision parser on every model's geometry chart image and generate field-level diffs showing what has changed. Models already audited today are skipped. Results appear as each model completes." })] }), e.jsxs(jt, { children: [e.jsx(yt, { disabled: ue.isPending, children: "Cancel" }), e.jsx(kt, { onClick: () => ue.mutate({ mode: Ne }), disabled: ue.isPending, "data-testid": "button-confirm-reaudit", children: ue.isPending ? e.jsxs(e.Fragment, { children: [e.jsx(Z, { className: "w-4 h-4 mr-2 animate-spin" }), "Starting\u2026"] }) : Ne === "exceptions" ? e.jsxs(e.Fragment, { children: [e.jsx(wt, { className: "w-4 h-4 mr-2" }), "Run Last Exceptions"] }) : e.jsxs(e.Fragment, { children: [e.jsx(ot, { className: "w-4 h-4 mr-2" }), "Re-Audit All"] }) })] })] }) }), e.jsx(De, { open: !!M, onOpenChange: (s) => !s && E(null), children: e.jsxs(Oe, { children: [e.jsxs(Ie, { children: [e.jsxs(Pe, { className: "flex items-center gap-2", children: [e.jsx(je, { className: "w-5 h-5 text-yellow-500" }), "Advisory: Missing Geometry Dimensions"] }), e.jsx(Ee, { children: "This model can still be published, but the following issues were detected. Provide a brief reason to confirm you are aware and proceeding intentionally." })] }), e.jsxs("div", { className: "space-y-3 py-2", children: [e.jsx("ul", { className: "list-disc list-inside text-sm text-muted-foreground space-y-1", children: M?.reasons.map((s, b) => e.jsx("li", { children: s }, b)) }), e.jsxs("div", { className: "space-y-1", children: [e.jsxs(ie, { htmlFor: "override-reason", className: "text-sm font-medium", children: ["Override reason ", e.jsx("span", { className: "text-destructive", children: "*" })] }), e.jsx(pa, { id: "override-reason", placeholder: "e.g. Manufacturer does not publish ETT \u2014 this is expected for this brand.", value: M?.overrideReason ?? "", onChange: (s) => E((b) => b ? { ...b, overrideReason: s.target.value } : null), rows: 3 })] })] }), e.jsxs(We, { children: [e.jsx($, { variant: "outline", onClick: () => E(null), children: "Cancel" }), e.jsx($, { disabled: !((Bt = M?.overrideReason) != null && Bt.trim()) || U.isPending, onClick: () => { 455 var s; 456 (s = M?.overrideReason) != null && s.trim() && U.mutate({ modelId: M.modelId, overrideReason: M.overrideReason.trim() }, { onSuccess: () => E(null) }); 457 }, children: U.isPending ? e.jsxs(e.Fragment, { children: [e.jsx(Z, { className: "w-4 h-4 mr-2 animate-spin" }), "Publishing\u2026"] }) : "Publish Anyway" })] })] }) }), e.jsx(xt, { open: !!F, onOpenChange: (s) => !s && X(null), children: e.jsxs(gt, { children: [e.jsxs(ft, { children: [e.jsx(pt, { children: "Delete Competitor Model?" }), e.jsx(bt, { children: "This will permanently delete the model and all its sizes. This action cannot be undone." })] }), e.jsxs(jt, { children: [e.jsx(yt, { children: "Cancel" }), e.jsx(kt, { onClick: () => F && Y.mutate(F), className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", children: "Delete" })] })] }) }), O && e.jsx(os, { models: Be, selectedIds: me, onChangeIds: le, isPending: ue.isPending, onConfirm: () => ue.mutate({ mode: "selected", modelIds: [...me] }), onClose: () => { 458 G(!1), le(/* @__PURE__ */ new Set()); 459 } }), a && !xe && e.jsx(ns, { status: a, onDismiss: ct, onReauditFailed: (s) => { 460 le(new Set(s)), G(!0); 461 } }), C && !l && e.jsx(ds, { progress: C, onDismiss: rt })] }); 462} 463function ns({ status: t, onDismiss: m, onReauditFailed: h }) { 464 const { toast: T } = He(), [A, N] = d.useState(null), [f, V] = d.useState(null), [S, R] = d.useState(!1), [K, Q] = d.useState(/* @__PURE__ */ new Map()), L = d.useRef(null), F = d.useMemo(() => t.results.map((r) => K.get(r.modelId) ?? r), [t.results, K]), X = t.total > 0 ? Math.round(t.completed / t.total * 100) : 0, H = F.filter((r) => r.flaggedDiffs > 0 && r.status === "done"), k = F.filter((r) => r.status === "failed"), x = F.find((r) => r.status === "running"), n = async (r, p, w) => { 465 if (!["image/png", "image/jpeg", "image/webp", "application/pdf"].includes(r.type)) { 466 T({ title: "Invalid File", description: "Only PNG, JPG, WebP, or PDF files are allowed.", variant: "destructive" }); 467 return; 468 } 469 if (r.size > 20 * 1024 * 1024) { 470 T({ title: "File Too Large", description: "Maximum file size is 20MB.", variant: "destructive" }); 471 return; 472 } 473 R(!0), Q((q) => { 474 const M = new Map(q), E = t.results.find((U) => U.modelId === p) ?? { modelId: p, brand: "", name: w, status: "failed", flaggedDiffs: 0, totalDiffs: 0, runI
474d: null, errorMsg: null }; 475 return M.set(p, { ...E, status: "running", errorMsg: null }), M; 476 }); 477 try { 478 const q = new FormData(); 479 q.append("file", r); 480 const M = await fetch(`/api/admin/competitor-database/models/${p}/geometry-image`, { method: "POST", credentials: "include", body: q }); 481 if (!M.ok) { 482 const Y = await M.json().catch(() => ({ error: "Upload failed" })); 483 throw new Error(Y.error || "Upload failed"); 484 } 485 const E = await fetch(`/api/admin/competitor-database/models/${p}/reaudit`, { method: "POST", credentials: "include" }); 486 if (!E.ok) { 487 const Y = await E.json().catch(() => ({ error: "Re-audit failed" })); 488 throw new Error(Y.error || "Re-audit failed"); 489 } 490 const U = await E.json(); 491 Q((Y) => { 492 const ce = new Map(Y), C = t.results.find((ee) => ee.modelId === p); 493 return ce.set(p, { ...C, status: "done", errorMsg: null, flaggedDiffs: U.flaggedDiffs ?? 0, totalDiffs: U.totalDiffs ?? 0, runId: U.runId ?? null }), ce; 494 }), V(null), T({ title: "Re-Audit Complete", description: U.flaggedDiffs > 0 ? `${w}: ${U.flaggedDiffs} diff${U.flaggedDiffs !== 1 ? "s" : ""} found \u2014 click the row to review.` : `${w}: no geometry changes detected.` }); 495 } catch (q) { 496 Q((M) => { 497 const E = new Map(M), U = t.results.find((Y) => Y.modelId === p); 498 return E.set(p, { ...U, status: "failed", errorMsg: q.message }), E; 499 }), T({ title: "Repair Failed", description: q.message, variant: "destructive" }); 500 } finally { 501 R(!1); 502 } 503 }, o = (r) => r.status === "running" ? e.jsx(Z, { className: "w-4 h-4 animate-spin text-primary shrink-0" }) : r.status === "done" && r.flaggedDiffs > 0 ? e.jsx(je, { className: "w-4 h-4 text-amber-500 shrink-0" }) : r.status === "done" ? e.jsx(Ae, { className: "w-4 h-4 text-emerald-500 shrink-0" }) : r.status === "skipped" ? e.jsx(Ae, { className: "w-4 h-4 text-muted-foreground shrink-0" }) : r.status === "failed" ? e.jsx(qe, { className: "w-4 h-4 text-destructive shrink-0" }) : e.jsx("div", { className: "w-4 h-4 rounded-full border border-border shrink-0" }), u = f ? F.find((r) => r.modelId === f) : null; 504 return e.jsxs(e.Fragment, { children: [e.jsx("input", { ref: L, type: "file", accept: "image/png,image/jpeg,image/webp,application/pdf", className: "sr-only", onChange: (r) => { 505 var w; 506 const p = (w = r.target.files) == null ? void 0 : w[0]; 507 p && f && u && n(p, f, `${u.brand} ${u.name}`.trim()), r.target.value = ""; 508 } }), e.jsx(De, { open: !0, onOpenChange: (r) => { 509 r || m(); 510 }, children: e.jsxs(Oe, { className: "max-w-2xl w-full", "data-testid": "dialog-reaudit-progress", children: [e.jsxs(Ie, { children: [e.jsx(Pe, { className: "flex items-center gap-2", children: t.running ? e.jsxs(e.Fragment, { children: [e.jsx(Z, { className: "w-5 h-5 animate-spin text-primary" }), " Vision Re-Audit in Progress"] }) : H.length > 0 || k.length > 0 ? e.jsxs(e.Fragment, { children: [e.jsx(je, { className: "w-5 h-5 text-amber-500" }), " Re-Audit Complete \u2014 Review Flagged Diffs"] }) : e.jsxs(e.Fragment, { children: [e.jsx(Ae, { className: "w-5 h-5 text-emerald-500" }), " Re-Audit Complete \u2014 All Clear"] }) }), e.jsx(Ee, { children: t.running ? x ? `Processing ${x.brand} ${x.name} \u2014 ${t.completed} of ${t.total} complete` : `${t.completed} of ${t.total} models processed` : `${t.completed} models audited${t.failed > 0 ? `, ${t.failed} failed` : ""}.${H.length > 0 ? ` ${H.length} model${H.length !== 1 ? "s" : ""} have diffs that need review.` : " No geometry changes detected."}` })] }), e.jsxs("div", { className: "space-y-1", children: [e.jsxs("div", { className: "flex justify-between text-xs text-muted-foreground", children: [e.jsxs("span", { children: [t.completed, " / ", t.total, " models"] }), e.jsxs("span", { children: [X, "%"] })] }), e.jsx("div", { className: "h-2 bg-muted rounded-full overflow-hidden", children: e.jsx("div", { className: "h-full bg-primary rounded-full transition-all duration-700", style: { width: `${X}%` } }) }), t.failed > 0 && e.jsxs("p", { className: "text-xs text-destructive", children: [t.failed, " model", t.failed !== 1 ? "s" : "", " failed \u2014 click a failed row to replace its chart image"] })] }), e.jsx(At, { className: "h-72 border border-border rounded-md", children: e.jsx("div", { className: "divide-y divide-border", children: (t.running ? F : [...F].sort((r, p) => { 511 const w = (B) => B.status === "done" && B.flaggedDiffs > 0 ? 0 : B.status === "failed" ? 1 : 2; 512 return w(r) - w(p); 513 })).map((r) => e.jsxs("div", { children: [e.jsxs("div", { className: `flex items-center gap-3 px-3 py-3 ${r.status === "running" ? "bg-primary/5" : r.status === "done" && r.flaggedDiffs > 0 ? "bg-amber-50 dark:bg-amber-950/20 cursor-pointer hover-elevate" : r.status === "failed" ? `cursor-pointer hover-elevate ${f === r.modelId ? "bg-blue-50 dark:bg-blue-950/20" : ""}` : ""}`, onClick: () => { 514 r.status === "done" && r.flaggedDiffs > 0 ? N({ id: r.modelId, name: `${r.brand} ${r.name}`.trim() }) : r.status === "failed" && V((p) => p === r.modelId ? null : r.modelId); 515 }, "data-testid": `reaudit-row-${r.modelId}`, children: [o(r), e.jsxs("div", { className: "flex-1 min-w-0", children: [e.jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [e.jsxs("span", { className: `text-sm font-medium ${r.status === "pending" ? "text-muted-foreground" : ""}`, children: [r.brand, " ", r.name] }), r.status === "done" && r.flaggedDiffs > 0 && e.jsxs(pe, { className: "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300 border-amber-300 dark:border-amber-700 text-xs", children: [r.flaggedDiffs, " diff", r.flaggedDiffs !== 1 ? "s" : "", " \u2014 click to review"] }), r.status === "done" && r.flaggedDiffs === 0 && r.totalDiffs === 0 && e.jsx("span", { className: "text-xs text-emerald-600 dark:text-emerald-400", children: "no changes" }), r.status === "done" && r.flaggedDiffs === 0 && r.totalDiffs > 0 && e.jsxs("span", { className: "text-xs text-muted-foreground", children: [r.totalDiffs, " minor diff", r.totalDiffs !== 1 ? "s" : ""] }), r.status === "failed" && e.jsx(pe, { className: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 border-blue-300 dark:border-blue-700 text-xs", children: f === r.modelId ? "cancel \u2191" : "click to replace image" })] }), r.status === "failed" && r.errorMsg && f !== r.modelId && e.jsx("p", { className: "text-xs text-destructive truncate", children: r.errorMsg })] }), e.jsxs("div", { className: "shrink-0 text-muted-foreground", children: [r.status === "pending" && e.jsx("span", { className: "text-xs", children: "queued" }), r.status === "running" && e.jsx("span", { className: "text-xs text-primary", children: "auditing\u2026" }), r.status === "done" && r.flaggedDiffs > 0 && e.jsx(ja, { className: "w-4 h-4 text-amber-500" }), r.status === "failed" && f !== r.modelId && e.jsx(Ge, { className: "w-4 h-4 text-blue-500" })] })] }), f === r.modelId && e.jsx("div", { className: "mx-3 mb-3 border-2 border-dashed border-blue-300 dark:border-blue-700 rounded-md p-4 bg-blue-50/50 dark:bg-blue-950/10", onDragOver: (p) => p.preventDefault(), onDrop: (p) => { 516 p.preventDefault(); 517 const w = p.dataTransfer.files[0]; 518 w && n(w, r.modelId, `${r.brand} ${r.name}`.trim()); 519 }, children: S ? e.jsxs("div", { className: "flex items-center gap-2 justify-center py-2 text-sm text-muted-foreground", children: [e.jsx(Z, { className: "w-4 h-4 animate-spin" }), "Uploading chart and re-auditing\u2026"] }) : e.jsxs("div", { className: "text-center space-y-2", children: [e.jsxs("p", { className: "text-xs text-muted-foreground", children: ["Chart image for ", e.jsxs("span", { className: "font-medium text-foreground", children: [r.brand, " ", r.name] }), " is missing. Upload a replacement to re-audit immediately."] }), e.jsxs("div", { className: "flex items-center justify-center gap-2", children: [e.jsxs($, { size: "sm", variant: "outline", className: "gap-1.5 text-blue-700 border-blue-300 hover:bg-blue-100 dar
519k:text-blue-400 dark:border-blue-700 dark:hover:bg-blue-900/30", onClick: () => { 520 var p; 521 return (p = L.current) == null ? void 0 : p.click(); 522 }, children: [e.jsx(Ge, { className: "w-3.5 h-3.5" }), "Choose File"] }), e.jsx("span", { className: "text-xs text-muted-foreground", children: "or drag & drop here" })] }), e.jsx("p", { className: "text-xs text-muted-foreground/70", children: "PNG, JPG, WebP, or PDF \xB7 max 20 MB" })] }) })] }, r.modelId)) }) }), e.jsx(We, { children: t.running ? e.jsx("p", { className: "text-xs text-muted-foreground mr-auto", children: "Audit is running in the background and will complete automatically." }) : e.jsxs("div", { className: "flex items-center gap-2 w-full", children: [H.length > 0 && !k.length && e.jsx("p", { className: "text-xs text-muted-foreground mr-auto", children: "Click any highlighted row above to inspect and approve or reject each diff." }), k.length > 0 && h && e.jsxs($, { variant: "outline", size: "sm", className: "gap-1.5 text-blue-700 border-blue-300 hover:bg-blue-100 dark:text-blue-400 dark:border-blue-700 dark:hover:bg-blue-900/30", onClick: () => { 523 h(k.map((r) => r.modelId)), m(); 524 }, children: [e.jsx(Pt, { className: "w-3.5 h-3.5" }), "Re-Audit ", k.length, " Failed Model", k.length !== 1 ? "s" : ""] }), e.jsx($, { onClick: m, "data-testid": "button-close-reaudit-dialog", className: "ml-auto", children: "Dismiss" })] }) })] }) }), A && e.jsx(Na, { open: !0, onOpenChange: () => N(null), modelId: A.id, modelName: A.name, hasImage: !0 })] }); 525} 526const ls = [{ status: "green", icon: Ae, label: "0\u20132mm", chipCls: "border-emerald-300 text-emerald-700 dark:border-emerald-700 dark:text-emerald-400", activeCls: "bg-emerald-100 dark:bg-emerald-900/40 border-emerald-400 dark:border-emerald-500" }, { status: "yellow", icon: je, label: "2\u20135mm", chipCls: "border-amber-300 text-amber-700 dark:border-amber-700 dark:text-amber-400", activeCls: "bg-amber-100 dark:bg-amber-900/40 border-amber-400 dark:border-amber-500" }, { status: "red", icon: Aa, label: ">5mm", chipCls: "border-red-300 text-red-700 dark:border-red-700 dark:text-red-400", activeCls: "bg-red-100 dark:bg-red-900/40 border-red-400 dark:border-red-500" }, { status: "purple", icon: Ga, label: "Incomplete", chipCls: "border-purple-300 text-purple-700 dark:border-purple-700 dark:text-purple-400", activeCls: "bg-purple-100 dark:bg-purple-900/40 border-purple-400 dark:border-purple-500" }]; 527function os({ models: t, selectedIds: m, onChangeIds: h, isPending: T, onConfirm: A, onClose: N }) { 528 const [f, V] = d.useState(""), [S, R] = d.useState(null), K = { green: t.filter((n) => n.overallConsistency === "green").length, yellow: t.filter((n) => n.overallConsistency === "yellow").length, red: t.filter((n) => n.overallConsistency === "red").length, purple: t.filter((n) => n.overallConsistency === "purple").length }, Q = (n) => { 529 const o = t.filter((p) => p.overallConsistency === n), u = S === n; 530 R(u ? null : n); 531 const r = new Set(m); 532 u ? o.forEach((p) => r.delete(p.id)) : o.forEach((p) => r.add(p.id)), h(r); 533 }, L = t.filter((n) => { 534 const o = !f.trim() || `${n.brand ?? ""} ${n.name}`.toLowerCase().includes(f.toLowerCase()), u = !S || n.overallConsistency === S; 535 return o && u; 536 }), F = L.length > 0 && L.every((n) => m.has(n.id)), X = L.some((n) => m.has(n.id)), H = () => { 537 const n = new Set(m); 538 F ? L.forEach((o) => n.delete(o.id)) : L.forEach((o) => n.add(o.id)), h(n); 539 }, k = (n) => { 540 const o = new Set(m); 541 o.has(n) ? o.delete(n) : o.add(n), h(o); 542 }, x = L.reduce((n, o) => { 543 const u = o.brand ?? "Unknown"; 544 return n[u] || (n[u] = []), n[u].push(o), n; 545 }, {}); 546 return e.jsx(De, { open: !0, onOpenChange: (n) => { 547 n || N(); 548 }, children: e.jsxs(Oe, { className: "max-w-lg w-full", "data-testid": "dialog-reaudit-picker", children: [e.jsxs(Ie, { children: [e.jsxs(Pe, { className: "flex items-center gap-2", children: [e.jsx(ya, { className: "w-5 h-5" }), "Run Selected \u2014 Choose Models"] }), e.jsx(Ee, { children: "Select which models to re-audit. Only models with a geometry chart image are listed." })] }), e.jsxs("div", { className: "relative", children: [e.jsx(xa, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" }), e.jsx("input", { className: "w-full pl-9 pr-3 py-2 text-sm border border-border rounded-md bg-background focus:outline-none focus:ring-1 focus:ring-ring", placeholder: "Filter models\u2026", value: f, onChange: (n) => V(n.target.value), "data-testid": "input-picker-search" })] }), e.jsxs("div", { className: "flex items-center gap-1.5 flex-wrap", children: [ls.map(({ status: n, icon: o, label: u, chipCls: r, activeCls: p }) => { 549 const w = K[n], B = S === n; 550 return e.jsxs("button", { onClick: () => Q(n), "data-testid": `picker-status-filter-${n}`, title: B ? `Deselect all ${u} models` : `Select all ${u} models`, className: `inline-flex items-center gap-1 px-2 py-1 rounded-full border text-xs font-medium transition-colors ${r} ${B ? p : "bg-background hover:bg-muted"}`, children: [e.jsx(o, { className: "w-3 h-3 shrink-0" }
550), u, e.jsxs("span", { className: "opacity-70", children: ["(", w, ")"] })] }, n); 551 }), S && e.jsxs("button", { onClick: () => R(null), title: "Clear status filter (keeps current selection)", className: "inline-flex items-center gap-1 px-2 py-1 rounded-full border border-border text-xs text-muted-foreground hover:bg-muted transition-colors", children: [e.jsx(qe, { className: "w-3 h-3" }), " Clear filter"] })] }), e.jsxs("div", { className: "flex items-center gap-2 py-1 border-b border-border", children: [e.jsx(ra, { id: "picker-select-all", checked: F ? !0 : X ? "indeterminate" : !1, onCheckedChange: H, "data-testid": "checkbox-select-all" }), e.jsxs("label", { htmlFor: "picker-select-all", className: "text-sm font-medium cursor-pointer select-none", children: [F ? "Deselect all" : "Select all", f || S ? " (filtered)" : ""] }), e.jsxs("span", { className: "ml-auto text-xs text-muted-foreground", children: [m.size, " / ", t.length, " selected"] })] }), e.jsx(At, { className: "h-72 -mx-1 px-1", children: t.length === 0 ? e.jsx("p", { className: "text-sm text-muted-foreground text-center py-8", children: "Loading models\u2026" }) : L.length === 0 ? e.jsxs("p", { className: "text-sm text-muted-foreground text-center py-8", children: ['No models match "', f, '"'] }) : Object.entries(x).sort(([n], [o]) => n.localeCompare(o)).map(([n, o]) => e.jsxs("div", { className: "mb-3", children: [e.jsx("p", { className: "text-xs font-semibold text-muted-foreground uppercase tracking-wider px-1 mb-1", children: n }), e.jsx("div", { className: "space-y-0.5", children: o.map((u) => e.jsxs("div", { className: `flex items-center gap-3 px-2 py-2 rounded-md cursor-pointer hover-elevate ${m.has(u.id) ? "bg-primary/5" : ""}`, onClick: () => k(u.id), "data-testid": `picker-row-${u.id}`, children: [e.jsx(ra, { id: `picker-${u.id}`, checked: m.has(u.id), onCheckedChange: () => k(u.id), onClick: (r) => r.stopPropagation() }), e.jsx("label", { htmlFor: `picker-${u.id}`, className: "text-sm cursor-pointer select-none flex-1", children: u.name })] }, u.id)) })] }, n)) }), e.jsxs(We, { children: [e.jsx($, { variant: "outline", onClick: N, disabled: T, children: "Cancel" }), e.jsx($, { onClick: A, disabled: m.size === 0 || T, "data-testid": "button-confirm-picker", children: T ? e.jsxs(e.Fragment, { children: [e.jsx(Z, { className: "w-4 h-4 mr-2 animate-spin" }), "Starting\u2026"] }) : e.jsxs(e.Fragment, { children: [e.jsx(ot, { className: "w-4 h-4 mr-2" }), "Run ", m.size, " Model", m.size !== 1 ? "s" : ""] }) })] })] }) }); 552} 553function ds({ progress: t, onDismiss: m }) { 554 const h = t.total > 0 ? Math.round(t.completed / t.total * 100) : 0; 555 return e.jsx(De, { open: !0, onOpenChange: (T) => { 556 T || m(); 557 }, children: e.jsxs(Oe, { className: "max-w-md w-full", "data-testid": "dialog-recalculate-progress", children: [e.jsxs(Ie, { children: [e.jsx(Pe, { className: "flex items-center gap-2", children: t.running ? e.jsxs(e.Fragment, { children: [e.jsx(Z, { className: "w-5 h-5 animate-spin text-primary" }), " Recalculating Geometry\u2026"] }) : e.jsxs(e.Fragment, { children: [e.jsx(Ae, { className: "w-5 h-5 text-emerald-500" }), " Recalculation Complete"] }) }), e.jsx(Ee, { children: t.running ? `Running geometry engine and AI analysis on all models. ${t.completed} of ${t.total} done.` : `${t.completed} model${t.completed !== 1 ? "s" : ""} recalculated${t.failed > 0 ? `, ${t.failed} with errors` : " successfully"}.` })] }), e.jsxs("div", { className: "space-y-2", children: [e.jsxs("div", { className: "flex justify-between text-xs text-muted-foreground", children: [e.jsxs("span", { children: [t.completed, " / ", t.total, " models"] }), e.jsxs("span", { children: [h, "%"] })] }), e.jsx("div", { className: "h-2 bg-muted rounded-full overflow-hidden", children: e.jsx("div", { className: "h-full bg-primary rounded-full transition-all duration-700", style: { width: `${h}%` } }) }), t.failed > 0 && e.jsxs("p", { className: "text-xs text-destructive", children: [t.failed, " model", t.failed !== 1 ? "s" : "", " encountered errors during recalculation."] })] }), e.jsx(We, { children: t.running ? e.jsx("p", { className: "text-xs text-muted-foreground", children: "Recalculation runs in the background \u2014 the model list updates automatically when complete." }) : e.jsx($, { onClick: m, "data-testid": "button-close-recalculate-dialog", children: "Done" }) })] }) }); 558} 559function cs({ model: t, isExpanded: m, onToggleExpand: h, onToggleStatus: T, onDelete: A, isTogglingStatus: N }) { 560 var v; 561 const { toast: f } = He(), [V, S] = d.useState(!1), [R, K] = d.useState(!1), [Q, L] = d.useState(!1), [F, X] = d.useState(!1), [H, k] = d.useState(!1), [x, n] = d.useState(1), { data: o = [], isLoading: u } = Ve({ queryKey: ["/api/admin/competitor-database/models", t.id, "sizes"], queryFn: async () => { 562 const a = await fetch(`/api/admin/competitor-database/models/${t.id}/sizes`, { credentials: "include" }); 563 if (!a.ok) throw new Error("Failed to fetch sizes"); 564 return a.json(); 565 }, enabled: m }), r = d.useMemo(() => [...o].sort((a, D) => (a.effectiveTopTube ?? 1 / 0) - (D.effectiveTopTube ?? 1 / 0)), [o]), p = d.useMemo(() => r.some((a) => a.isFlipChip && a.forkOffset2 != null), [r]), w = m && !u && o.length > 0 ? La(o) : [], B = w.some((a) => a.key === "forkLength"), q = t.isManufacturerIncomplete || B, M = m && !u && o.length > 0 ? w.map((a) => a.label) : t.missingManufacturerDimensions ?? [], Y = ["fork axle-to-crown length (A2C)", "fork offset", "BB drop"].every((a) => M.includes(a)) && (m && !u && o.length > 0 ? o.every((a) => a.forkOffsetSource === "not-published" || a.forkOffsetSource == null) : !0), ce = Y ? ["front center", "fork axle-to-crown length (A2C)", "BB drop", "fork offset", "trail"] : M, C = m && !u && o.length > 0 ? Ba(o) : null, ee = t.status === "draft" && C != null && !C.canPublish; 566 ke({ mutationFn: async ({ sizeId: a, data: D }) => se("PATCH", `/api/admin/competitor-database/sizes/${a}`, D), onSuccess: () => { 567 ne.invalidateQueries({ predicate: (a) => typeof a.queryKey[0] == "string" && a.queryKey[0].includes("competitor-database") }); 568 }, onError: () => { 569 f({ title: "Error", description: "Failed to update size.", variant: "destructive" }); 570 } }); 571 const l = ke({ mutationFn: async (a) => { 572 await Promise.all(a.map((D) => se("PATCH", `/api/admin/competitor-database/sizes/${D.sizeId}`, D.data))); 573 }, onSuccess: () => { 574 ne.invalidateQueries({ predicate: (a) => typeof a.queryKey[0] == "string" && a.queryKey[0].includes("competitor-database") }), f({ title: "Geometry Saved", description: "All geometry values have been updated." }), S(!1), t.status === "published" && j.mutate(); 575 }, onError: () => { 576 f({ title: "Error", description: "Failed to save geometry changes.", variant: "destructive" }); 577 } }), j = ke({ mutationFn: async () => (await se("POST", `/api/admin/competitor-database/models/${t.id}/process`)).json(), onSuccess: (a) => { 578 ne.invalidateQueries({ predicate: (D) => typeof D.queryKey[0] == "string" && D.queryKey[0].includes("competitor-database") }), f({ title: "Geometry Recalculated", description: `${a.processed} sizes processed${a.failed ? `, ${a.failed} failed` : ""}.` }); 579 }, onError: () => { 580 f({ title: "Error", description: "Failed to recalculate geometry.", variant: "destructive" }); 581 } }), g = ke({ mutationFn: async (a) => se("PATCH", `/api/admin/competitor-database/models/${t.id}`, a), onSuccess: () => { 582 ne.invalidateQueries({ predicate: (a) => typeof a.queryKey[0] == "string" && a.queryKey[0].includes("competitor-database") }), f({ title: "Model Updated", description: "Model details have been saved." }), K(!1); 583 }, onError: () => { 584 f({ title: "Error", description: "Failed to update model.", variant: "destructive" }); 585 } }); 586 return e.jsxs(e.Fragment, { children: [e.jsx(ga, { "data-testid": `card-admin-model-${t.id}`, children: e.jsxs(Da, { open: m, onOpenChange: h, children: [e.jsx(Oa, { asChild: !0, children: e.jsx(Ta, { className: "cursor-pointer hover-elevate py-3 px-4", children: e.jsxs("div", { className: "flex items-center justify-between gap-4", children: [e.jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [e.jsx(Ra, { className: "w-5 h-5 text-muted-foreground flex-shrink-0" }), e.jsx("div", { className: "min-w-0", children: e.jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [e.jsxs(Fa, { className: "text-base font-semibold", children: [t.modelYear ? `${t.modelYear} ` : "", t.brand, " ", t.name] }), t.bikeType && e.jsx(pe, { variant: "outline", className: "text-xs", children: t.bikeType }), e.jsx(pe, { variant: t.status === "published" ? "default" : "secondary", className: "text-xs", children: t.status === "published" ? e.jsxs(e.Fragment, { children: [e.jsx(ia, { className: "w-3 h-3 mr-1" }), " Published"] }) : e.jsxs(e.Fragment, { children: [e.jsx(na, { className: "w-3 h-3 mr-1" }), " Draft"] }) }), t.dimensionConflict && !t.dimensionConflict.resolution && e.jsxs(Ce, { children: [e.jsx(we, { asChild: !0, children: e.jsxs("span", { className: "inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-semibold cursor-default select-none", style: { background: "rgba(202,138,4,0.12)", color: "#b45309", border: "1px solid rgba(202,138,4,0.4)" }, "data-testid": `badge-admin-dimension-conflict-${t.id}`, children: [e.jsx(wt, { className: "w-3 h-3 flex-shrink-0" }), "Dimension Conflict"] }) }), e.jsxs(Se, { side: "bottom", className: "max-w-xs", children: [e.jsx("p", { className: "font-semibold text-xs mb-1", children: "Geometry Inconsistency \u2014 Review Required" }), e.jsxs("p", { className: "text-xs leading-relaxed text-muted-foreground", children: [t.dimensionConflict.outlierLabel, " \u2014 geometric inconsistency detected coordinate dimensions (Reach, WB, FC) by >5mm in", " ", t.dimensionConflict.conflictingSizes.length, " size", t.dimensionConflict.conflictingSizes.length !== 1 ? "s" : "", ". Open the audit dialog to override or dismiss."] })] })] }), q && e.jsxs(Ce, { children: [e.jsx(we, { asChild: !0, children: e.jsxs("span", { className: "inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-semibold cursor-default select-none", style: { background: "rgba(220,38,38,0.12)", color: "#dc2626", border: "1px solid rgba(220,38,38,0.35)" }, "data-testid": `badge-admin-manufacturer-incomplete-${t.id}`, children: [e.jsx(je, { className: "w-3 h-3 flex-shrink-0" }), "Incomplete Manufacturer Data"] }) }), e.jsxs(Se, { side: "bottom", className: "max-w-sm", children: [e.jsx("p", { className: "font-semibold text-xs mb-1", children: "Manufacturer Has Not Published Sufficient Geometry" }), e.jsxs("p", { className: "text-xs leading-relaxed text-muted-foreground", children: [Y ? "Five dimensions are absent from this manufacturer's published specification: front center, fork axle-to-crown length (A2C), BB drop, fork offset, trail." : (t.missingManufacturerDimensions ?? []).length > 0 ? `Missing from manufacturer's spec: ${(t.missingManufacturerDimensions ?? []).join(", ")}.` : "Critical dimensions are absent from this manufacturer's published specifications.", " ", "Stability index and agility index cannot be calculated, so these sizes cannot be added to Garage or used for c
586omparison. The model can still be published and audited \u2014 the audit icon reflects real findings."] })] })] }), e.jsx(ma, { severity: t.overallConsistency, size: "md" }), e.jsx("span", { className: "text-xs text-muted-foreground ml-2", children: new Date(t.createdAt).toLocaleDateString() }), (t.frontTireRange || t.rearTireRange || t.specTireMm != null) && e.jsxs("span", { className: "text-xs text-muted-foreground flex items-center gap-2 ml-1", children: [t.frontTireRange && e.jsxs("span", { children: ["Max F: ", e.jsxs("span", { className: "font-medium text-foreground", children: [t.frontTireRange.max, "mm"] })] }), t.rearTireRange && e.jsxs("span", { children: ["Max R: ", e.jsxs("span", { className: "font-medium text-foreground", children: [t.rearTireRange.max, "mm"] })] }), t.specTireMm != null && e.jsxs("span", { children: ["RT: ", e.jsxs("span", { className: "font-medium text-foreground", children: [t.specTireMm, "mm"] })] })] })] }) })] }), e.jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [e.jsxs(Ce, { children: [e.jsx(we, { asChild: !0, children: e.jsx($, { size: "icon", variant: "ghost", onClick: (a) => { 587 a.stopPropagation(), K(!0); 588 }, "data-testid": `button-edit-model-${t.id}`, children: e.jsx(la, { className: "w-4 h-4" }) }) }), e.jsx(Se, { children: "Edit Model Details" })] }), e.jsxs(Ce, { children: [e.jsx(we, { asChild: !0, children: e.jsx("span", { children: e.jsx($, { size: "icon", variant: "ghost", onClick: (a) => { 589 a.stopPropagation(), T(); 590 }, disabled: N || ee, "data-testid": `button-toggle-status-${t.id}`, children: t.status === "published" ? e.jsx(na, { className: "w-4 h-4" }) : e.jsx(ia, { className: `w-4 h-4 ${ee ? "text-muted-foreground" : ""}` }) }) }) }), e.jsx(Se, { children: t.status === "published" ? "Unpublish" : ee && (v = C?.hardBlockReasons) != null && v.length ? `Cannot publish: ${C.hardBlockReasons.join(" ")}` : "Publish" })] }), e.jsxs(Ce, { children: [e.jsx(we, { asChild: !0, children: e.jsx($, { size: "icon", variant: "ghost", onClick: (a) => { 591 a.stopPropagation(), j.mutate(); 592 }, disabled: j.isPending, "data-testid": `button-reprocess-${t.id}`, children: e.jsx(Pt, { className: `w-4 h-4 ${j.isPending ? "animate-spin" : ""}` }) }) }), e.jsx(Se, { children: "Recalculate Geometry" })] }), e.jsxs(Ce, { children: [e.jsx(we, { asChild: !0, children: e.jsx($, { size: "icon", variant: "ghost", onClick: (a) => { 593 a.stopPropagation(), m || h(), L(!0); 594 }, "data-testid": `button-audit-${t.id}`, children: e.jsx(Ct, { className: "w-4 h-4" }) }) }), e.jsx(Se, { children: "View Audit Report" })] }), e.jsxs(Ce, { children: [e.jsx(we, { asChild: !0, children: e.jsx($, { size: "icon", variant: "ghost", onClick: (a) => { 595 a.stopPropagation(), X(!0); 596 }, "data-testid": `button-reaudit-${t.id}`, children: e.jsx(dt, { className: "w-4 h-4" }) }) }), e.jsx(Se, { children: "Vision Re-Audit" })] }), e.jsxs(Ce, { children: [e.jsx(we, { asChild: !0, children: e.jsx($, { size: "icon", variant: "ghost", onClick: (a) => { 597 a.stopPropagation(), k(!0); 598 }, "data-testid": `button-change-history-${t.id}`, children: e.jsx(Ft, { className: "w-4 h-4" }) }) }), e.jsx(Se, { children: "Change History" })] }), e.jsxs(Ce, { children: [e.jsx(we, { asChild: !0, children: e.jsx($, { size: "icon", variant: "ghost", onClick: (a) => { 599 a.stopPropagation(), A(); 600 }, "data-testid": `button-delete-${t.id}`, children: e.jsx(ba, { className: "w-4 h-4 text-destructive" }) }) }), e.jsx(Se, { children: "Delete Model" })] }), m ? e.jsx($a, { className: "w-4 h-4 text-muted-foreground" }) : e.jsx(ha, { className: "w-4 h-4 text-muted-foreground" })] })] }) }) }), e.jsx(Ia, { children: e.jsx(fa, { className: "pt-0 pb-4", children: u ? e.jsx("div", { className: "flex items-center justify-center py-8", children: e.jsx(Z, { className: "w-6 h-6 animate-spin text-muted-foreground" }) }) : o.length === 0 ? e.jsx("p", { className: "text-sm text-muted-foreground text-center py-4", children: "No sizes imported" }) : e.jsxs(e.Fragment, { children: [q && e.jsxs("div", { className: "flex items-start gap-3 rounded-lg p-3 mb-4", style: { background: "rgba(220,38,38,0.08)", border: "1px solid rgba(220,38,38,0.35)" }, "data-testid": `banner-manufacturer-incomplete-${t.id}`, children: [e.jsx(je, { className: "w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" }), e.jsxs("div", { className: "space-y-1 min-w-0", children: [e.jsx("p", { className: "text-sm font-semibold text-red-600 dark:text-red-400", children: "Manufacturer has not published sufficient geometry data" }), e.jsxs("p", { className: "text-xs text-muted-foreground leading-relaxed", children: [ce.length > 0 ? e.jsxs(e.Fragment, { children: ["The following dimensions are absent from this manufacturer's published specification: ", e.jsx("strong", { children: ce.join(", ") }), "."] }) : "Critical geometry dimensions are absent from this manufacturer's published specification.", " ", Y ? "Without fork offset, fork A2C, and BB drop, stability index and agility index also cannot be calculated, so these sizes cannot be added to Garage or used for geometry comparison." : "Without these values, trail, stability index, and agility index cannot be calculated, so these sizes cannot be added to Garage or used for geometry comparison.", " ", "This model can still be published and audited \u2014 all applicable warnings will be attributed to the manufacturer on public and admin views."] })] })] }), p && e.jsxs("div", { className: "flex items-center gap-2 mb-3 px-1", children: [e.jsx("span", { className: "text-xs text-muted-foreground font-medium", children: "Flip Chip:" }), e.jsx($, { size: "sm", variant: x === 1 ? "default" : "outline", className: "text-xs", onClick: () => n(1), "data-testid": `button-admin-flip-pos-1-${t.id}`, children: "Position 1" }), e.jsx($, { size: "sm", variant: x === 2 ? "default" : "outline", className: "text-xs", onClick: () => n(2), "data-testid": `button-admin-flip-pos-2-${t.id}`, children: "Position 2" }), e.jsx("span", { className: "text-xs text-muted-foreground", children: "Offset shows both positions \xB7 Trail recalculates for active position" })] }), e.jsxs("div", { className: "border rounded-md overflow-hidden", children: [e.jsxs(Dt, { children: [e.jsx(Ot, { children: e.jsxs(Ke, { children: [e.jsx(I, { className: "w-20", children: "Size" }), e.jsx(I, { className: "text-right", children: "Stack" }), e.jsx(I, { className: "text-right", children: "Reach" }), e.jsx(I, { className: "text-right", children: "HTA" }), e.jsx(I, { className: "text-right", children: "STA" }), e.jsx(I, { className: "text-right", children: "ETT" }), e.jsx(I, { className: "text-right", children: "WB" }), e.jsx(I, { className: "text-right", children: "CS" }), e.jsx(I, { className: "text-right", children: "BBD" }), e.jsx(I, { className: "text-right", children: "A2C" }), e.jsx(I, { className: "text-right", children: "Offset" }), e.jsx(I, { className: "text-right", children: "Stab" }), e.jsx(I, { className: "text-right", children: "Agil" }), e.jsx(I, { className: "text-right", children: "Trail" }), e.jsx(I, { className: "text-center w-8", children: "Audit" })] }) }), e.jsx(It, { children: r.map((a) => { 601 var D, z, Me, xe, Le, Ue, ge, Ne, c, O, G, me; 602 return e.jsxs(Ke, { "data-testid": `row-size-${a.id}`, children: [e.jsx(P, { className: "font-medium", children: a.bikeSize }), e.jsx(P, { className: "text-right", children: ((D = a.stack) == null ? void 0 : D.toFixed(1)) ?? "-" }), e.jsx(P, { className: "text-right", children: ((z = a.reach) == null ? void 0 : z.toFixed(1)) ?? "-" }), e.jsxs(P, { className: "text-right", children: [((Me = a.headTubeAngle) == null ? void 0 : Me.toFixed(2)) ?? "-", "\xB0"] }), e.jsxs(P, { className: "text-right", children: [((xe = a.seatTubeAngle) == null ? void 0 : xe.toFixed(2)) ?? "-", "\xB0"] }), e.jsx(P, { className: "text-right", children: ((Le = a.effectiveTopTube) == null ? void 0 : Le.toFixed(1)) ?? "-" }), e.jsx(P, { className: "text-right", children: ((Ue = a.wheelbase) == null ? void 0 : Ue.toFixed(1)) ?? "-" }), e.jsx(P, { className: "text-right", children: ((ge = a.csLength0) == null ? void 0 : ge.toFixed(1)) ?? "-" }), e.jsx(P, { className: "text-right", children: (() => { 603 if (a.forkOffsetSource === "not-published" || a.forkOffsetSource == null && a.forkOffset === 0 || a.bbDrop0 == null) return "-"; 604 const W = Ua(a); 605 return e.jsxs(e.Fragment, { children: [a.bbDrop0.toFixed(1), W && e.jsx("sup", { className: "text-amber-500 text-[9px] ml-0.5 cursor-help", title: "Not published by manufacturer", children: "*" })] }); 606 })() }), e.jsxs(P, { className: "text-right", children: [a.forkLength == null ? "-" : lt(a.forkLength), za(a) && e.jsx("sup", { className: "text-amber-500 text-[9px] ml-0.5 cursor-help", title: a.forkLengthSource === "manual" ? "Entered from third-party research" : "Analytically derived \u2014 not published by manufacturer", children: "*" })] }), e.jsx(P, { className: "text-right tabular-nums", children: (() => { 607 if (a.forkOffsetSource === "not-published" || a.forkOffsetSource == null && a.forkOffset === 0) return "-"; 608 const W = a.forkOffsetSource === "manual" || a.forkOffsetSource === "calculated" || a.forkOffsetSource === "derived" ? e.jsx("sup", { className: "text-amber-500 text-[9px] ml-0.5 cursor-help", title: "Not published by manufacturer \u2014 third-party sourced or engine-derived", children: "*" }) : null; 609 return a.isFlipChip && a.forkOffset2 != null ? e.jsxs("span", { className: "whitespace-nowrap", children: [e.jsxs("span", { className: x === 1 ? "font-medium" : "text-muted-foreground", children: [a.forkOffset == null ? "-" : lt(a.forkOffset), W] }), e.jsx("span", { className: "text-muted-foreground mx-0.5", children: "/" }), e.jsx("span", { className: x === 2 ? "font-medium" : "text-muted-foreground", children: lt(a.forkOffset2) })] }) : e.jsxs(e.Fragment, { children: [a.forkOffset == null ? "-" : lt(a.forkOffset), W] }); 610 })() }), e.jsx(P, { className: "text-right", children: ((Ne = a.stabilityIndex) == null ? void 0 : Ne.toFixed(1)) ?? "-" }), e.jsx(P, { className: "text-right", children: ((c = a.agilityIndex) == null ? void 0 : c.toFixed(1)) ?? "-" }), e.jsx(P, { className: "text-right", children: (() => { 611 const W = x === 2 && a.isFlipChip && a.forkOffset2 != null ? is(a, a.forkOffset2) : a.actualTrail; 612 if (W == null) return "-"; 613 const Be = rs(a); 614 return e.jsxs("span", { className: "whitespace-nowrap text-foreground
614/80", children: [(Math.floor(W * 10) / 10).toFixed(1), Be != null && e.jsxs("span", { className: "text-muted-foreground text-[10px] ml-0.5", children: ["@R\u209C", Be] })] }); 615 })() }), e.jsx(P, { className: "text-center", children: a.geometryConsistency ? e.jsxs(Ce, { children: [e.jsx(we, { asChild: !0, children: e.jsx("span", { className: "cursor-help inline-flex items-center justify-center", children: e.jsx(ma, { severity: a.geometryConsistency.overall, size: "md" }) }) }), e.jsxs(Se, { side: "left", className: "max-w-md", children: [e.jsx("p", { className: "font-medium text-xs mb-1", children: a.geometryConsistency.summary }), ((O = a.geometryConsistency.engineeringAnalysis) == null ? void 0 : O.conclusion) && e.jsx("p", { className: "text-xs mb-1.5 leading-relaxed font-medium text-foreground", "data-testid": "text-audit-interpretation", children: a.geometryConsistency.engineeringAnalysis.conclusion }), ((G = a.geometryConsistency.engineeringAnalysis) == null ? void 0 : G.impliedA2CConsensus) != null && e.jsxs("p", { className: "text-xs mb-1 text-muted-foreground", children: ["Implied A2C: ", a.geometryConsistency.engineeringAnalysis.impliedA2CConsensus, "mm", a.geometryConsistency.engineeringAnalysis.impliedA2CSpread != null && ` (\xB1${a.geometryConsistency.engineeringAnalysis.impliedA2CSpread}mm spread)`] }), ((me = a.geometryConsistency.engineeringAnalysis) == null ? void 0 : me.impliedOffsetFromTrail) != null && e.jsxs("p", { className: "text-xs mb-1 text-muted-foreground", children: ["Implied offset: ", a.geometryConsistency.engineeringAnalysis.impliedOffsetFromTrail, "mm"] }), a.geometryConsistency.checks.filter((le) => le.severity !== "green").map((le, W) => e.jsx("p", { className: "text-xs text-muted-foreground", children: le.explanation }, W))] })] }) : e.jsx("span", { className: "text-muted-foreground text-xs", children: "-" }) })] }, a.id); 616 }) })] }), r.some((a) => a.forkLengthSource === "manual" || a.forkLengthSource === "calculated" || a.forkOffsetSource === "manual" || a.forkOffsetSource === "calculated") && e.jsx("p", { className: "text-[11px] text-amber-600/70 mt-1.5 px-1", children: "* This dimension is not published by the manufacturer." })] }), e.jsx("div", { className: "flex flex-wrap justify-end mt-3", children: e.jsxs($, { variant: "outline", onClick: (a) => { 617 a.stopPropagation(), S(!0); 618 }, "data-testid": `button-edit-geometry-${t.id}`, children: [e.jsx(la, { className: "w-4 h-4 mr-2" }), "Edit Geometry"] }) })] }) }) })] }) }), V && r.length > 0 && e.jsx(ms, { sizes: r, modelName: `${t.modelYear ? t.modelYear + " " : ""}${t.brand} ${t.name}`, model: t, open: V, onOpenChange: S, onSave: (a) => l.mutate(a), onSaveModel: (a) => g.mutate(a), isSaving: l.isPending }), e.jsx(us, { model: t, open: R, onOpenChange: K, onSave: (a) => g.mutate(a), isSaving: g.isPending }), e.jsx(_a, { open: Q, onOpenChange: L, model: t, sizes: o, isLoading: u && Q, onModelUpdated: () => { 619 ne.invalidateQueries({ predicate: (a) => typeof a.queryKey[0] == "string" && a.queryKey[0].includes("competitor-database") }); 620 } }), e.jsx(Na, { open: F, onOpenChange: X, modelId: t.id, modelName: `${t.modelYear ? `${t.modelYear} ` : ""}${t.brand ?? ""} ${t.name}`.trim(), hasImage: !!t.geometryImageUrl }), e.jsx(ts, { open: H, onOpenChange: k, modelId: t.id, modelName: `${t.modelYear ? `${t.modelYear} ` : ""}${t.brand ?? ""} ${t.name}`.trim() })] }); 621} 622const vt = [{ key: "headTubeAngle", label: "Head Tube Angle", unit: "\xB0", group: "Frame Angles", decimals: 2 }, { key: "seatTubeAngle", label: "Seat Tube Angle", unit: "\xB0", group: "Frame Angles", decimals: 2 }, { key: "stack", label: "Stack", unit: "mm", group: "Fit", decimals: 1 }, { key: "reach", label: "Reach", unit: "mm", group: "Fit", decimals: 1 }, { key: "wheelbase", label: "Wheelbase", unit: "mm", group: "Fit", decimals: 1 }, { key: "standoverHeight", label: "Standover Height", unit: "mm", group: "Fit", decimals: 1 }, { key: "effectiveTopTube", label: "Effective Top Tube", unit: "mm", group: "Fit", decimals: 1 }, { key: "frontCenter", label: "Front Center", unit: "mm", group: "Fit", decimals: 1 }, { key: "headTubeLength", label: "Head Tube Length", unit: "mm", group: "Frame Tubes", decimals: 1 }, { key: "seatTubeC2C", label: "Seat Tube C2C", unit: "mm", group: "Frame Tubes", decimals: 1 }, { key: "seatTubeC2T", label: "Seat Tube C2T", unit: "mm", group: "Frame Tubes", decimals: 1 }, { key: "actualTopTube", label: "Actual Top Tube", unit: "mm", group: "Frame Tubes", decimals: 1 }, { key: "csLength0", label: "Chainstay Length", unit: "mm", group: "Rear Triangle", decimals: 1 }, { key: "rearCenter", label: "Rear Center", unit: "mm", group: "Rear Triangle", decimals: 1 }, { key: "bbDrop0", label: "BB Drop", unit: "mm", group: "Rear Triangle", decimals: 1 }, { key: "bbHeight", label: "BB Height", unit: "mm", group: "Rear Triangle", decimals: 1 }, { key: "maxRearTireSize", label: "Max Rear Tire", unit: "mm", group: "Rear Triangle", decimals: 0 }, { key: "forkLength", label: "Fork A2C", unit: "mm", group: "Fork", decimals: 1 }, { key: "forkOffset", label: "Fork Offset", unit: "mm", group: "Fork", decimals: 1 }, { key: "forkMaxTireClearance", label: "Max Tire Clearance", unit: "mm", group: "Fork", decimals: 0 }, { key: "specificationTireMm", label: "Specification Tire", unit: "mm", group: "Fork", decimals: 0 }, { key: "isSuspension", label: "Suspension Fork", unit: "", group: "Fork", decimals: 0, type: "boolean" }, { key: "forkTravel", label: "Fork Travel", unit: "mm", group: "Fork", decimals: 0 }, { key: "isFlipChip", label: "Flip-Chip", unit: "", group: "Fork", decimals: 0, type: "boolean" }, { key: "forkLength2", label: "Fork A2C (Position 2)", unit: "mm", group: "Fork", decimals: 1 }, { key: "forkOffset2", label: "Fork Offset (Position 2)", unit: "mm", group: "Fork", decimals: 1 }, { key: "rimDiameter", label: "Rim Diameter", unit: "mm", group: "Wheels", decimals: 0 }, { key: "stackRef", label: "Stack (Mfr Ref)", unit: "mm", group: "Reference Values", decimals: 1 }, { key: "reachRef", label: "Reach (Mfr Ref)", unit: "mm", group: "Reference Values", decimals: 1 }, { key: "standoverRef", label: "Standover (Mfr Ref)", unit: "mm", group: "Reference Values", decimals: 1 }, { key: "wheelbaseRef", label: "Wheelbase (Mfr Ref)", unit: "mm", group: "Reference Values", decimals: 1 }, { key: "frontCenterRef", label: "Front Center (Mfr Ref)", unit: "mm", group: "Reference Values", decimals: 1 }, { key: "actualTrail", label: "Trail", unit: "mm", group: "Calculated", decimals: 1 }, { key: "mechanicalTrail", label: "Mechanical Trail", unit: "mm", group: "Calculated", decimals: 1 }, { key: "manufacturerTrail", label: "Manufacturer Trail", unit: "mm", group: "Calculated", decimals: 1 }, { key: "wheelFlop", label: "Wheel Flop", unit: "mm", group: "Calculated", decimals: 1 }, { key: "stabilityIndex", label: "Stability Index", unit: "", group: "Calculated", decimals: 2 }, { key: "agilityIndex", label: "Agility Index", unit: "", group: "Calculated", decimals: 2 }, { key: "handlingIndex", label: "Handling Index", unit: "", group: "Calculated", decimals: 2 }]; 623function ms({ sizes: t, modelName: m, model: h, open: T, onOpenChange: A, onSave: N, onSaveModel: f, isSaving: V }) { 624 const { toast: S } = He(), [R, K] = d.useState({}), [Q, L] = d.useState(h.additionalWheelSizes ?? []); 625 d.useEffect(() => { 626 L(h.additionalWheelSizes ?? []); 627 }, [h]); 628 const F = (x) => { 629 L((n) => n.includes(x) ? n.filter((o) => o !== x) : [...n, x]); 630 }; 631 d.useEffect(() => { 632 const x = {}; 633 t.forEach((n) => { 634 const o = {}; 635 o.bikeSize = n.bikeSize, vt.forEach((u) => { 636 const r = n[u.key]; 637 u.type === "boolean" ? o[u.key] = r === !0 ? "true" : r === !1 ? "false" : "" : o[u.key] = r != null && !Number.isNaN(r) ? String(r) : ""; 638 }), x[n.id] = o; 639 }), K(x); 640 }, [t]); 641 const X = (x, n, o) => { 642 K((u) => ({ ...u, [x]: { ...u[x], [n]: o } })); 643 }, H = () => { 644 const x = [], n = []; 645 if (t.forEach((r) => { 646 const p = R[r.id]; 647 if (!p) return; 648 const w = {}; 649 p.bikeSize && p.bikeSize !== r.bikeSize && (w.bikeSize = p.bikeSize), vt.forEach((B) => { 650 const q = p[B.key], M = r[B.key]; 651 if (B.type === "boolean") {
652 const E = q === "true" ? !0 : q === "false" ? !1 : null; 653 E !== M && (w[B.key] = E); 654 } else if (q === "" && M != null) w[B.key] = null; 655 else if (q !== "") { 656 const E = parseFloat(q); 657 isNaN(E) ? x.push(`${r.bikeSize} \u2192 ${B.label}`) : E !== M && (w[B.key] = E); 658 } 659 }), Object.keys(w).length > 0 && n.push({ sizeId: r.id, data: w }); 660 }), x.length > 0) { 661 S({ title: "Invalid values", description: `Fix non-numeric entries: ${x.slice(0, 5).join(", ")}${x.length > 5 ? ` and ${x.length - 5} more` : ""}`, variant: "destructive" }); 662 return; 663 } 664 const o = h.additionalWheelSizes ?? [], u = JSON.stringify([...Q].sort()) !== JSON.stringify([...o].sort()); 665 if (n.length === 0 && !u) { 666 A(!1); 667 return; 668 } 669 u && f && f({ additionalWheelSizes: Q }), n.length > 0 ? N(n) : A(!1); 670 }, k = vt.reduce((x, n) => (x[n.group] || (x[n.group] = []), x[n.group].push(n), x), {}); 671 return e.jsx(De, { open: T, onOpenChange: A, children: e.jsxs(Oe, { className: "max-w-[95vw] w-full max-h-[90vh] overflow-hidden flex flex-col", children: [e.jsxs(Ie, { children: [e.jsx(Pe, { children: "Edit Geometry" }), e.jsx(Ee, { children: m })] }), e.jsx("div", { className: "border border-border rounded-md px-4 py-3 flex-shrink-0", children: e.jsxs("div", { className: "flex items-start gap-4 flex-wrap", children: [e.jsxs("div", { className: "flex-shrink-0", children: [e.jsx("p", { className: "text-sm font-medium leading-none mb-0.5", children: "Dual Wheel Compatibility" }), e.jsx("p", { className: "text-xs text-muted-foreground", children: "Additional sizes this frame can run (beyond native wheel size)" })] }), e.jsx("div", { className: "flex gap-4 flex-wrap items-center", children: ["700c", "650b", "26"].map((x) => e.jsxs("label", { className: "flex items-center gap-2 cursor-pointer select-none", "data-testid": `checkbox-geo-additional-wheel-${x}`, children: [e.jsx("input", { type: "checkbox", checked: Q.includes(x), onChange: () => F(x), className: "accent-primary" }), e.jsx("span", { className: "text-sm font-medium", children: x === "700c" ? '700c / 29"' : x === "650b" ? '650b / 27.5"' : '26"' })] }, x)) })] }) }), e.jsx("div", { className: "flex-1 overflow-auto min-h-0", children: e.jsxs("table", { className: "w-full border-collapse text-sm", children: [e.jsx("thead", { className: "sticky top-0 z-10 bg-background", children: e.jsxs("tr", { children: [e.jsx("th", { className: "text-left px-2 py-1.5 font-medium border-b min-w-[180px] sticky left-0 bg-background z-20", children: "Field" }), t.map((x) => { 672 var n; 673 return e.jsx("th", { className: "text-center px-1 py-1.5 font-medium border-b min-w-[90px]", children: e.jsx(ye, { value: ((n = R[x.id]) == null ? void 0 : n.bikeSize) ?? "", onChange: (o) => X(x.id, "bikeSize", o.target.value), className: "h-7 text-center text-xs font-semibold", "data-testid": `input-size-name-${x.id}` }) }, x.id); 674 })] }) }), e.jsx("tbody", { children: Object.entries(k).map(([x, n]) => e.jsxs(d.Fragment, { children: [e.jsx("tr", { children: e.jsx("td", { colSpan: t.length + 1, className: "px-2 pt-4 pb-1 text-xs font-bold uppercase tracking-wider text-muted-foreground border-b bg-muted/30", children: x }) }), n.map((o) => e.jsxs("tr", { className: "border-b border-border/50", children: [e.jsxs("td", { className: "px-2 py-1 text-xs text-muted-foreground sticky left-0 bg-background whitespace-nowrap", children: [o.label, o.unit ? e.jsxs("span", { className: "ml-1 opacity-60", children: ["(", o.unit, ")"] }) : null] }), t.map((u) => { 675 var r, p; 676 return e.jsx("td", { className: "px-1 py-0.5", children: o.type === "boolean" ? e.jsx("div", { className: "flex justify-center", children: e.jsx($t, { checked: ((r = R[u.id]) == null ? void 0 : r[o.key]) === "true", onCheckedChange: (w) => X(u.id, o.key, String(w)), "data-testid": `switch-${o.key}-${u.id}` }) }) : e.jsx(ye, { type: "number", step: o.decimals > 0 ? Math.pow(10, -o.decimals) : 1, value: ((p = R[u.id]) == null ? void 0 : p[o.key]) ?? "", onChange: (w) => X(u.id, o.key, w.target.value), placeholder: "-", className: "text-xs text-center px-1", "data-testid": `input-${o.key}-${u.id}` }) }, u.id); 677 })] }, o.key))] }, x)) })] }) }), e.jsxs(We, { className: "gap-2 pt-3 border-t flex-shrink-0", children: [e.jsxs($, { variant: "outline", onClick: () => A(!1), disabled: V, children: [e.jsx(qe, { className: "w-4 h-4 mr-1" }), "Cancel"] }), e.jsxs($, { onClick: H, disabled: V, "data-testid": "button-save-geometry", children: [V ? e.jsx(Z, { className: "w-4 h-4 mr-1 animate-spin" }) : e.jsx(ka, { className: "w-4 h-4 mr-1" }), "Save All Changes"] })] })] }) }); 678} 679function us({ model: t, open: m, onOpenChange: h, onSave: T, isSaving: A }) { 680 var ge, Ne; 681 const { toast: N } = He(), f = d.useRef(null), [V, S] = d.useState(t.brand), [R, K] = d.useState(t.name), [Q, L] = d.useState(t.modelYear ? String(t.modelYear) : ""), [F, X] = d.useState(t.bikeType ?? ""), [H, k] = d.useState(t.sourceUrl ?? ""), [x, n] = d.useState(t.geometryImageUrl ?? ""), [o, u] = d.useState(t.fitStyle ?? ""), [r, p] = d.useState(t.sizingNotes ?? ""), [w, B] = d.useState(t.additionalWheelSizes ?? []), [q, M] = d.useState(t.specTireMm != null ? String(Math.round(t.specTireMm)) : ""), [E, U] = d.useState(((ge = t.frontTireRange) == null ? void 0 : ge.max) != null ? String(Math.round(t.frontTireRange.max)) : ""), [Y, ce] = d.useState(((Ne = t.rearTireRange) == null ? void 0 : Ne.max) != null ? String(Math.round(t.rearTireRange.max)) : ""), [C, ee] = d.useState(!1), [l, j] = d.useState(!1), [g, v] = d.useState(!1); 682 d.useEffect(() => { 683 var c, O; 684 S(t.brand), K(t.name), L(t.modelYear ? String(t.modelYear) : ""), X(t.bikeType ?? ""), k(t.sourceUrl ?? ""), n(t.geometryImageUrl ?? ""), u(t.fitStyle ?? ""), p(t.sizingNotes ?? ""), B(t.additionalWheelSizes ?? []), M(t.specTireMm != null ? String(Math.round(t.specTireMm)) : ""), U(((c = t.frontTireRange) == null ? void 0 : c.max) != null ? String(Math.round(t.frontTireRange.max)) : ""), ce(((O = t.rearTireRange) == null ? void 0 : O.max) != null ? String(Math.round(t.rearTireRange.max)) : ""); 685 }, [t]); 686 const a = (c) => { 687 B((O) => O.includes(c) ? O.filter((G) => G !== c) : [...O, c]); 688 }, D = async (c) => { 689 if (!["image/png", "image/jpeg", "image/webp", "application/pdf"].includes(c.type)) { 690 N({ title: "Invalid File", description: "Only PNG, JPG, WebP, or PDF files are allowed.", variant: "destructive" }); 691 return; 692 } 693 if (c.size > 20 * 1024 * 1024) { 694 N({ title: "File Too Large", description: "Maximum file size is 20MB.", variant: "destructive" }); 695 return; 696 } 697 j(!0); 698 try { 699 const G = new FormData(); 700 G.append("file", c); 701 const me = await fetch(`/api/admin/competitor-database/models/${t.id}/geometry-image`, { method: "POST", credentials: "include", body: G }); 702 if (!me.ok) { 703 const W = await me.json().catch(() => ({ error: "Upload failed" })); 704 throw new Error(W.error || "Upload failed"); 705 } 706 const le = await me.json(); 707 n(le.geometryImageUrl), ne.invalidateQueries({ predicate: (W) => typeof W.queryKey[0] == "string" && W.queryKey[0].includes("competitor-database") }), N({ title: "Chart Uploaded", description: `${c.name} has been saved.` }); 708 } catch (G) { 709 N({ title: "Upload Failed", description: G.message, variant: "destructive" }); 710 } finally { 711 j(!1); 712 } 713 }, z = async () => { 714 j(!0); 715 try { 716 if (!(await fetch(`/api/admin/competitor-database/models/${t.id}/geometry-image`, { method: "DELETE", credentials: "include" })).ok) throw new Error("Failed to remove"); 717 n(""), ne.invalidateQueries({ predicate: (O) => typeof O.queryKey[0] == "string" && O.queryKey[0].includes("competitor-database") }), N({ title: "Chart Removed" }); 718 } catch (c) { 719 N({ title: "Error", description: c.message, variant: "destructive" }); 720 } finally { 721 j(!1); 722 } 723 }, Me = (c) => { 724 c.preventDefault(), v(!1); 725 const O = c.dataTransfer.files[0]; 726 O && D(O); 727 }, xe = (c) => { 728 const O = c.clipboardData.items; 729 for (let G = 0; G < O.length; G++) if (O[G].type.startsWith("image/") || O[G].type === "application/pdf") { 730 const me = O[G].getAsFile(); 731 if (me) { 732 c.preventDefault(), D(me); 733 return; 734 } 735 } 736 }, Le = x.toLowerCase().endsWith(".pdf"), Ue = async () => { 737 var rt, it; 738 const c = {}; 739 V !== t.brand && (c.brand = V), R !== t.name && (c.name = R); 740 const O = Q ? parseInt(Q, 10) : null; 741 O !== t.modelYear && (c.modelYear = O), F !== (t.bikeType ?? "") && (c.bikeType = F || null), H !== (t.sourceUrl ?? "") && (c.sourceUrl = H || null); 742 const G = t.fitStyle ?? ""; 743 o !== G && (c.fitStyle = o || null); 744 const me = t.sizingNotes ?? ""; 745 r !== me && (c.sizingNotes = r || null); 746 const le = t.additionalWheelSizes ?? []; 747 JSON.stringify([...w].sort()) !== JSON.stringify([...le].sort()) && (c.additionalWheelSizes = w); 748 const W = {}, Be = q !== "" ? parseFloat(q) : null, Ye = E !== "" ? parseFloat(E) : null, Te = Y !== "" ? parseFloat(Y) : null, Qe = t.specTireMm != null ? Math.round(t.specTireMm) : null, Je = ((rt = t.frontTireRange) == null ? void 0 : rt.max) != null ? Math.round(t.frontTireRange.max) : null, ct = ((it = t.rearTireRange) == null ? void 0 : it.max) != null ? Math.round(t.rearTireRange.max) : null; 749 if (Be !== Qe && (W.specificationTireMm = Be), Ye !== Je && (W.forkMaxTireClearance = Ye), Te !== ct && (W.maxRearTireSize = Te), Object.keys(c).length === 0 && Object.keys(W).length === 0) { 750 h(!1); 751 return; 752 } 753 if (Object.keys(c).length > 0 && T(c), Object.keys(W).length > 0) { 754 ee(!0);
755 try { 756 const ue = await fetch(`/api/admin/competitor-database/models/${t.id}/tire-sizes`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(W) }); 757 if (!ue.ok) { 758 const ze = await ue.json().catch(() => ({ error: "Failed to update tire sizes" })); 759 throw new Error(ze.error || "Failed to update tire sizes"); 760 } 761 ne.invalidateQueries({ predicate: (ze) => typeof ze.queryKey[0] == "string" && ze.queryKey[0].includes("competitor-database") }), N({ title: "Tire Sizes Updated", description: "Applied to all sizes and geometry recalculated." }), Object.keys(c).length === 0 && h(!1); 762 } catch (ue) { 763 N({ title: "Error", description: ue.message, variant: "destructive" }); 764 } finally { 765 ee(!1); 766 } 767 } 768 }; 769 return e.jsx(De, { open: m, onOpenChange: h, children: e.jsxs(Oe, { className: "max-w-md", onPaste: xe, children: [e.jsxs(Ie, { children: [e.jsx(Pe, { children: "Edit Model Details" }), e.jsxs(Ee, { children: [t.modelYear ? `${t.modelYear} ` : "", t.brand, " ", t.name] })] }), e.jsxs("div", { className: "space-y-4 py-2", children: [e.jsxs("div", { children: [e.jsx(ie, { htmlFor: "edit-model-brand", className: "text-sm font-medium", children: "Brand" }), e.jsx(ye, { id: "edit-model-brand", value: V, onChange: (c) => S(c.target.value), className: "mt-1", "data-testid": "input-edit-model-brand" })] }), e.jsxs("div", { children: [e.jsx(ie, { htmlFor: "edit-model-name", className: "text-sm font-medium", children: "Model Name" }), e.jsx(ye, { id: "edit-model-name", value: R, onChange: (c) => K(c.target.value), className: "mt-1", "data-testid": "input-edit-model-name" })] }), e.jsxs("div", { children: [e.jsx(ie, { htmlFor: "edit-model-year", className: "text-sm font-medium", children: "Model Year" }), e.jsx(ye, { id: "edit-model-year", type: "number", value: Q, onChange: (c) => L(c.target.value), placeholder: "e.g. 2025", className: "mt-1", "data-testid": "input-edit-model-year" })] }), e.jsxs("div", { children: [e.jsx(ie, { htmlFor: "edit-model-type", className: "text-sm font-medium", children: "Bike Type" }), e.jsx(ye, { id: "edit-model-type", value: F, onChange: (c) => X(c.target.value), placeholder: "e.g. Gravel, Road, MTB", className: "mt-1", "data-testid": "input-edit-model-type" })] }), e.jsxs("div", { children: [e.jsx(ie, { className: "text-sm font-medium", children: "Tire Sizes (mm)" }), e.jsx("p", { className: "text-xs text-muted-foreground mt-0.5 mb-2", children: "Applied to all sizes. Leave blank to auto-derive from trail calculation." }), e.jsxs("div", { className: "grid grid-cols-3 gap-2", children: [e.jsxs("div", { children: [e.jsx(ie, { htmlFor: "edit-model-ref-tire", className: "text-xs text-muted-foreground", children: "Ref Tire" }), e.jsx(ye, { id: "edit-model-ref-tire", type: "number", value: q, onChange: (c) => M(c.target.value), placeholder: "e.g. 25", className: "mt-0.5", "data-testid": "input-edit-model-ref-tire" })] }), e.jsxs("div", { children: [e.jsx(ie, { htmlFor: "edit-model-max-front", className: "text-xs text-muted-foreground", children: "Max Front" }), e.jsx(ye, { id: "edit-model-max-front", type: "number", value: E, onChange: (c) => U(c.target.value), placeholder: "e.g. 32", className: "mt-0.5", "data-testid": "input-edit-model-max-front" })] }), e.jsxs("div", { children: [e.jsx(ie, { htmlFor: "edit-model-max-rear", className: "text-xs text-muted-foreground", children: "Max Rear" }), e.jsx(ye, { id: "edit-model-max-rear", type: "number", value: Y, onChange: (c) => ce(c.target.value), placeholder: "e.g. 35", className: "mt-0.5", "data-testid": "input-edit-model-max-rear" })] })] })] }), e.jsxs("div", { children: [e.jsx(ie, { htmlFor: "edit-model-source-url", className: "text-sm font-medium", children: "Manufacturer Page URL" }), e.jsx(ye, { id: "edit-model-source-url", value: H, onChange: (c) => k(c.target.value), placeholder: "https://brand.com/bikes/model", className: "mt-1", "data-testid": "input-edit-model-source-url" })] }), e.jsxs("div", { children: [e.jsx(ie, { htmlFor: "edit-model-fit-style", className: "text-sm font-medium", children: "Fit Style" }), e.jsxs(St, { value: o || "unset", onValueChange: (c) => u(c === "unset" ? "" : c), children: [e.jsx(Mt, { id: "edit-model-fit-style", className: "mt-1", "data-testid": "select-edit-model-fit-style", children: e.jsx(Tt, { placeholder: "Select fit style..." }) }), e.jsxs(Rt, { children: [e.jsx(be, { value: "unset", children: "N
769ot specified" }), e.jsx(be, { value: "race", children: "Race" }), e.jsx(be, { value: "performance", children: "Performance" }), e.jsx(be, { value: "endurance", children: "Endurance" }), e.jsx(be, { value: "relaxed", children: "Relaxed" }), e.jsx(be, { value: "comfort", children: "Comfort" })] })] })] }), e.jsxs("div", { children: [e.jsx(ie, { htmlFor: "edit-model-sizing-notes", className: "text-sm font-medium", children: "Sizing Notes" }), e.jsx(pa, { id: "edit-model-sizing-notes", value: r, onChange: (c) => p(c.target.value), placeholder: "Manufacturer sizing guidance, e.g. 'Runs small \u2014 size up'", className: "mt-1 resize-none", rows: 2, "data-testid": "textarea-edit-model-sizing-notes" })] }), e.jsxs("div", { children: [e.jsx(ie, { className: "text-sm font-medium", children: "Dual Wheel Compatibility" }), e.jsx("p", { className: "text-xs text-muted-foreground mt-0.5 mb-2", children: "Additional wheel sizes this frame is designed to run (beyond its native size). Used for dirt jump and mullet-capable bikes." }), e.jsx("div", { className: "flex flex-wrap gap-3", children: ["700c", "650b", "26"].map((c) => e.jsxs("label", { className: "flex items-center gap-2 cursor-pointer select-none", "data-testid": `checkbox-additional-wheel-${c}`, children: [e.jsx("input", { type: "checkbox", checked: w.includes(c), onChange: () => a(c), className: "accent-primary" }), e.jsx("span", { className: "text-sm", children: c === "700c" ? '700c / 29"' : c === "650b" ? '650b / 27.5"' : '26"' })] }, c)) })] }), e.jsxs("div", { children: [e.jsx(ie, { className: "text-sm font-medium", children: "Geometry Chart" }), x ? e.jsxs("div", { className: "mt-1.5 space-y-2", children: [e.jsx("div", { className: "relative rounded border border-border overflow-hidden bg-muted/30", children: Le ? e.jsxs("div", { className: "flex items-center gap-2 p-3", children: [e.jsx(Ge, { className: "w-5 h-5 text-muted-foreground flex-shrink-0" }), e.jsx("span", { className: "text-sm truncate", children: x.split("/").pop() })] }) : e.jsx("img", { src: x, alt: "Geometry chart", className: "max-h-40 w-full object-contain", "data-testid": "img-edit-geometry-preview" }) }), e.jsxs("div", { className: "flex gap-2", children: [e.jsxs($, { size: "sm", variant: "outline", onClick: () => { 770 var c; 771 return (c = f.current) == null ? void 0 : c.click(); 772 }, disabled: l, "data-testid": "button-replace-geometry-chart", children: [e.jsx(Ge, { className: "w-3 h-3 mr-1" }), "Replace"] }), e.jsxs($, { size: "sm", variant: "outline", onClick: z, disabled: l, className: "text-destructive", "data-testid": "button-remove-geometry-chart", children: [e.jsx(ba, { className: "w-3 h-3 mr-1" }), "Remove"] })] })] }) : e.jsx("div", { className: `mt-1.5 border-2 border-dashed rounded-md p-6 text-center cursor-pointer transition-colors ${g ? "border-primary bg-primary/5" : "border-border hover:border-muted-foreground/50"}`, onClick: () => { 773 var c; 774 return (c = f.current) == null ? void 0 : c.click(); 775 }, onDragOver: (c) => { 776 c.preventDefault(), v(!0); 777 }, onDragLeave: () => v(!1), onDrop: Me, "data-testid": "dropzone-geometry-chart", children: l ? e.jsxs("div", { className: "flex flex-col items-center gap-2", children: [e.jsx(Z, { className: "w-6 h-6 animate-spin text-muted-foreground" }), e.jsx("span", { className: "text-sm text-muted-foreground", children: "Uploading..." })] }) : e.jsxs("div", { className: "flex flex-col items-center gap-2", children: [e.jsx(Ge, { className: "w-6 h-6 text-muted-foreground" }), e.jsx("span", { className: "text-sm text-muted-foreground", children: "Drop image/PDF here, paste from clipboard, or click to browse" }), e.jsx("span", { className: "text-xs text-muted-foreground/70", children: "PNG, JPG, WebP, or PDF up to 20MB" })] }) }), e.jsx("input", { ref: f, type: "file", accept: "image/png,image/jpeg,image/webp,application/pdf", className: "hidden", onChange: (c) => { 778 var G; 779 const O = (G = c.target.files) == null ? void 0 : G[0]; 780 O && D(O), c.target.value = ""; 781 }, "data-testid": "input-geometry-chart-file" })] })] }), e.jsxs(We, { className: "gap-2", children: [e.jsxs($, { variant: "outline", onClick: () => h(!1), disabled: A || C, children: [e.jsx(qe, { className: "w-4 h-4 mr-1" }), "Cancel"] }), e.jsxs($, { onClick: Ue, disabled: A || C, "data-testid": "button-save-model", children: [A || C ? e.jsx(Z, { className: "w-4 h-4 mr-1 animate-spin" }) : e.jsx(ka, { className: "w-4 h-4 mr-1" }), "Save Changes"] })] })] }) }); 782} 783export { _s as default };
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.