feat: add acquisition admin workflow
Quality / test (push) Canceled after 0s

Add the admin Acquisition form and history, calculated cost-basis display, API client, and deterministic authenticated browser coverage.
This commit is contained in:
2026-09-09 00:52:03 -04:00
parent eee8e1978f
commit 8a64feb181
3 changed files with 109 additions and 12 deletions
+64 -4
View File
@@ -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);
<input id="cost-basis" v-model="costBasis" class="fixture-input" type="number" min="0" required />
<button class="button button--primary" type="submit" :disabled="isSaving">Record sale</button>
</form>
<form class="showcase-panel" @submit.prevent="recordAcquisition">
<h2>Record acquisition</h2>
<label class="fixture-label" for="acquisition-inventory">Inventory copy</label>
<select id="acquisition-inventory" v-model="acquisitionInventoryId" class="fixture-input" required>
<option value="" disabled>Select a copy</option>
<option v-for="item in inventory" :key="item.id" :value="item.id">{{ item.id }}</option>
</select>
<label class="fixture-label" for="acquisition-method">Method</label>
<input id="acquisition-method" v-model="acquisitionMethod" class="fixture-input" required />
<label class="fixture-label" for="acquisition-date">Acquisition date</label>
<input id="acquisition-date" v-model="acquisitionDate" class="fixture-input" type="date" required />
<label class="fixture-label" for="acquisition-cost">Cost (cents)</label>
<input id="acquisition-cost" v-model="acquisitionCost" class="fixture-input" type="number" min="0" required />
<label class="fixture-label" for="acquisition-shipping">Shipping (cents)</label>
<input id="acquisition-shipping" v-model="acquisitionShipping" class="fixture-input" type="number" min="0" />
<label class="fixture-label" for="acquisition-tax">Tax (cents)</label>
<input id="acquisition-tax" v-model="acquisitionTax" class="fixture-input" type="number" min="0" />
<button class="button button--primary" type="submit" :disabled="isSaving">Record acquisition</button>
</form>
</div>
<section class="admin-list" aria-labelledby="products-title">
@@ -188,7 +235,7 @@ onMounted(load);
<section class="admin-list" aria-labelledby="inventory-title">
<h2 id="inventory-title">Physical inventory</h2>
<p v-if="!inventory.length && !isLoading" class="state state--empty">No physical copies yet.</p>
<ul v-else><li v-for="item in inventory" :key="item.id"><span>{{ item.id }}</span> · {{ item.status }} <RouterLink :to="{ name: 'admin-image-manager', params: { id: item.id } }">Manage images</RouterLink></li></ul>
<ul v-else><li v-for="item in inventory" :key="item.id" :data-product-id="item.productId"><span>{{ item.id }}</span> · {{ item.status }} <RouterLink :to="{ name: 'admin-image-manager', params: { id: item.id } }">Manage images</RouterLink></li></ul>
</section>
<section class="admin-list" aria-labelledby="listings-title">
<h2 id="listings-title">Listings</h2>
@@ -213,5 +260,18 @@ onMounted(load);
</li>
</ul>
</section>
<section class="admin-list" aria-labelledby="acquisitions-title">
<h2 id="acquisitions-title">Acquisition history</h2>
<p v-if="!acquisitions.length && !isLoading" class="state state--empty">No acquisitions recorded yet.</p>
<ul v-else>
<li v-for="acquisition in acquisitions" :key="acquisition.id">
<strong>{{ acquisition.inventoryItemId }}</strong>
· {{ acquisition.date ?? 'Undated' }}
· {{ acquisition.method }}
· Cost ${{ (acquisition.costMinor / 100).toFixed(2) }}
· Basis ${{ ((acquisition.costBasisMinor ?? 0) / 100).toFixed(2) }}
</li>
</ul>
</section>
</section>
</template>