Commit Graph
24 Commits
Author SHA1 Message Date
toanhoang 2cbe746bde Add AssetItem.cost/costCurrency/externalPoNumber, make import upsert-capable
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.
2026-08-23 22:14:48 -04:00
toanhoang 69437cf38f 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).
2026-08-23 21:33:48 -04:00
toanhoang d9b360c1df Auto-fill AssetItem.warrantyEndDate from AssetModel.warrantyMonths
CI / test (push) Failing after 8s
AssetModel gains an optional warrantyMonths (standard vendor warranty
length). createAssetItem() now defaults warrantyEndDate to purchaseDate +
warrantyMonths (month-end clamped, e.g. Dec 31 + 2mo -> Feb 28, not Mar 3)
whenever the caller omits warrantyEndDate -- still overridable per item
for batches with extended/different warranty terms. /asset-models UI gets
an input for the new field; /assets needs no change since it already
omits warrantyEndDate from the payload when left blank.

Hand-written migration + manual smoke test (tsx script, not Vitest),
same deviation as the other 2026-08-2x work in this branch -- see
CLAUDE.md. tsc/lint clean.
2026-08-21 09:10:55 -04:00
toanhoang 8ac95c974b Add ExpectedAsset CSV import + bot search/pick for pre-known inventory
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.
2026-08-21 08:59:18 -04:00
toanhoangandClaude Sonnet 5 3078d9e8c5 feat: add Area and InventoryScan Prisma models + migrations
Office-move inventory kiem ke feature. See docs/asset-license-management.md InventoryScan section for full business rationale (Area vs Seat, why not AssetItem, asset tag/serial nullable).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-19 22:06:52 -04:00
toanhoangandClaude Sonnet 5 b702528e19 fix: change Assignment.assetItemId/licenseId FK onDelete to RESTRICT
Was implicitly SET NULL (Prisma default for nullable FK columns),
inconsistent with employeeId's RESTRICT. Once a delete endpoint for
AssetItem/License exists, SET NULL would silently null out historical
Assignment rows on delete, erasing what was assigned and re-violating
the assetItemId/licenseId XOR retroactively. No delete endpoint exists
yet, so no real data was ever affected by the old behavior.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-08-04 06:41:13 -04:00
toanhoang 5089bd6dfc feat: add MaintenanceRecord Prisma model + migration
New entity tracking repair/warranty/preventive maintenance jobs for an
AssetItem. Reuses Vendor (nullable) for the repair location, no cost
field (mirrors PurchaseOrder). "At most 1 open record per AssetItem"
invariant left to the application layer, mirroring Assignment.
2026-08-04 05:25:22 -04:00
toanhoang 07b01cd707 feat: add Assignment Prisma model + migration
Links Employee to exactly one of AssetItem/License (XOR enforced at the
Zod layer, not built yet) via nullable FKs, plus assignedAt/returnedAt/
signedAt timestamps. See docs/asset-license-management.md's "Assignment"
section for the full spec.
2026-08-02 21:13:46 -04:00
toanhoang 2a5792df67 fix: retrofit AssetItem.model to modelId FK
AssetItem.model was a free-text string with no referential integrity.
Now a required FK to AssetModel, mirroring the Request.employeeId and
License.vendorId retrofit precedent (no backfill needed, no real
AssetItem data existed).
2026-08-02 21:07:12 -04:00
toanhoang c02235f381 feat: add AssetModel Prisma model + migration 2026-08-02 20:46:48 -04:00
toanhoang f7b9566b04 feat: add AssetCategoryAttributeDef Prisma model + migration
Per-category dynamic attribute definitions (key/label/dataType/required/
sortOrder), FK to AssetCategory, composite unique (categoryId, key) so the
same key is allowed across different categories but not duplicated within
one. dataType is a new closed Prisma enum (AssetAttributeDataType), a
dev-defined technical set distinct from AssetCategoryValueGroup.
2026-08-02 20:38:06 -04:00
toanhoang 69fa00012b feat: add AssetCategory Prisma model + migration 2026-08-02 20:30:38 -04:00
toanhoang 65232fc62c feat: add Request.purchaseOrderId (nullable FK) 2026-08-02 20:24:59 -04:00
toanhoang abec58d239 feat: add License.purchaseOrderId (nullable FK)
Nullable FK from License to PurchaseOrder for supply-side traceability
(which PO a license came in through), mirroring AssetItem.purchaseOrderId's
just-merged pattern exactly. Separate commit from the earlier
License.vendorId retrofit per the project's 1-commit-per-business-change
convention.
2026-08-02 20:17:09 -04:00
toanhoang a6d7c7b302 feat: add AssetItem.purchaseOrderId (nullable FK) 2026-08-02 20:09:06 -04:00
toanhoang 58235112f0 feat: add PurchaseOrder Prisma model + migration 2026-08-02 19:57:58 -04:00
toanhoang 9d99ebb425 fix: retrofit License.vendor to vendorId FK + createLicense error handling
License.vendor (free-text string) is now vendorId, a required FK to
Vendor, per docs/asset-license-management.md's "## Vendor (chốt
2026-08-02)" spec. Mirrors the Request.employeeId string->FK precedent:
Zod validates UUID format only, Prisma's own FK constraint (P2003) is
caught at insert time and translated into a clean 400 instead of a raw
500. No License data existed yet, so this is a pure column-type
migration, no backfill needed.

Also converts createLicense() to the {ok,value}|{ok,error} result
pattern used elsewhere (createRequest, createAssetItem) instead of
returning a bare LicenseRecord promise - closes the gap flagged in
CLAUDE.md now that License has its first FK-violation error case to
handle.
2026-08-02 19:41:33 -04:00
toanhoang 07ef1582d3 feat: add Vendor Prisma model + migration 2026-08-02 19:28:09 -04:00
toanhoang 96088421ba feat: add License Prisma model + migration 2026-08-02 08:54:53 -04:00
toanhoang 09b54f3c6b feat: add AssetItem Prisma model + migration 2026-07-31 16:04:01 +07:00
toanhoang 49d96b77e8 feat: relate Request.employee_id to Employee via FK
Retrofits Request.employeeId from a free-text String with no
referential integrity to a real relation: employeeId String @db.Uuid
+ employee Employee @relation(...), plus the inverse Employee.requests
back-relation field. Migration alters requests.employee_id from TEXT
to UUID with an FK constraint (drop+recreate, confirmed empty table -
no production/dev data existed to migrate).
2026-07-31 14:58:47 +07:00
toanhoang 4c69bcf2c1 feat: add Employee Prisma model + migration
Employee directory entity per docs/asset-license-management.md's data
model table (id, name, department, manager). department stays a plain
String for now (not a separate Department entity). managerId is a
nullable self-relation (EmployeeManager) - top-level employees have
managerId = null.
2026-07-31 14:53:57 +07:00
toanhoang 41de13b1ff feat: add User Prisma model + migration 2026-07-31 11:48:55 +07:00
toanhoang afdbdbdcfb Initial commit: Next.js scaffold, Request entity, Prisma+Zod, Tailwind v4+shadcn
- Next.js 14 (TS, App Router) monolith
- Request entity (Draft state) via Prisma + Postgres, validated with Zod
- ESLint configured
- Tailwind v4 + shadcn/ui (new-york-v4 style)
- Vitest test suite (6 tests, runs against throwaway Postgres DB per file)
- Multi-agent dev workflow docs in CLAUDE.md and .claude/agents/
2026-07-30 18:14:36 +07:00