CI / test (push) Failing after 8s
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.
85 lines
3.2 KiB
TypeScript
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 "🔍 Tìm trong list" để 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>
|
|
);
|
|
}
|