feat(assets): add asset metadata views and enforce assignment transitions

This commit is contained in:
2026-06-19 17:14:22 +02:00
parent c1763ed007
commit f32d55a7b0
17 changed files with 1573 additions and 70 deletions
@@ -0,0 +1,202 @@
import { createElement } from "react"
import { renderToStaticMarkup } from "react-dom/server"
import { beforeEach, describe, expect, it, vi } from "vitest"
const mocks = vi.hoisted(() => ({
useRouter: vi.fn(() => ({ push: vi.fn() })),
updateAssetAction: vi.fn(),
}))
vi.mock("next/navigation", () => ({
useRouter: mocks.useRouter,
}))
vi.mock("@/actions/asset.actions", () => ({
updateAssetAction: mocks.updateAssetAction,
}))
describe("edit asset form", () => {
beforeEach(() => {
vi.clearAllMocks()
})
it("renders the operational asset fields and current values on the edit form", async () => {
const { default: EditAssetForm } = await import(
"@/app/(dashboard)/inventory/assets/_components/edit.asset.form"
)
const html = renderToStaticMarkup(
createElement(EditAssetForm, {
asset: {
id: "asset-1",
itemId: "item-1",
serialNumber: "SERIAL-1",
status: "AVAILABLE",
assetTag: "IT-000777",
manufacturer: "Lenovo",
model: "ThinkPad X1",
purchaseDate: new Date("2026-01-15T00:00:00.000Z"),
purchasePrice: 1249.99,
warrantyEndsAt: new Date("2028-01-15T00:00:00.000Z"),
deliveryNote: null,
notes: null,
personId: null,
item: null,
assignment: null,
},
items: [{ id: "item-1", name: "Laptop" }],
people: [{ id: "person-1", firstName: "Ada", lastName: "Lovelace" }],
formCopy: {
itemLabel: "Item",
itemPlaceholder: "Select an item",
serialNumberLabel: "Serial Number",
serialNumberPlaceholder: "Serial number",
deliveryNoteLabel: "Delivery Note",
deliveryNotePlaceholder: "Delivery note",
statusLabel: "Status",
statusPlaceholder: "Select a status",
personLabel: "Person",
personPlaceholder: "Select a person",
assetTagLabel: "Asset Tag",
assetTagPlaceholder: "Asset tag",
manufacturerLabel: "Manufacturer",
manufacturerPlaceholder: "Manufacturer",
modelLabel: "Model",
modelPlaceholder: "Model",
purchaseDateLabel: "Purchase Date",
purchaseDatePlaceholder: "YYYY-MM-DD",
purchasePriceLabel: "Purchase Price",
purchasePricePlaceholder: "0.00",
warrantyEndsAtLabel: "Warranty Ends At",
warrantyEndsAtPlaceholder: "YYYY-MM-DD",
createSubmit: "Create Asset",
updateSubmit: "Update Asset",
},
schemaCopy: {
itemRequired: "Item is required",
serialNumberRequired: "Serial number is required",
idRequired: "ID is required",
statusRequired: "Status is required",
invalidCreateStatus: "Status must be Available or Assigned",
invalidUpdateStatus: "Invalid status",
personRequired: "Person is required",
},
statusCopy: {
AVAILABLE: "Available",
ASSIGNED: "Assigned",
IN_REPAIR: "In repair",
BROKEN: "Broken",
LOST: "Lost",
STOLEN: "Stolen",
DISPOSED: "Disposed",
RETIRED: "Retired",
},
submitButtonCopy: {
defaultLabel: "Submit",
processing: "Processing",
success: "Success",
},
}),
)
expect(html).toContain("Asset Tag")
expect(html).toContain("IT-000777")
expect(html).toContain("Manufacturer")
expect(html).toContain("Lenovo")
expect(html).toContain("Purchase Price")
})
it("exposes every backend-supported update status", async () => {
const { default: EditAssetForm } = await import(
"@/app/(dashboard)/inventory/assets/_components/edit.asset.form"
)
const html = renderToStaticMarkup(
createElement(EditAssetForm, {
asset: {
id: "asset-1",
itemId: "item-1",
serialNumber: "SERIAL-1",
status: "AVAILABLE",
assetTag: null,
manufacturer: null,
model: null,
purchaseDate: null,
purchasePrice: null,
warrantyEndsAt: null,
deliveryNote: null,
notes: null,
personId: null,
item: null,
assignment: null,
},
items: [{ id: "item-1", name: "Laptop" }],
people: [],
formCopy: {
itemLabel: "Item",
itemPlaceholder: "Select an item",
serialNumberLabel: "Serial Number",
serialNumberPlaceholder: "Serial number",
deliveryNoteLabel: "Delivery Note",
deliveryNotePlaceholder: "Delivery note",
statusLabel: "Status",
statusPlaceholder: "Select a status",
personLabel: "Person",
personPlaceholder: "Select a person",
assetTagLabel: "Asset Tag",
assetTagPlaceholder: "Asset tag",
manufacturerLabel: "Manufacturer",
manufacturerPlaceholder: "Manufacturer",
modelLabel: "Model",
modelPlaceholder: "Model",
purchaseDateLabel: "Purchase Date",
purchaseDatePlaceholder: "YYYY-MM-DD",
purchasePriceLabel: "Purchase Price",
purchasePricePlaceholder: "0.00",
warrantyEndsAtLabel: "Warranty Ends At",
warrantyEndsAtPlaceholder: "YYYY-MM-DD",
createSubmit: "Create Asset",
updateSubmit: "Update Asset",
},
schemaCopy: {
itemRequired: "Item is required",
serialNumberRequired: "Serial number is required",
idRequired: "ID is required",
statusRequired: "Status is required",
invalidCreateStatus: "Status must be Available or Assigned",
invalidUpdateStatus: "Invalid status",
personRequired: "Person is required",
},
statusCopy: {
AVAILABLE: "Available",
ASSIGNED: "Assigned",
IN_REPAIR: "In repair",
BROKEN: "Broken",
LOST: "Lost",
STOLEN: "Stolen",
DISPOSED: "Disposed",
RETIRED: "Retired",
},
submitButtonCopy: {
defaultLabel: "Submit",
processing: "Processing",
success: "Success",
},
}),
)
for (const status of [
"AVAILABLE",
"ASSIGNED",
"IN_REPAIR",
"BROKEN",
"LOST",
"STOLEN",
"DISPOSED",
"RETIRED",
]) {
expect(html).toContain(`value="${status}"`)
}
expect(html).not.toContain('value="RESERVED"')
})
})
@@ -0,0 +1,160 @@
import { createElement } from "react"
import { renderToStaticMarkup } from "react-dom/server"
import { beforeEach, describe, expect, it, vi } from "vitest"
const mocks = vi.hoisted(() => ({
useRouter: vi.fn(() => ({ push: vi.fn() })),
createAssetAction: vi.fn(),
}))
vi.mock("next/navigation", () => ({
useRouter: mocks.useRouter,
}))
vi.mock("@/actions/asset.actions", () => ({
createAssetAction: mocks.createAssetAction,
}))
describe("new asset form", () => {
beforeEach(() => {
vi.clearAllMocks()
})
it("renders the operational asset fields on the create form", async () => {
const { default: NewAssetForm } = await import(
"@/app/(dashboard)/inventory/assets/_components/new.asset.form"
)
const html = renderToStaticMarkup(
createElement(NewAssetForm, {
items: [{ id: "item-1", name: "Laptop" }],
people: [{ id: "person-1", firstName: "Ada", lastName: "Lovelace" }],
formCopy: {
itemLabel: "Item",
itemPlaceholder: "Select an item",
serialNumberLabel: "Serial Number",
serialNumberPlaceholder: "Serial number",
deliveryNoteLabel: "Delivery Note",
deliveryNotePlaceholder: "Delivery note",
statusLabel: "Status",
statusPlaceholder: "Select a status",
personLabel: "Person",
personPlaceholder: "Select a person",
assetTagLabel: "Asset Tag",
assetTagPlaceholder: "Asset tag",
manufacturerLabel: "Manufacturer",
manufacturerPlaceholder: "Manufacturer",
modelLabel: "Model",
modelPlaceholder: "Model",
purchaseDateLabel: "Purchase Date",
purchaseDatePlaceholder: "YYYY-MM-DD",
purchasePriceLabel: "Purchase Price",
purchasePricePlaceholder: "0.00",
warrantyEndsAtLabel: "Warranty Ends At",
warrantyEndsAtPlaceholder: "YYYY-MM-DD",
createSubmit: "Create Asset",
updateSubmit: "Update Asset",
},
schemaCopy: {
itemRequired: "Item is required",
serialNumberRequired: "Serial number is required",
idRequired: "ID is required",
statusRequired: "Status is required",
invalidCreateStatus: "Status must be Available or Assigned",
invalidUpdateStatus: "Invalid status",
personRequired: "Person is required",
},
statusCopy: {
AVAILABLE: "Available",
ASSIGNED: "Assigned",
IN_REPAIR: "In repair",
BROKEN: "Broken",
LOST: "Lost",
STOLEN: "Stolen",
DISPOSED: "Disposed",
RETIRED: "Retired",
},
submitButtonCopy: {
defaultLabel: "Submit",
processing: "Processing",
success: "Success",
},
}),
)
expect(html).toContain("Asset Tag")
expect(html).toContain("Manufacturer")
expect(html).toContain("Model")
expect(html).toContain("Purchase Date")
expect(html).toContain("Purchase Price")
expect(html).toContain("Warranty Ends At")
})
it("only exposes create-supported asset statuses", async () => {
const { default: NewAssetForm } = await import(
"@/app/(dashboard)/inventory/assets/_components/new.asset.form"
)
const html = renderToStaticMarkup(
createElement(NewAssetForm, {
items: [{ id: "item-1", name: "Laptop" }],
people: [],
formCopy: {
itemLabel: "Item",
itemPlaceholder: "Select an item",
serialNumberLabel: "Serial Number",
serialNumberPlaceholder: "Serial number",
deliveryNoteLabel: "Delivery Note",
deliveryNotePlaceholder: "Delivery note",
statusLabel: "Status",
statusPlaceholder: "Select a status",
personLabel: "Person",
personPlaceholder: "Select a person",
assetTagLabel: "Asset Tag",
assetTagPlaceholder: "Asset tag",
manufacturerLabel: "Manufacturer",
manufacturerPlaceholder: "Manufacturer",
modelLabel: "Model",
modelPlaceholder: "Model",
purchaseDateLabel: "Purchase Date",
purchaseDatePlaceholder: "YYYY-MM-DD",
purchasePriceLabel: "Purchase Price",
purchasePricePlaceholder: "0.00",
warrantyEndsAtLabel: "Warranty Ends At",
warrantyEndsAtPlaceholder: "YYYY-MM-DD",
createSubmit: "Create Asset",
updateSubmit: "Update Asset",
},
schemaCopy: {
itemRequired: "Item is required",
serialNumberRequired: "Serial number is required",
idRequired: "ID is required",
statusRequired: "Status is required",
invalidCreateStatus: "Status must be Available or Assigned",
invalidUpdateStatus: "Invalid status",
personRequired: "Person is required",
},
statusCopy: {
AVAILABLE: "Available",
ASSIGNED: "Assigned",
IN_REPAIR: "In repair",
BROKEN: "Broken",
LOST: "Lost",
STOLEN: "Stolen",
DISPOSED: "Disposed",
RETIRED: "Retired",
},
submitButtonCopy: {
defaultLabel: "Submit",
processing: "Processing",
success: "Success",
},
}),
)
expect(html).toContain('value="AVAILABLE"')
expect(html).toContain('value="ASSIGNED"')
expect(html).not.toContain('value="IN_REPAIR"')
expect(html).not.toContain('value="BROKEN"')
})
})