+
+
+
{d.supplierName || d.fileName}
+
{d.status}
+
+
+ {d.supplyId && (
+
+ )}
+ {d.status === 'failed' && d.error && (
+
+ Ошибка распознавания: {d.error}
+
+ )}
+
+ {/* шапка */}
+
+
+ {/* сканер в общее поле */}
+
+
+
+
+ {unmatched > 0 && (
+
+ Не сопоставлено строк: {unmatched} из {lines.length}. Карточки подсвечены оранжевым.
+
+ )}
+
+ {/* строки */}
+
+ {lines.map((l, i) => (
+ setLines(prev => prev.map((x, j) => j === i ? patched : x))}
+ onRemove={() => setLines(prev => prev.filter((_, j) => j !== i))}
+ onSearch={(q) => setSearchHit({ row: i, q })}
+ />
+ ))}
+
+
+ {/* bottom */}
+
+
+
+
+
+
+
+ {searchHit && (
+
setSearchHit(null)}
+ onPick={(h) => {
+ setLines(prev => prev.map((x, j) => j === searchHit.row
+ ? { ...x, productId: h.id, productName: h.name, matchedBy: 'manual' } : x))
+ setSearchHit(null)
+ }} />
+ )}
+
+ {applyOpen && (
+ setApplyOpen(false)}
+ onDone={(supplyId) => { setApplyOpen(false); nav(`/purchases/supplies/${supplyId}`) }} />
+ )}
+
+ )
+}
+
+/// Карточка строки. Оранжевый бордер если не сопоставлена.
+function InvoiceLineCard({ line, onChange, onRemove, onSearch }: {
+ line: InvoiceLine
+ onChange: (l: InvoiceLine) => void
+ onRemove: () => void
+ onSearch: (q: string) => void
+}) {
+ const bad = !line.productId
+ const sum = line.quantity * line.price
+ return (
+