diff --git a/apps/web/src/pages/AdminHomePage.stories.js b/apps/web/src/pages/AdminHomePage.stories.js index 11b72a1..4a0dff9 100644 --- a/apps/web/src/pages/AdminHomePage.stories.js +++ b/apps/web/src/pages/AdminHomePage.stories.js @@ -2,3 +2,13 @@ import AdminHomePage from './AdminHomePage.vue'; export default { title: 'Pages/AdminHomePage', component: AdminHomePage }; 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.', + }, + }, + }, +}; diff --git a/apps/web/src/pages/AdminHomePage.vue b/apps/web/src/pages/AdminHomePage.vue index 336ab30..9431961 100644 --- a/apps/web/src/pages/AdminHomePage.vue +++ b/apps/web/src/pages/AdminHomePage.vue @@ -10,6 +10,7 @@ import { createAdminSale, listAdminSales, voidAdminSale } from '@/services/admin import { createAdminAcquisition, listAdminAcquisitions } from '@/services/admin-acquisition-service.js'; import { getAdminDashboard } from '@/services/admin-dashboard-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 products = ref([]); @@ -48,6 +49,12 @@ const pricingPrice = ref(''); const overridePrice = ref(''); const overrideReason = 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() { return document.cookie.split('; ').find((value) => value.startsWith('pokemon_inventory_csrf='))?.split('=')[1] ?? ''; @@ -71,6 +78,28 @@ async function createProduct() { 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() { isSaving.value = true; try { @@ -218,6 +247,23 @@ onMounted(load); +
+

Link provider mapping

+ + + + + + + + + + + +

Add inventory copy

@@ -304,6 +350,11 @@ onMounted(load);

No products yet.

+
+

Active provider mappings

+

Select a product to view mappings.

+ +

Physical inventory

No physical copies yet.

diff --git a/apps/web/src/services/admin-provider-mapping-service.js b/apps/web/src/services/admin-provider-mapping-service.js new file mode 100644 index 0000000..0cf4bd6 --- /dev/null +++ b/apps/web/src/services/admin-provider-mapping-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 ?? '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), + }); +} diff --git a/apps/web/tests/e2e/admin-catalog-workflow.spec.js b/apps/web/tests/e2e/admin-catalog-workflow.spec.js index 6519f75..56c3ffa 100644 --- a/apps/web/tests/e2e/admin-catalog-workflow.spec.js +++ b/apps/web/tests/e2e/admin-catalog-workflow.spec.js @@ -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 expect(page.locator('#inventory-product option').nth(1)).toBeAttached(); - const createdProductId = await page.getByLabel('Product', { exact: true }).locator('option', { hasText: productName }).getAttribute('value'); - await page.getByLabel('Product', { exact: true }).selectOption(createdProductId); + const createdProductId = await page.locator('#inventory-product').locator('option', { hasText: productName }).getAttribute('value'); + 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.getByRole('button', { name: 'Create copy' }).click();