Add AssetItem.cost/costCurrency/externalPoNumber, make import upsert-capable
CI / test (push) Failing after 9s
CI / test (push) Failing after 9s
Three new fields: cost (Decimal, not Float -- exact money math), costCurrency (free text), externalPoNumber (free text PO number from an external system, distinct from purchaseOrderId which is a real FK into this apps own PurchaseOrder table -- external exports PO numbers are messy free text with nothing to link to here). importAssetItems() now upserts: a row matching an existing assetTag or serialNumber updates that row instead of erroring on the uniqueness conflict. Needed to backfill cost/PO onto the 613 already-imported rows without recreating them, and sets this up for future re-syncs when the company re-exports their ITAM report. Update only touches fields the row actually carries a value for (blank cell = no change, not clear) -- warrantyEndDate is the one field NOT auto-recomputed from warrantyMonths on update (unlike create), so a manually-corrected date survives a re-sync. Placeholder junk (Not available/Pending/N/A/etc, case -insensitive) in PO number/cost/currency cells is treated as absent. Re-ran the full 613-row import after this: 0 created, 613 updated, 0 errors -- backfilled cost onto the 8 rows that had one and PO number onto the 2 that had a real (non-placeholder) value, verified against the source file. Hand-written migration, no Vitest coverage -- same deviation as the rest of this branch, see CLAUDE.md. tsc/lint clean.
This commit is contained in:
+67
-3
@@ -31,7 +31,10 @@ interface AssetItemRecord {
|
||||
purchase_date: string | null;
|
||||
warranty_end_date: string | null;
|
||||
status: "in_stock" | "assigned" | "repair" | "retired";
|
||||
cost: number | null;
|
||||
cost_currency: string | null;
|
||||
purchase_order_id: string | null;
|
||||
external_po_number: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -76,6 +79,9 @@ export default function AssetsPage() {
|
||||
const [purchaseDate, setPurchaseDate] = useState("");
|
||||
const [warrantyEndDate, setWarrantyEndDate] = useState("");
|
||||
const [purchaseOrderId, setPurchaseOrderId] = useState(NO_PURCHASE_ORDER_VALUE);
|
||||
const [cost, setCost] = useState("");
|
||||
const [costCurrency, setCostCurrency] = useState("");
|
||||
const [externalPoNumber, setExternalPoNumber] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
|
||||
@@ -115,7 +121,7 @@ export default function AssetsPage() {
|
||||
.join("; ") +
|
||||
(errorCount > 5 ? ` (+${errorCount - 5} lỗi khác)` : "")
|
||||
: "";
|
||||
setImportMessage(`Đã import ${body.created} asset.` + errorSummary);
|
||||
setImportMessage(`Tạo mới ${body.created}, cập nhật ${body.updated} asset.` + errorSummary);
|
||||
refetch();
|
||||
} catch {
|
||||
setImportStatus("error");
|
||||
@@ -131,6 +137,15 @@ export default function AssetsPage() {
|
||||
const hasAssetTag = assetTag.trim() !== "";
|
||||
const hasWarrantyEndDate = warrantyEndDate.trim() !== "";
|
||||
const hasPurchaseOrderId = purchaseOrderId !== NO_PURCHASE_ORDER_VALUE;
|
||||
const hasCost = cost.trim() !== "";
|
||||
const hasCostCurrency = costCurrency.trim() !== "";
|
||||
const hasExternalPoNumber = externalPoNumber.trim() !== "";
|
||||
|
||||
if (hasCost && Number.isNaN(Number(cost))) {
|
||||
setSubmitError("Cost must be a number");
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/assets", {
|
||||
@@ -146,6 +161,9 @@ export default function AssetsPage() {
|
||||
...(hasAssetTag ? { assetTag } : {}),
|
||||
...(hasWarrantyEndDate ? { warrantyEndDate } : {}),
|
||||
...(hasPurchaseOrderId ? { purchaseOrderId } : {}),
|
||||
...(hasCost ? { cost: Number(cost) } : {}),
|
||||
...(hasCostCurrency ? { costCurrency } : {}),
|
||||
...(hasExternalPoNumber ? { externalPoNumber } : {}),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -174,6 +192,9 @@ export default function AssetsPage() {
|
||||
setPurchaseDate("");
|
||||
setWarrantyEndDate("");
|
||||
setPurchaseOrderId(NO_PURCHASE_ORDER_VALUE);
|
||||
setCost("");
|
||||
setCostCurrency("");
|
||||
setExternalPoNumber("");
|
||||
refetch();
|
||||
} catch {
|
||||
setSubmitError("Request failed");
|
||||
@@ -202,7 +223,9 @@ export default function AssetsPage() {
|
||||
<TableHead>Purchase Date</TableHead>
|
||||
<TableHead>Warranty End Date</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Cost</TableHead>
|
||||
<TableHead>Purchase Order ID</TableHead>
|
||||
<TableHead>External PO Number</TableHead>
|
||||
<TableHead>Created At</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -216,7 +239,13 @@ export default function AssetsPage() {
|
||||
<TableCell>{assetItem.purchase_date ?? "-"}</TableCell>
|
||||
<TableCell>{assetItem.warranty_end_date ?? "-"}</TableCell>
|
||||
<TableCell>{assetItem.status}</TableCell>
|
||||
<TableCell>
|
||||
{assetItem.cost !== null
|
||||
? `${assetItem.cost}${assetItem.cost_currency ? " " + assetItem.cost_currency : ""}`
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell>{assetItem.purchase_order_id ?? "-"}</TableCell>
|
||||
<TableCell>{assetItem.external_po_number ?? "-"}</TableCell>
|
||||
<TableCell>{assetItem.created_at}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -233,11 +262,14 @@ export default function AssetsPage() {
|
||||
<a href="/asset-models" className="underline">
|
||||
/asset-models
|
||||
</a>
|
||||
), <code>purchase date</code> (bắt buộc -- cũng nhận <code>order received</code>).
|
||||
), <code>purchase date</code> (không 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>.
|
||||
in_stock), <code>cost</code>, <code>cost currency</code>,{" "}
|
||||
<code>po number</code> (text tự do, khác <code>purchase order id</code> là UUID trong
|
||||
hệ thống). Dòng có <code>asset tag</code> hoặc <code>serial number</code> đã tồn tại sẽ{" "}
|
||||
<b>cập nhật</b> thay vì báo lỗi trùng -- import lại file mới để đồng bộ dữ liệu.
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<a href="/api/assets/export">
|
||||
@@ -348,6 +380,38 @@ export default function AssetsPage() {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="cost">Cost (optional)</Label>
|
||||
<Input
|
||||
id="cost"
|
||||
type="number"
|
||||
min={0}
|
||||
step="0.01"
|
||||
value={cost}
|
||||
onChange={(event) => setCost(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="costCurrency">Cost Currency (optional)</Label>
|
||||
<Input
|
||||
id="costCurrency"
|
||||
value={costCurrency}
|
||||
onChange={(event) => setCostCurrency(event.target.value)}
|
||||
placeholder="e.g. VND, USD"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="externalPoNumber">External PO Number (optional)</Label>
|
||||
<Input
|
||||
id="externalPoNumber"
|
||||
value={externalPoNumber}
|
||||
onChange={(event) => setExternalPoNumber(event.target.value)}
|
||||
placeholder="PO number from an external system, if any"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{submitError ? <p className="text-sm text-destructive">{submitError}</p> : null}
|
||||
|
||||
<Button type="submit" disabled={submitting}>
|
||||
|
||||
Reference in New Issue
Block a user