This commit is contained in:
@@ -25,6 +25,16 @@ export function createInventoryItemRepository(db) {
|
||||
.orderBy(asc(inventoryItems.createdAt));
|
||||
},
|
||||
|
||||
async listBySellerId(sellerId) {
|
||||
return db
|
||||
.select({ item: inventoryItems })
|
||||
.from(inventoryItems)
|
||||
.innerJoin(products, eq(products.id, inventoryItems.productId))
|
||||
.where(eq(products.sellerId, sellerId))
|
||||
.orderBy(asc(inventoryItems.createdAt))
|
||||
.then((rows) => rows.map(({ item }) => item));
|
||||
},
|
||||
|
||||
async searchPublic({ page = 1, limit = 20 } = {}) {
|
||||
const conditions = [
|
||||
eq(inventoryItems.status, 'ACTIVE'),
|
||||
|
||||
@@ -54,4 +54,8 @@ export async function registerAdminInventoryRoutes(app, options) {
|
||||
|
||||
return reply.code(201).send({ data: toAdminInventoryItemDto(inventoryItem) });
|
||||
});
|
||||
|
||||
app.get('/inventory', async (request) => ({
|
||||
data: (await inventoryItemService.listBySellerId(request.admin.sellerId)).map(toAdminInventoryItemDto),
|
||||
}));
|
||||
}
|
||||
@@ -7,6 +7,10 @@ export function createInventoryItemService({
|
||||
productRepository,
|
||||
}) {
|
||||
return {
|
||||
async listBySellerId(sellerId) {
|
||||
return inventoryItemRepository.listBySellerId(sellerId);
|
||||
},
|
||||
|
||||
async create(input) {
|
||||
const inventoryItem = createInventoryItem(input);
|
||||
const [product, condition] = await Promise.all([
|
||||
|
||||
@@ -51,6 +51,14 @@ h1 { max-width: 760px; margin: var(--space-3) 0 var(--space-4); font-size: var(-
|
||||
.admin-sidebar__footer { display: grid; gap: var(--space-3); align-self: end; color: var(--color-text-muted); font-size: var(--text-sm); }
|
||||
.admin-shell__content { padding: var(--space-12); }
|
||||
.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 form { display: grid; gap: var(--space-3); }
|
||||
.admin-list { margin-top: var(--space-8); }
|
||||
.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 li { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; border-bottom: var(--border-thin) solid var(--color-border); padding: var(--space-3) 0; }
|
||||
.admin-list li span { color: var(--color-text-muted); font-size: var(--text-sm); }
|
||||
.admin-list a { color: var(--color-interactive-text); font-weight: var(--weight-bold); }
|
||||
.showcase-header { display: flex; align-items: end; justify-content: space-between; gap: var(--space-8); margin-bottom: var(--space-12); }
|
||||
.showcase-header h1 { max-width: 720px; }
|
||||
.showcase-header p { max-width: 600px; color: var(--color-text-muted); }
|
||||
@@ -109,4 +117,5 @@ h1 { max-width: 760px; margin: var(--space-3) 0 var(--space-4); font-size: var(-
|
||||
.admin-shell { grid-template-columns: 1fr; }
|
||||
.admin-sidebar { border-right: 0; border-bottom: var(--border-thin) solid var(--color-border); }
|
||||
.admin-shell__content { padding: var(--space-6); }
|
||||
.admin-workspace-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,91 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
|
||||
import { useAuth } from '@/composables/useAuth.js';
|
||||
import { createAdminInventory, createAdminProduct, listAdminInventory, listAdminProducts } from '@/services/admin-catalog-service.js';
|
||||
|
||||
const { principal } = useAuth();
|
||||
const products = ref([]);
|
||||
const inventory = ref([]);
|
||||
const isLoading = ref(true);
|
||||
const isSaving = ref(false);
|
||||
const error = ref(null);
|
||||
const productName = ref('');
|
||||
const categoryId = ref('');
|
||||
const inventoryProductId = ref('');
|
||||
const conditionId = ref('');
|
||||
|
||||
function csrfToken() {
|
||||
return document.cookie.split('; ').find((value) => value.startsWith('pokemon_inventory_csrf='))?.split('=')[1] ?? '';
|
||||
}
|
||||
|
||||
async function load() {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
[products.value, inventory.value] = await Promise.all([listAdminProducts(), listAdminInventory()]);
|
||||
} catch (requestError) { error.value = requestError; }
|
||||
finally { isLoading.value = false; }
|
||||
}
|
||||
|
||||
async function createProduct() {
|
||||
isSaving.value = true;
|
||||
try {
|
||||
const product = await createAdminProduct({ name: productName.value, categoryId: categoryId.value }, csrfToken());
|
||||
products.value.push(product);
|
||||
productName.value = '';
|
||||
} catch (requestError) { error.value = requestError; }
|
||||
finally { isSaving.value = false; }
|
||||
}
|
||||
|
||||
async function createInventory() {
|
||||
isSaving.value = true;
|
||||
try {
|
||||
const item = await createAdminInventory({ productId: inventoryProductId.value, conditionId: conditionId.value }, csrfToken());
|
||||
inventory.value.push(item);
|
||||
} catch (requestError) { error.value = requestError; }
|
||||
finally { isSaving.value = false; }
|
||||
}
|
||||
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="admin-page"><p class="catalog-kicker">Overview</p><h1>Collector workspace</h1><p class="detail-notes">Select an inventory item to manage its images and listing details.</p></section>
|
||||
<section class="admin-page">
|
||||
<p class="catalog-kicker">Overview</p>
|
||||
<h1>Collector workspace</h1>
|
||||
<p class="detail-notes">{{ principal?.email }} · Build the catalog one physical copy at a time.</p>
|
||||
<p v-if="isLoading" class="state state--loading" role="status">Loading inventory...</p>
|
||||
<p v-if="error" class="state state--error" role="alert">{{ error.message }}</p>
|
||||
|
||||
<div class="admin-workspace-grid">
|
||||
<form class="showcase-panel" @submit.prevent="createProduct">
|
||||
<h2>Add product</h2>
|
||||
<label class="fixture-label" for="product-name">Name</label>
|
||||
<input id="product-name" v-model="productName" class="fixture-input" required />
|
||||
<label class="fixture-label" for="product-category">Category ID</label>
|
||||
<input id="product-category" v-model="categoryId" class="fixture-input" required />
|
||||
<button class="button button--primary" type="submit" :disabled="isSaving">Create product</button>
|
||||
</form>
|
||||
<form class="showcase-panel" @submit.prevent="createInventory">
|
||||
<h2>Add inventory copy</h2>
|
||||
<label class="fixture-label" for="inventory-product">Product ID</label>
|
||||
<input id="inventory-product" v-model="inventoryProductId" class="fixture-input" required />
|
||||
<label class="fixture-label" for="inventory-condition">Condition ID</label>
|
||||
<input id="inventory-condition" v-model="conditionId" class="fixture-input" required />
|
||||
<button class="button button--primary" type="submit" :disabled="isSaving">Create copy</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<section class="admin-list" aria-labelledby="products-title">
|
||||
<h2 id="products-title">Products</h2>
|
||||
<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>
|
||||
</section>
|
||||
<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>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
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: { 'content-type': 'application/json', ...options.headers },
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) throw new Error(payload?.error?.message ?? 'Admin catalog request failed.');
|
||||
return payload?.data;
|
||||
}
|
||||
|
||||
export function listAdminProducts() { return request('/admin/products'); }
|
||||
export function listAdminInventory() { return request('/admin/inventory'); }
|
||||
|
||||
export function createAdminProduct(input, csrfToken) {
|
||||
return request('/admin/products', { method: 'POST', headers: { 'x-csrf-token': csrfToken }, body: JSON.stringify(input) });
|
||||
}
|
||||
|
||||
export function createAdminInventory(input, csrfToken) {
|
||||
return request('/admin/inventory', { method: 'POST', headers: { 'x-csrf-token': csrfToken }, body: JSON.stringify(input) });
|
||||
}
|
||||
Reference in New Issue
Block a user