This commit is contained in:
@@ -53,6 +53,8 @@ h1 { max-width: 760px; margin: var(--space-3) 0 var(--space-4); font-size: var(-
|
|||||||
.admin-page { max-width: 880px; }
|
.admin-page { max-width: 880px; }
|
||||||
.admin-workspace-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-4); margin: var(--space-8) 0; }
|
.admin-workspace-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-4); margin: var(--space-8) 0; }
|
||||||
.admin-workspace-grid form { display: grid; gap: var(--space-3); }
|
.admin-workspace-grid form { display: grid; gap: var(--space-3); }
|
||||||
|
.admin-wizard { display: grid; gap: var(--space-4); margin: var(--space-8) 0; }
|
||||||
|
.admin-wizard__nav { display: flex; justify-content: space-between; gap: var(--space-3); }
|
||||||
.admin-list { margin-top: var(--space-8); }
|
.admin-list { margin-top: var(--space-8); }
|
||||||
.admin-list h2 { margin-bottom: var(--space-3); font-size: var(--text-xl); }
|
.admin-list h2 { margin-bottom: var(--space-3); font-size: var(--text-xl); }
|
||||||
.admin-list ul { display: grid; gap: var(--space-2); margin: 0; padding: 0; list-style: none; }
|
.admin-list ul { display: grid; gap: var(--space-2); margin: 0; padding: 0; list-style: none; }
|
||||||
|
|||||||
@@ -24,19 +24,28 @@
|
|||||||
border-right: 0;
|
border-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inventory-workflow__step a {
|
.inventory-workflow__link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
min-height: 76px;
|
min-height: 76px;
|
||||||
|
width: 100%;
|
||||||
padding: var(--space-3);
|
padding: var(--space-3);
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
color: var(--color-text-subtle);
|
color: var(--color-text-subtle);
|
||||||
text-decoration: none;
|
text-align: left;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inventory-workflow__step a:hover,
|
.inventory-workflow__link:hover,
|
||||||
.inventory-workflow__step a:focus-visible {
|
.inventory-workflow__link:focus-visible {
|
||||||
background: var(--color-surface-muted);
|
box-shadow: inset 3px 0 0 var(--color-interactive);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-workflow__step--active .inventory-workflow__link {
|
||||||
|
box-shadow: inset 3px 0 0 var(--color-interactive);
|
||||||
}
|
}
|
||||||
|
|
||||||
.inventory-workflow__index {
|
.inventory-workflow__index {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||||
|
|
||||||
import InventoryWorkflowStepper from './InventoryWorkflowStepper.vue';
|
import InventoryWorkflowStepper from './InventoryWorkflowStepper.vue';
|
||||||
|
|
||||||
export default { title: 'Molecules/InventoryWorkflowStepper', component: InventoryWorkflowStepper };
|
export default { title: 'Molecules/InventoryWorkflowStepper', component: InventoryWorkflowStepper };
|
||||||
|
|
||||||
export const InProgress = {
|
export const InProgress = {
|
||||||
args: {
|
args: {
|
||||||
|
activeStepId: 'workflow-mapping',
|
||||||
steps: [
|
steps: [
|
||||||
{ id: 'workflow-product', label: 'Product', description: 'Identify item', status: 'complete' },
|
{ id: 'workflow-product', label: 'Product', description: 'Identify item', status: 'complete' },
|
||||||
{ id: 'workflow-mapping', label: 'Sources', description: 'Link markets', status: 'current' },
|
{ id: 'workflow-mapping', label: 'Sources', description: 'Link markets', status: 'current' },
|
||||||
@@ -17,6 +20,7 @@ export const InProgress = {
|
|||||||
|
|
||||||
export const Complete = {
|
export const Complete = {
|
||||||
args: {
|
args: {
|
||||||
|
activeStepId: 'workflow-listing',
|
||||||
steps: [
|
steps: [
|
||||||
{ id: 'workflow-product', label: 'Product', description: 'Identify item', status: 'complete' },
|
{ id: 'workflow-product', label: 'Product', description: 'Identify item', status: 'complete' },
|
||||||
{ id: 'workflow-mapping', label: 'Sources', description: 'Link markets', status: 'complete' },
|
{ id: 'workflow-mapping', label: 'Sources', description: 'Link markets', status: 'complete' },
|
||||||
@@ -27,3 +31,12 @@ export const Complete = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SelectStepWithKeyboard = {
|
||||||
|
args: { ...InProgress.args, onSelect: fn() },
|
||||||
|
play: async ({ args, canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
await userEvent.click(canvas.getByRole('button', { name: /Copy/ }));
|
||||||
|
await expect(args.onSelect).toHaveBeenCalledWith('workflow-inventory');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,20 +4,25 @@ defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
activeStepId: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
defineEmits(['select']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="inventory-workflow" aria-label="Inventory creation workflow">
|
<nav class="inventory-workflow" aria-label="Inventory creation workflow">
|
||||||
<ol class="inventory-workflow__steps">
|
<ol class="inventory-workflow__steps">
|
||||||
<li v-for="(step, index) in steps" :key="step.id" :class="['inventory-workflow__step', `inventory-workflow__step--${step.status}`]">
|
<li v-for="(step, index) in steps" :key="step.id" :class="['inventory-workflow__step', `inventory-workflow__step--${step.status}`, { 'inventory-workflow__step--active': activeStepId === step.id }]">
|
||||||
<a :href="`#${step.id}`">
|
<button type="button" class="inventory-workflow__link" :aria-current="activeStepId === step.id ? 'step' : undefined" @click="$emit('select', step.id)">
|
||||||
<span class="inventory-workflow__index">{{ index + 1 }}</span>
|
<span class="inventory-workflow__index">{{ index + 1 }}</span>
|
||||||
<span>
|
<span>
|
||||||
<strong>{{ step.label }}</strong>
|
<strong>{{ step.label }}</strong>
|
||||||
<small>{{ step.description }}</small>
|
<small>{{ step.description }}</small>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -56,6 +56,17 @@ import {
|
|||||||
} from "@/services/admin-provider-mapping-service.js";
|
} from "@/services/admin-provider-mapping-service.js";
|
||||||
|
|
||||||
const { principal } = useAuth();
|
const { principal } = useAuth();
|
||||||
|
const WIZARD_STEP_IDS = [
|
||||||
|
"workflow-product",
|
||||||
|
"workflow-mapping",
|
||||||
|
"workflow-inventory",
|
||||||
|
"workflow-images",
|
||||||
|
"workflow-pricing",
|
||||||
|
"workflow-listing",
|
||||||
|
];
|
||||||
|
const WIZARD_STORAGE_KEY = "pokemon-inventory:admin-wizard-step";
|
||||||
|
const activeStepId = ref(WIZARD_STEP_IDS[0]);
|
||||||
|
const activeStepIndex = computed(() => WIZARD_STEP_IDS.indexOf(activeStepId.value));
|
||||||
const products = ref([]);
|
const products = ref([]);
|
||||||
const inventory = ref([]);
|
const inventory = ref([]);
|
||||||
const selectedInventoryIds = ref([]);
|
const selectedInventoryIds = ref([]);
|
||||||
@@ -538,7 +549,25 @@ async function voidSale(saleId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goToStep(stepId) {
|
||||||
|
if (!WIZARD_STEP_IDS.includes(stepId)) return;
|
||||||
|
activeStepId.value = stepId;
|
||||||
|
localStorage.setItem(WIZARD_STORAGE_KEY, stepId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToNextStep() {
|
||||||
|
goToStep(WIZARD_STEP_IDS[Math.min(activeStepIndex.value + 1, WIZARD_STEP_IDS.length - 1)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToPreviousStep() {
|
||||||
|
goToStep(WIZARD_STEP_IDS[Math.max(activeStepIndex.value - 1, 0)]);
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(load);
|
onMounted(load);
|
||||||
|
onMounted(() => {
|
||||||
|
const storedStepId = localStorage.getItem(WIZARD_STORAGE_KEY);
|
||||||
|
if (storedStepId && WIZARD_STEP_IDS.includes(storedStepId)) activeStepId.value = storedStepId;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -677,10 +706,11 @@ onMounted(load);
|
|||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<InventoryWorkflowStepper :steps="workflowSteps" />
|
<InventoryWorkflowStepper :steps="workflowSteps" :active-step-id="activeStepId" @select="goToStep" />
|
||||||
|
|
||||||
<div class="admin-workspace-grid">
|
<div class="admin-wizard">
|
||||||
<ProductIdentityStep
|
<ProductIdentityStep
|
||||||
|
v-show="activeStepId === 'workflow-product'"
|
||||||
:product-name="productName"
|
:product-name="productName"
|
||||||
:category-id="categoryId"
|
:category-id="categoryId"
|
||||||
:categories="categories"
|
:categories="categories"
|
||||||
@@ -690,6 +720,7 @@ onMounted(load);
|
|||||||
@submit="createProduct"
|
@submit="createProduct"
|
||||||
/>
|
/>
|
||||||
<ProviderMappingStep
|
<ProviderMappingStep
|
||||||
|
v-show="activeStepId === 'workflow-mapping'"
|
||||||
:product-id="mappingProductId"
|
:product-id="mappingProductId"
|
||||||
:provider-id="mappingProviderId"
|
:provider-id="mappingProviderId"
|
||||||
:mapping-type="mappingType"
|
:mapping-type="mappingType"
|
||||||
@@ -706,6 +737,7 @@ onMounted(load);
|
|||||||
@submit="createProviderMapping"
|
@submit="createProviderMapping"
|
||||||
/>
|
/>
|
||||||
<InventoryCopyStep
|
<InventoryCopyStep
|
||||||
|
v-show="activeStepId === 'workflow-inventory'"
|
||||||
:product-id="inventoryProductId"
|
:product-id="inventoryProductId"
|
||||||
:condition-id="conditionId"
|
:condition-id="conditionId"
|
||||||
:products="products"
|
:products="products"
|
||||||
@@ -715,7 +747,34 @@ onMounted(load);
|
|||||||
@update:condition-id="conditionId = $event"
|
@update:condition-id="conditionId = $event"
|
||||||
@submit="createInventory"
|
@submit="createInventory"
|
||||||
/>
|
/>
|
||||||
|
<InventoryImagesStep
|
||||||
|
v-show="activeStepId === 'workflow-images'"
|
||||||
|
:inventory="inventory"
|
||||||
|
:missing-images="dashboard?.counts.missingImages ?? 0"
|
||||||
|
/>
|
||||||
|
<PricingReviewStep
|
||||||
|
v-show="activeStepId === 'workflow-pricing'"
|
||||||
|
:inventory-item-id="pricingInventoryId"
|
||||||
|
:provider="pricingProvider"
|
||||||
|
:source-identifier="pricingSource"
|
||||||
|
:observed-price="pricingPrice"
|
||||||
|
:override-price="overridePrice"
|
||||||
|
:override-reason="overrideReason"
|
||||||
|
:inventory="inventory"
|
||||||
|
:refresh-status="refreshStatus"
|
||||||
|
:is-saving="isSaving"
|
||||||
|
@update:inventory-item-id="pricingInventoryId = $event"
|
||||||
|
@update:provider="pricingProvider = $event"
|
||||||
|
@update:source-identifier="pricingSource = $event"
|
||||||
|
@update:observed-price="pricingPrice = $event"
|
||||||
|
@update:override-price="overridePrice = $event"
|
||||||
|
@update:override-reason="overrideReason = $event"
|
||||||
|
@calculate="calculatePricing"
|
||||||
|
@refresh="refreshPricing"
|
||||||
|
@override="overridePricing"
|
||||||
|
/>
|
||||||
<ReviewListStep
|
<ReviewListStep
|
||||||
|
v-show="activeStepId === 'workflow-listing'"
|
||||||
:inventory-item-id="listingInventoryId"
|
:inventory-item-id="listingInventoryId"
|
||||||
:asking-price="askingPrice"
|
:asking-price="askingPrice"
|
||||||
:inventory="inventory"
|
:inventory="inventory"
|
||||||
@@ -724,6 +783,13 @@ onMounted(load);
|
|||||||
@update:asking-price="askingPrice = $event"
|
@update:asking-price="askingPrice = $event"
|
||||||
@submit="createListing"
|
@submit="createListing"
|
||||||
/>
|
/>
|
||||||
|
<div class="admin-wizard__nav">
|
||||||
|
<button class="button button--secondary" type="button" :disabled="activeStepIndex === 0" @click="goToPreviousStep">Back</button>
|
||||||
|
<button class="button button--secondary" type="button" :disabled="activeStepIndex === WIZARD_STEP_IDS.length - 1" @click="goToNextStep">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-workspace-grid">
|
||||||
<form class="showcase-panel" @submit.prevent="recordSale">
|
<form class="showcase-panel" @submit.prevent="recordSale">
|
||||||
<h2>Record sale</h2>
|
<h2>Record sale</h2>
|
||||||
<label class="fixture-label" for="sale-inventory">Inventory copy</label>
|
<label class="fixture-label" for="sale-inventory">Inventory copy</label>
|
||||||
@@ -846,26 +912,6 @@ onMounted(load);
|
|||||||
Record acquisition
|
Record acquisition
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<PricingReviewStep
|
|
||||||
:inventory-item-id="pricingInventoryId"
|
|
||||||
:provider="pricingProvider"
|
|
||||||
:source-identifier="pricingSource"
|
|
||||||
:observed-price="pricingPrice"
|
|
||||||
:override-price="overridePrice"
|
|
||||||
:override-reason="overrideReason"
|
|
||||||
:inventory="inventory"
|
|
||||||
:refresh-status="refreshStatus"
|
|
||||||
:is-saving="isSaving"
|
|
||||||
@update:inventory-item-id="pricingInventoryId = $event"
|
|
||||||
@update:provider="pricingProvider = $event"
|
|
||||||
@update:source-identifier="pricingSource = $event"
|
|
||||||
@update:observed-price="pricingPrice = $event"
|
|
||||||
@update:override-price="overridePrice = $event"
|
|
||||||
@update:override-reason="overrideReason = $event"
|
|
||||||
@calculate="calculatePricing"
|
|
||||||
@refresh="refreshPricing"
|
|
||||||
@override="overridePricing"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="admin-list" aria-labelledby="products-title">
|
<section class="admin-list" aria-labelledby="products-title">
|
||||||
@@ -960,10 +1006,6 @@ onMounted(load);
|
|||||||
· {{ item.status }}
|
· {{ item.status }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<InventoryImagesStep
|
|
||||||
:inventory="inventory"
|
|
||||||
:missing-images="dashboard?.counts.missingImages ?? 0"
|
|
||||||
/>
|
|
||||||
<ul v-if="hasValuations" aria-label="Market valuations">
|
<ul v-if="hasValuations" aria-label="Market valuations">
|
||||||
<li
|
<li
|
||||||
v-for="(valuation, inventoryItemId) in valuations"
|
v-for="(valuation, inventoryItemId) in valuations"
|
||||||
|
|||||||
@@ -178,6 +178,7 @@ Remaining roadmap areas are approved external provider credentials/adapters, bro
|
|||||||
- Public listing detail now shows market-value freshness and suppresses asking price for sold listings, with Storybook coverage for sold and valued states.
|
- Public listing detail now shows market-value freshness and suppresses asking price for sold listings, with Storybook coverage for sold and valued states.
|
||||||
- Public catalog filters now collapse behind an accessible mobile disclosure, show an active-filter count, and provide a reset action while preserving URL synchronization.
|
- Public catalog filters now collapse behind an accessible mobile disclosure, show an active-filter count, and provide a reset action while preserving URL synchronization.
|
||||||
- The admin image manager now supports multi-file upload with a drag-and-drop dropzone, per-image alt-text editing, primary selection, up/down reordering, and per-image retry/delete actions targeted at the correct image instead of the first uploaded image.
|
- The admin image manager now supports multi-file upload with a drag-and-drop dropzone, per-image alt-text editing, primary selection, up/down reordering, and per-image retry/delete actions targeted at the correct image instead of the first uploaded image.
|
||||||
|
- The guided inventory workflow is now a true single-step-at-a-time wizard: the stepper is clickable, only the active stage's panel is shown, Back/Next controls advance between stages, and the active stage persists across reloads through local storage.
|
||||||
|
|
||||||
## Local Commands
|
## Local Commands
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user