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:
@@ -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> {
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user