diff --git a/apps/web/src/pages/AdminHomePage.vue b/apps/web/src/pages/AdminHomePage.vue
index 466aa3f..9a2b862 100644
--- a/apps/web/src/pages/AdminHomePage.vue
+++ b/apps/web/src/pages/AdminHomePage.vue
@@ -7,12 +7,14 @@ import AdminListingStatus from '@/components/molecules/AdminListingStatus.vue';
import { createAdminInventory, createAdminProduct, listAdminCategories, listAdminConditions, listAdminInventory, listAdminProducts } from '@/services/admin-catalog-service.js';
import { createAdminListing, listAdminListings, markListingPending, publishAdminListing } from '@/services/admin-listing-service.js';
import { createAdminSale, listAdminSales, voidAdminSale } from '@/services/admin-sale-service.js';
+import { createAdminAcquisition, listAdminAcquisitions } from '@/services/admin-acquisition-service.js';
const { principal } = useAuth();
const products = ref([]);
const inventory = ref([]);
const listings = ref([]);
const sales = ref([]);
+const acquisitions = ref([]);
const categories = ref([]);
const conditions = ref([]);
const isLoading = ref(true);
@@ -28,6 +30,12 @@ const saleInventoryId = ref('');
const soldPrice = ref('');
const costBasis = ref('');
const saleDate = ref(new Date().toISOString().slice(0, 10));
+const acquisitionInventoryId = ref('');
+const acquisitionMethod = ref('PURCHASE');
+const acquisitionCost = ref('');
+const acquisitionShipping = ref('');
+const acquisitionTax = ref('');
+const acquisitionDate = ref(new Date().toISOString().slice(0, 10));
const voidReasons = ref({});
function csrfToken() {
@@ -37,7 +45,7 @@ function csrfToken() {
async function load() {
isLoading.value = true;
try {
- [products.value, inventory.value, categories.value, conditions.value, listings.value, sales.value] = await Promise.all([listAdminProducts(), listAdminInventory(), listAdminCategories(), listAdminConditions(), listAdminListings(), listAdminSales()]);
+ [products.value, inventory.value, categories.value, conditions.value, listings.value, sales.value, acquisitions.value] = await Promise.all([listAdminProducts(), listAdminInventory(), listAdminCategories(), listAdminConditions(), listAdminListings(), listAdminSales(), listAdminAcquisitions()]);
} catch (requestError) { error.value = requestError; }
finally { isLoading.value = false; }
}
@@ -66,8 +74,8 @@ async function createListing() {
const product = products.value.find((candidate) => candidate.id === item?.productId);
isSaving.value = true;
try {
- const listing = await createAdminListing({ inventoryItemId: item.id, productName: product.name, categoryId: product.categoryId, conditionId: item.conditionId, askingPriceMinor: Number(askingPrice.value) }, csrfToken());
- listings.value.push(listing);
+ await createAdminListing({ inventoryItemId: item.id, productName: product.name, categoryId: product.categoryId, conditionId: item.conditionId, askingPriceMinor: Number(askingPrice.value) }, csrfToken());
+ listings.value = await listAdminListings();
askingPrice.value = '';
} catch (requestError) { error.value = requestError; }
finally { isSaving.value = false; }
@@ -102,6 +110,26 @@ async function recordSale() {
finally { isSaving.value = false; }
}
+async function recordAcquisition() {
+ isSaving.value = true;
+ try {
+ const acquisition = await createAdminAcquisition({
+ inventoryItemId: acquisitionInventoryId.value,
+ method: acquisitionMethod.value,
+ costMinor: Number(acquisitionCost.value),
+ shippingMinor: Number(acquisitionShipping.value || 0),
+ taxMinor: Number(acquisitionTax.value || 0),
+ date: acquisitionDate.value,
+ }, csrfToken());
+ acquisitions.value.unshift(acquisition);
+ acquisitionInventoryId.value = '';
+ acquisitionCost.value = '';
+ acquisitionShipping.value = '';
+ acquisitionTax.value = '';
+ } catch (requestError) { error.value = requestError; }
+ finally { isSaving.value = false; }
+}
+
async function voidSale(saleId) {
isSaving.value = true;
try {
@@ -178,6 +206,25 @@ onMounted(load);
+
@@ -188,7 +235,7 @@ onMounted(load);
Physical inventory
No physical copies yet.
- - {{ item.id }} · {{ item.status }} Manage images
+ - {{ item.id }} · {{ item.status }} Manage images
Listings
@@ -213,5 +260,18 @@ onMounted(load);
+
+ Acquisition history
+ No acquisitions recorded yet.
+
+ -
+ {{ acquisition.inventoryItemId }}
+ · {{ acquisition.date ?? 'Undated' }}
+ · {{ acquisition.method }}
+ · Cost ${{ (acquisition.costMinor / 100).toFixed(2) }}
+ · Basis ${{ ((acquisition.costBasisMinor ?? 0) / 100).toFixed(2) }}
+
+
+
diff --git a/apps/web/src/services/admin-acquisition-service.js b/apps/web/src/services/admin-acquisition-service.js
new file mode 100644
index 0000000..02167e3
--- /dev/null
+++ b/apps/web/src/services/admin-acquisition-service.js
@@ -0,0 +1,24 @@
+const apiBaseUrl = import.meta.env?.VITE_API_BASE_URL ?? '/api/v1';
+
+async function request(path, options = {}) {
+ const response = await fetch(`${apiBaseUrl}${path}`, {
+ credentials: 'include',
+ ...options,
+ headers: { ...(options.body ? { 'content-type': 'application/json' } : {}), ...options.headers },
+ });
+ const payload = await response.json().catch(() => null);
+ if (!response.ok) throw new Error(payload?.error?.message ?? 'Acquisition request failed.');
+ return payload?.data;
+}
+
+export function listAdminAcquisitions() {
+ return request('/admin/acquisitions');
+}
+
+export function createAdminAcquisition(input, csrfToken) {
+ return request('/admin/acquisitions', {
+ method: 'POST',
+ headers: { 'x-csrf-token': csrfToken },
+ body: JSON.stringify(input),
+ });
+}
diff --git a/apps/web/tests/e2e/admin-catalog-workflow.spec.js b/apps/web/tests/e2e/admin-catalog-workflow.spec.js
index 32b9694..90193a1 100644
--- a/apps/web/tests/e2e/admin-catalog-workflow.spec.js
+++ b/apps/web/tests/e2e/admin-catalog-workflow.spec.js
@@ -14,21 +14,33 @@ test('signs in and publishes a listing after adding an inventory image', async (
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/\/admin$/);
await expect(page.getByRole('heading', { name: 'Collector workspace' })).toBeVisible();
+ await expect(page.getByRole('status', { name: 'Loading inventory...' })).toHaveCount(0);
const category = page.locator('#product-category option').nth(1);
await expect(category).toBeAttached();
- await page.getByLabel('Name').fill(`Browser test collectible ${Date.now()}`);
+ const productName = `Browser test collectible ${Date.now()}`;
+ await page.getByLabel('Name').fill(productName);
await page.getByLabel('Category').selectOption({ index: 1 });
await page.getByRole('button', { name: 'Create product' }).click();
await expect(page.locator('#inventory-product option').nth(1)).toBeAttached();
- await page.getByLabel('Product', { exact: true }).selectOption({ index: 1 });
+ const createdProductId = await page.getByLabel('Product', { exact: true }).locator('option', { hasText: productName }).getAttribute('value');
+ await page.getByLabel('Product', { exact: true }).selectOption(createdProductId);
await page.getByLabel('Condition', { exact: true }).selectOption({ index: 1 });
await page.getByRole('button', { name: 'Create copy' }).click();
- const inventoryLink = page.locator('section[aria-labelledby="inventory-title"]').getByRole('link', { name: 'Manage images' }).last();
+ const createdInventoryRow = page.locator(`section[aria-labelledby="inventory-title"] li[data-product-id="${createdProductId}"]`).last();
+ const createdInventoryId = await createdInventoryRow.locator('a').getAttribute('href').then((href) => href.split('/').at(-2));
+ const inventoryLink = page.locator(`a[href="/admin/inventory/${createdInventoryId}/images"]`);
await expect(inventoryLink).toBeVisible();
- const createdInventoryId = await page.getByLabel('Inventory copy').locator('option').last().getAttribute('value');
+ await page.locator('#acquisition-inventory').selectOption(createdInventoryId);
+ await page.getByLabel('Method').fill('PURCHASE');
+ await page.getByLabel('Acquisition date').fill('2026-09-09');
+ await page.getByLabel('Cost (cents)').fill('1000');
+ await page.getByLabel('Shipping (cents)').fill('100');
+ await page.getByLabel('Tax (cents)').fill('50');
+ await page.getByRole('button', { name: 'Record acquisition' }).click();
+ await expect(page.locator('section[aria-labelledby="acquisitions-title"] li').first()).toContainText('Basis $11.50');
await inventoryLink.click();
await expect(page).toHaveURL(/\/admin\/inventory\/[^/]+\/images$/);
await expect(page.getByRole('heading', { name: 'Manage images' })).toBeVisible();
@@ -44,11 +56,12 @@ test('signs in and publishes a listing after adding an inventory image', async (
await page.getByLabel('Inventory copy', { exact: true }).first().selectOption(createdInventoryId);
await page.getByLabel('Asking price (cents)').fill('2499');
await page.getByRole('button', { name: 'Create listing' }).click();
- await expect(page.getByText('Add at least one image before publishing.')).toHaveCount(0);
- await page.getByRole('button', { name: 'Publish' }).last().click();
- await expect(page.getByText('AVAILABLE').last()).toBeVisible();
+ const createdListing = page.locator('section[aria-labelledby="listings-title"] article').filter({ hasText: productName }).last();
+ await expect(createdListing.getByText('Add at least one image before publishing.')).toHaveCount(0);
+ await createdListing.getByRole('button', { name: 'Publish' }).click();
+ await expect(createdListing).toContainText('AVAILABLE');
- await page.getByLabel('Inventory copy', { exact: true }).last().selectOption(createdInventoryId);
+ await page.locator('#sale-inventory').selectOption(createdInventoryId);
await page.getByLabel('Sale date').fill('2026-09-09');
await page.getByLabel('Sold price (cents)').fill('2499');
await page.getByLabel('Cost basis (cents)').fill('1200');