feat: duplicate product detection — STEP conflict warnings on Excel import and CAD upload

- Excel preview detects when a product already has a different STEP file linked
- Excel preview detects intra-Excel conflicts (same product, different CAD model names)
- Product STEP upload warns when replacing an existing file and shows render count
- All warnings are non-blocking (amber badges, toast warnings)
- LEARNINGS.md: all open items resolved

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 13:05:40 +01:00
parent f0dd952f63
commit b6bac080bb
10 changed files with 207 additions and 173 deletions
+2
View File
@@ -126,6 +126,8 @@ export interface ProductCadUploadResponse {
file_hash: string
status: string
product_id: string
warnings?: string[]
existing_render_count?: number
}
export async function uploadProductCad(id: string, file: File): Promise<ProductCadUploadResponse> {
+8
View File
@@ -13,6 +13,12 @@ export interface ExcelPreviewRow {
has_step: boolean
is_duplicate: boolean
duplicate_of_row: number | null
step_conflict: boolean
step_conflict_existing_name: string | null
step_conflict_excel_name: string | null
cad_name_conflict: boolean
cad_name_conflict_other_name: string | null
cad_name_conflict_row: number | null
}
export interface ExcelPreviewResult {
@@ -26,6 +32,8 @@ export interface ExcelPreviewResult {
has_step_count: number
no_step_count: number
duplicate_count: number
step_conflict_count: number
cad_name_conflict_count: number
warnings: string[]
rows: ExcelPreviewRow[]
column_headers: string[]
+9 -1
View File
@@ -349,8 +349,16 @@ export default function ProductDetailPage() {
const cadUploadMut = useMutation({
mutationFn: (file: File) => uploadProductCad(id!, file),
onSuccess: () => {
onSuccess: (data) => {
toast.success('STEP file uploaded — processing started')
if (data.warnings && data.warnings.length > 0) {
data.warnings.forEach((w) => toast.warning(w))
}
if (data.existing_render_count && data.existing_render_count > 0) {
toast.warning(
`This product has ${data.existing_render_count} existing render${data.existing_render_count !== 1 ? 's' : ''} from the previous STEP file. Consider re-rendering.`,
)
}
qc.invalidateQueries({ queryKey: ['product', id] })
},
onError: (e: any) => toast.error(e.response?.data?.detail || 'Upload failed'),
+32 -6
View File
@@ -4,7 +4,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import {
FileSpreadsheet, CheckCircle, X, Plus,
Info, PackagePlus, PackageCheck, Ban, FileBox, ArrowRight, Copy,
Info, PackagePlus, PackageCheck, Ban, FileBox, ArrowRight, Copy, AlertTriangle,
} from 'lucide-react'
import { toast } from 'sonner'
import { uploadExcel, finalizeExcelImport, getImportValidation } from '../api/uploads'
@@ -335,6 +335,20 @@ export default function UploadPage() {
description="Same Product Series appears multiple times. Pre-unchecked — only first occurrence imported."
color="bg-status-warning-bg border-border-default text-status-warning-text"
/>
<StatCard
icon={<AlertTriangle size={18} className="text-amber-600" />}
value={previewResult.step_conflict_count ?? 0}
label="STEP conflicts"
description="Product exists with a different STEP file than referenced in Excel."
color="bg-amber-50 border-amber-200 text-amber-800"
/>
<StatCard
icon={<AlertTriangle size={18} className="text-amber-600" />}
value={previewResult.cad_name_conflict_count ?? 0}
label="CAD name conflicts"
description="Same product appears in multiple rows with different CAD model names."
color="bg-amber-50 border-amber-200 text-amber-800"
/>
</div>
</div>
@@ -459,11 +473,23 @@ export default function UploadPage() {
)}
</td>
<td className="px-4 py-2 text-center">
{!hasId ? null : row.has_step ? (
<CheckCircle size={14} className="text-green-500 mx-auto" aria-label="STEP file linked" />
) : (
<X size={14} className="text-red-400 mx-auto" aria-label="No STEP file" />
)}
<div className="flex items-center justify-center gap-1">
{!hasId ? null : row.has_step ? (
<CheckCircle size={14} className="text-green-500" aria-label="STEP file linked" />
) : (
<X size={14} className="text-red-400" aria-label="No STEP file" />
)}
{row.step_conflict && (
<span title={`STEP conflict: DB has "${row.step_conflict_existing_name}", Excel references "${row.step_conflict_excel_name}"`}>
<AlertTriangle size={14} className="text-amber-500" />
</span>
)}
{row.cad_name_conflict && (
<span title={`CAD name conflict with row ${row.cad_name_conflict_row}: "${row.cad_name_conflict_other_name}"`}>
<AlertTriangle size={14} className="text-amber-500" />
</span>
)}
</div>
</td>
</tr>
)