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

This commit is contained in:
2026-09-09 13:28:15 -04:00
parent 4c041e069d
commit 0ffccf6617
4 changed files with 92 additions and 2 deletions
@@ -2,3 +2,13 @@ import AdminHomePage from './AdminHomePage.vue';
export default { title: 'Pages/AdminHomePage', component: AdminHomePage }; export default { title: 'Pages/AdminHomePage', component: AdminHomePage };
export const Overview = {}; export const Overview = {};
export const ProviderMappingWorkflow = {
parameters: {
docs: {
description: {
story: 'The admin workspace includes Product-level provider mapping controls alongside inventory, acquisition, pricing, listing, and sales workflows.',
},
},
},
};
+51
View File
@@ -10,6 +10,7 @@ import { createAdminSale, listAdminSales, voidAdminSale } from '@/services/admin
import { createAdminAcquisition, listAdminAcquisitions } from '@/services/admin-acquisition-service.js'; import { createAdminAcquisition, listAdminAcquisitions } from '@/services/admin-acquisition-service.js';
import { getAdminDashboard } from '@/services/admin-dashboard-service.js'; import { getAdminDashboard } from '@/services/admin-dashboard-service.js';
import { addAdminPricingObservation, calculateAdminValuation, overrideAdminValuation } from '@/services/admin-pricing-service.js'; import { addAdminPricingObservation, calculateAdminValuation, overrideAdminValuation } from '@/services/admin-pricing-service.js';
import { createAdminProviderMapping, listAdminProviderMappings } from '@/services/admin-provider-mapping-service.js';
const { principal } = useAuth(); const { principal } = useAuth();
const products = ref([]); const products = ref([]);
@@ -48,6 +49,12 @@ const pricingPrice = ref('');
const overridePrice = ref(''); const overridePrice = ref('');
const overrideReason = ref(''); const overrideReason = ref('');
const voidReasons = ref({}); const voidReasons = ref({});
const mappingProductId = ref('');
const mappingProviderId = ref('');
const mappingType = ref('PRODUCT');
const mappingExternalIdentifier = ref('');
const mappingSearchQuery = ref('');
const providerMappings = ref([]);
function csrfToken() { function csrfToken() {
return document.cookie.split('; ').find((value) => value.startsWith('pokemon_inventory_csrf='))?.split('=')[1] ?? ''; return document.cookie.split('; ').find((value) => value.startsWith('pokemon_inventory_csrf='))?.split('=')[1] ?? '';
@@ -71,6 +78,28 @@ async function createProduct() {
finally { isSaving.value = false; } finally { isSaving.value = false; }
} }
async function loadProviderMappings(productId = mappingProductId.value) {
if (!productId) { providerMappings.value = []; return; }
providerMappings.value = await listAdminProviderMappings(productId);
}
async function createProviderMapping() {
isSaving.value = true;
try {
const mapping = await createAdminProviderMapping(mappingProductId.value, {
providerId: mappingProviderId.value,
mappingType: mappingType.value,
externalIdentifier: mappingExternalIdentifier.value || undefined,
searchQuery: mappingSearchQuery.value || undefined,
}, csrfToken());
providerMappings.value.push(mapping);
mappingProviderId.value = '';
mappingExternalIdentifier.value = '';
mappingSearchQuery.value = '';
} catch (requestError) { error.value = requestError; }
finally { isSaving.value = false; }
}
async function createInventory() { async function createInventory() {
isSaving.value = true; isSaving.value = true;
try { try {
@@ -218,6 +247,23 @@ onMounted(load);
</select> </select>
<button class="button button--primary" type="submit" :disabled="isSaving">Create product</button> <button class="button button--primary" type="submit" :disabled="isSaving">Create product</button>
</form> </form>
<form class="showcase-panel" @submit.prevent="createProviderMapping">
<h2>Link provider mapping</h2>
<label class="fixture-label" for="mapping-product">Product</label>
<select id="mapping-product" v-model="mappingProductId" class="fixture-input" required @change="loadProviderMappings()">
<option value="" disabled>Select a product</option>
<option v-for="product in products" :key="product.id" :value="product.id">{{ product.name }}</option>
</select>
<label class="fixture-label" for="mapping-provider">Provider ID</label>
<input id="mapping-provider" v-model="mappingProviderId" class="fixture-input" required placeholder="ebay" />
<label class="fixture-label" for="mapping-type">Mapping type</label>
<select id="mapping-type" v-model="mappingType" class="fixture-input"><option value="PRODUCT">Product</option><option value="SEARCH">Search</option></select>
<label class="fixture-label" for="mapping-external">External identifier</label>
<input id="mapping-external" v-model="mappingExternalIdentifier" class="fixture-input" />
<label class="fixture-label" for="mapping-query">Search query</label>
<input id="mapping-query" v-model="mappingSearchQuery" class="fixture-input" />
<button class="button button--primary" type="submit" :disabled="isSaving">Save provider mapping</button>
</form>
<form class="showcase-panel" @submit.prevent="createInventory"> <form class="showcase-panel" @submit.prevent="createInventory">
<h2>Add inventory copy</h2> <h2>Add inventory copy</h2>
<label class="fixture-label" for="inventory-product">Product</label> <label class="fixture-label" for="inventory-product">Product</label>
@@ -304,6 +350,11 @@ onMounted(load);
<p v-if="!products.length && !isLoading" class="state state--empty">No products yet.</p> <p v-if="!products.length && !isLoading" class="state state--empty">No products yet.</p>
<ul v-else><li v-for="product in products" :key="product.id">{{ product.name }} <span>{{ product.id }}</span></li></ul> <ul v-else><li v-for="product in products" :key="product.id">{{ product.name }} <span>{{ product.id }}</span></li></ul>
</section> </section>
<section class="admin-list" aria-labelledby="provider-mappings-title">
<h2 id="provider-mappings-title">Active provider mappings</h2>
<p v-if="!providerMappings.length" class="state state--empty">Select a product to view mappings.</p>
<ul v-else><li v-for="mapping in providerMappings" :key="mapping.id">{{ mapping.providerId }} · {{ mapping.mappingType }} · {{ mapping.externalIdentifier ?? mapping.searchQuery }}</li></ul>
</section>
<section class="admin-list" aria-labelledby="inventory-title"> <section class="admin-list" aria-labelledby="inventory-title">
<h2 id="inventory-title">Physical inventory</h2> <h2 id="inventory-title">Physical inventory</h2>
<p v-if="!inventory.length && !isLoading" class="state state--empty">No physical copies yet.</p> <p v-if="!inventory.length && !isLoading" class="state state--empty">No physical copies yet.</p>
@@ -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 ?? 'Provider mapping request failed.');
return payload?.data;
}
export function listAdminProviderMappings(productId) {
return request(`/admin/products/${encodeURIComponent(productId)}/provider-mappings`);
}
export function createAdminProviderMapping(productId, input, csrfToken) {
return request(`/admin/products/${encodeURIComponent(productId)}/provider-mappings`, {
method: 'POST',
headers: { 'x-csrf-token': csrfToken },
body: JSON.stringify(input),
});
}
@@ -26,8 +26,13 @@ test('signs in and publishes a listing after adding an inventory image', async (
await page.getByRole('button', { name: 'Create product' }).click(); await page.getByRole('button', { name: 'Create product' }).click();
await expect(page.locator('#inventory-product option').nth(1)).toBeAttached(); await expect(page.locator('#inventory-product option').nth(1)).toBeAttached();
const createdProductId = await page.getByLabel('Product', { exact: true }).locator('option', { hasText: productName }).getAttribute('value'); const createdProductId = await page.locator('#inventory-product').locator('option', { hasText: productName }).getAttribute('value');
await page.getByLabel('Product', { exact: true }).selectOption(createdProductId); await page.locator('#mapping-product').selectOption(createdProductId);
await page.getByLabel('Provider ID').fill('fixture-provider');
await page.getByLabel('External identifier').fill('fixture-product-1');
await page.getByRole('button', { name: 'Save provider mapping' }).click();
await expect(page.getByRole('region', { name: 'Active provider mappings' })).toContainText('fixture-provider');
await page.locator('#inventory-product').selectOption(createdProductId);
await page.getByLabel('Condition', { exact: true }).selectOption({ index: 1 }); await page.getByLabel('Condition', { exact: true }).selectOption({ index: 1 });
await page.getByRole('button', { name: 'Create copy' }).click(); await page.getByRole('button', { name: 'Create copy' }).click();