Files
ubi-dan-itam/src/app/inventory-scans/import/page.tsx
T
toanhoang 8ac95c974b
CI / test (push) Failing after 8s
Add ExpectedAsset CSV import + bot search/pick for pre-known inventory
Lets the operator upload a CSV of already-known assets (asset tag, model,
serial, old area/assignee) before an office-move walk, via a new
session-authed /inventory-scans/import page. The Telegram bot gains a
"tim trong list" search step at the asset-tag prompt: picking a match
pre-fills tag/serial and offers one-tap "giu nguyen" buttons for the
old area/assignee, then marks that row matched once the scan is created.

Follows the same-day decision to turn off local OCR (qwen2.5vl via Ollama)
for this feature -- see CLAUDE.md and the ITAM/Ollama vault notes for the
accuracy/speed numbers behind that call.

Deviates from repo convention same as the InventoryScan work above: no
Vitest coverage (manual smoke test only, via simulated webhook calls),
hand-written migration, not through the ba->pm->feature-implementer->
test-runner->code-reviewer pipeline. tsc/lint are clean.
2026-08-21 08:59:18 -04:00

85 lines
3.2 KiB
TypeScript

"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
// Pre-move CSV upload -- see src/lib/expected-assets.ts's
// parseExpectedAssetsCsv() for the accepted column names and
// prisma/schema.prisma's ExpectedAsset comment for the "why" (2026-08-21
// decision: OCR wasn't reliable/fast enough for ~300-400 assets, but
// re-typing a list that already exists in Excel is still wasted effort --
// this feeds the bot's "🔍 Tìm trong list" picker instead). Plain
// client-side File.text() + raw-text POST, no multipart/FormData -- these
// files are small (hundreds of rows) and this repo has no existing
// multipart-upload plumbing to reuse.
export default function ImportExpectedAssetsPage() {
const [status, setStatus] = useState<"idle" | "uploading" | "done" | "error">("idle");
const [message, setMessage] = useState<string | null>(null);
async function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
event.target.value = ""; // allow re-selecting the same file name after an error
if (!file) return;
setStatus("uploading");
setMessage(null);
try {
const text = await file.text();
const res = await fetch("/api/expected-assets/import", {
method: "POST",
headers: { "Content-Type": "text/csv" },
body: text,
});
const body = await res.json();
if (!res.ok) {
setStatus("error");
setMessage(body.error ?? "Import thất bại");
return;
}
setStatus("done");
setMessage(`Đã import ${body.imported} dòng.`);
} catch {
setStatus("error");
setMessage("Không đọc được file -- kiểm tra lại định dạng CSV.");
}
}
return (
<main className="mx-auto max-w-2xl space-y-4 p-6">
<h1 className="text-xl font-semibold">Import danh sách asset đã có (CSV)</h1>
<p className="text-sm text-muted-foreground">
Upload file CSV liệt kê asset đã biết từ trước (asset tag, model, khu vực/người giữ cũ).
Trong lúc đi kiểm kê, bot Telegram sẽ có nút &quot;🔍 Tìm trong list&quot; để chọn từ đây
thay vì gõ tay, và tự gợi ý khu vực/người giữ cũ.
</p>
<p className="text-sm text-muted-foreground">
Cột nhận diện theo tên (không phân biệt hoa/thường, thứ tự tuỳ ý): <code>asset tag</code>,{" "}
<code>model</code>, <code>serial</code>, <code>area</code> (khu vực cũ),{" "}
<code>assignee</code> (người giữ cũ). Dòng đầu tiên phải là header.
</p>
<div>
<input
type="file"
accept=".csv,text/csv"
onChange={handleFileChange}
disabled={status === "uploading"}
className="hidden"
id="csv-input"
/>
<Button asChild>
<label htmlFor="csv-input" className="cursor-pointer">
{status === "uploading" ? "Đang import..." : "Chọn file CSV"}
</label>
</Button>
</div>
{message ? (
<p className={status === "error" ? "text-sm text-destructive" : "text-sm text-foreground"}>
{message}
</p>
) : null}
</main>
);
}