Add AssetItem.assetTag, status-aware bulk import, nullable purchaseDate
CI / test (push) Failing after 8s

Prep work for importing a real company ITAM export (613 real assets):

- AssetItem.assetTag (nullable, unique) -- company physical tag, was only
  on InventoryScan (move feature) before, now on the actual asset record.
- AssetItem.purchaseDate is now nullable (was required) -- some legacy
  rows genuinely have no known purchase date. Manual /assets create form
  still requires it in the HTML input; this only relaxes the DB/import
  path for historical data.
- importAssetItems() accepts a status column now (free-text "In use"/
  "In stock"/etc mapped via IMPORT_STATUS_MAP), unlike manual creation
  which always defaults in_stock -- bulk-imported inventory is usually
  already deployed, not freshly purchased, so forcing in_stock would
  misrepresent reality.

Hand-written migrations (asset_tag, purchase_date nullable), no Vitest
coverage -- same deviation as the rest of this branch, see CLAUDE.md.
tsc/lint clean.

Actually imported 613/613 real AssetItem rows from the export with 0
errors after this (not committed here -- thats data, not code; see
CLAUDE.md for the seeding/import process and whats deliberately out of
scope this round: Cost, PO number, employee Assignment).
This commit is contained in:
toanhoang
2026-08-23 21:33:48 -04:00
parent aa7f5515db
commit 69437cf38f
7 changed files with 202 additions and 44 deletions
+23 -4
View File
@@ -26,8 +26,9 @@ import {
interface AssetItemRecord {
id: string;
serial_number: string;
asset_tag: string | null;
model_id: string;
purchase_date: string;
purchase_date: string | null;
warranty_end_date: string | null;
status: "in_stock" | "assigned" | "repair" | "retired";
purchase_order_id: string | null;
@@ -70,6 +71,7 @@ export default function AssetsPage() {
const { data: purchaseOrders } = useApiList<PurchaseOrderRecord>("/api/purchase-orders");
const [serialNumber, setSerialNumber] = useState("");
const [assetTag, setAssetTag] = useState("");
const [modelId, setModelId] = useState("");
const [purchaseDate, setPurchaseDate] = useState("");
const [warrantyEndDate, setWarrantyEndDate] = useState("");
@@ -126,6 +128,7 @@ export default function AssetsPage() {
setSubmitting(true);
setSubmitError(null);
const hasAssetTag = assetTag.trim() !== "";
const hasWarrantyEndDate = warrantyEndDate.trim() !== "";
const hasPurchaseOrderId = purchaseOrderId !== NO_PURCHASE_ORDER_VALUE;
@@ -140,6 +143,7 @@ export default function AssetsPage() {
// Omit optional keys entirely when left blank, rather than
// sending null — matches the create-form pattern used on other
// pages (e.g. purchase-orders page's receivedAt/receivedById).
...(hasAssetTag ? { assetTag } : {}),
...(hasWarrantyEndDate ? { warrantyEndDate } : {}),
...(hasPurchaseOrderId ? { purchaseOrderId } : {}),
}),
@@ -165,6 +169,7 @@ export default function AssetsPage() {
}
setSerialNumber("");
setAssetTag("");
setModelId("");
setPurchaseDate("");
setWarrantyEndDate("");
@@ -192,6 +197,7 @@ export default function AssetsPage() {
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Serial Number</TableHead>
<TableHead>Asset Tag</TableHead>
<TableHead>Model ID</TableHead>
<TableHead>Purchase Date</TableHead>
<TableHead>Warranty End Date</TableHead>
@@ -205,8 +211,9 @@ export default function AssetsPage() {
<TableRow key={assetItem.id}>
<TableCell>{assetItem.id}</TableCell>
<TableCell>{assetItem.serial_number}</TableCell>
<TableCell>{assetItem.asset_tag ?? "-"}</TableCell>
<TableCell>{assetItem.model_id}</TableCell>
<TableCell>{assetItem.purchase_date}</TableCell>
<TableCell>{assetItem.purchase_date ?? "-"}</TableCell>
<TableCell>{assetItem.warranty_end_date ?? "-"}</TableCell>
<TableCell>{assetItem.status}</TableCell>
<TableCell>{assetItem.purchase_order_id ?? "-"}</TableCell>
@@ -226,8 +233,11 @@ export default function AssetsPage() {
<a href="/asset-models" className="underline">
/asset-models
</a>
), <code>purchase date</code> (bắt buộc), <code>warranty end date</code>,{" "}
<code>purchase order id</code> (tuỳ chọn -- bỏ trống thì tự tính từ warranty của model).
), <code>purchase date</code> (bắt buộc -- cũng nhận <code>order received</code>).
Tuỳ chọn: <code>asset tag</code>, <code>warranty end date</code> (bỏ trống thì tự tính
từ warranty của model), <code>status</code>/<code>state</code> (nhận{" "}
<code>In use</code> → assigned, <code>In stock</code> → in_stock, bỏ trống thì mặc định
in_stock), <code>purchase order id</code>.
</p>
<div className="flex flex-wrap items-center gap-3">
<a href="/api/assets/export">
@@ -275,6 +285,15 @@ export default function AssetsPage() {
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="assetTag">Asset Tag (optional)</Label>
<Input
id="assetTag"
value={assetTag}
onChange={(event) => setAssetTag(event.target.value)}
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="modelId">Model</Label>
<Select value={modelId} onValueChange={setModelId} required>