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

This commit is contained in:
2026-09-08 23:15:50 -04:00
parent 3c8a1d1154
commit 4e0e4ebd48
2 changed files with 55 additions and 1 deletions
+42 -1
View File
@@ -4,10 +4,12 @@ import { RouterLink } from 'vue-router';
import { useAuth } from '@/composables/useAuth.js';
import { createAdminInventory, createAdminProduct, listAdminCategories, listAdminConditions, listAdminInventory, listAdminProducts } from '@/services/admin-catalog-service.js';
import { createAdminListing, listAdminListings, markListingPending, publishAdminListing } from '@/services/admin-listing-service.js';
const { principal } = useAuth();
const products = ref([]);
const inventory = ref([]);
const listings = ref([]);
const categories = ref([]);
const conditions = ref([]);
const isLoading = ref(true);
@@ -17,6 +19,8 @@ const productName = ref('');
const categoryId = ref('');
const inventoryProductId = ref('');
const conditionId = ref('');
const listingInventoryId = ref('');
const askingPrice = ref('');
function csrfToken() {
return document.cookie.split('; ').find((value) => value.startsWith('pokemon_inventory_csrf='))?.split('=')[1] ?? '';
@@ -25,7 +29,7 @@ function csrfToken() {
async function load() {
isLoading.value = true;
try {
[products.value, inventory.value, categories.value, conditions.value] = await Promise.all([listAdminProducts(), listAdminInventory(), listAdminCategories(), listAdminConditions()]);
[products.value, inventory.value, categories.value, conditions.value, listings.value] = await Promise.all([listAdminProducts(), listAdminInventory(), listAdminCategories(), listAdminConditions(), listAdminListings()]);
} catch (requestError) { error.value = requestError; }
finally { isLoading.value = false; }
}
@@ -49,6 +53,27 @@ async function createInventory() {
finally { isSaving.value = false; }
}
async function createListing() {
const item = inventory.value.find((candidate) => candidate.id === listingInventoryId.value);
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);
askingPrice.value = '';
} catch (requestError) { error.value = requestError; }
finally { isSaving.value = false; }
}
async function transitionListing(action, id) {
isSaving.value = true;
try {
const listing = await action(id, csrfToken());
listings.value = listings.value.map((candidate) => candidate.id === listing.id ? listing : candidate);
} catch (requestError) { error.value = requestError; }
finally { isSaving.value = false; }
}
onMounted(load);
</script>
@@ -86,6 +111,17 @@ onMounted(load);
</select>
<button class="button button--primary" type="submit" :disabled="isSaving">Create copy</button>
</form>
<form class="showcase-panel" @submit.prevent="createListing">
<h2>Create listing draft</h2>
<label class="fixture-label" for="listing-inventory">Inventory copy</label>
<select id="listing-inventory" v-model="listingInventoryId" class="fixture-input" required>
<option value="" disabled>Select a physical copy</option>
<option v-for="item in inventory" :key="item.id" :value="item.id">{{ item.id }} · {{ item.status }}</option>
</select>
<label class="fixture-label" for="listing-price">Asking price (cents)</label>
<input id="listing-price" v-model="askingPrice" class="fixture-input" type="number" min="1" required />
<button class="button button--primary" type="submit" :disabled="isSaving">Create listing</button>
</form>
</div>
<section class="admin-list" aria-labelledby="products-title">
@@ -98,5 +134,10 @@ onMounted(load);
<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>
</section>
<section class="admin-list" aria-labelledby="listings-title">
<h2 id="listings-title">Listings</h2>
<p v-if="!listings.length && !isLoading" class="state state--empty">No listings yet.</p>
<ul v-else><li v-for="listing in listings" :key="listing.id"><span>{{ listing.productName }}</span> · {{ listing.status }} · ${{ (listing.askingPriceMinor / 100).toFixed(2) }} <button v-if="listing.status === 'DRAFT'" class="button button--secondary" type="button" @click="transitionListing(publishAdminListing, listing.id)">Publish</button><button v-if="listing.status === 'DRAFT'" class="button button--secondary" type="button" @click="transitionListing(markListingPending, listing.id)">Mark pending</button></li></ul>
</section>
</section>
</template>