Files
vmall/openspec/changes/archive/2026-09-17-frontend-apps/agent-mall.md
T

3.5 KiB

Task: build apps/mall (customer storefront) — READ openspec/changes/frontend-apps/agent-conventions.md FIRST and follow it exactly. Your app = @vmall/mall, port 3000. Allowed roles: customer.

Replace the placeholder pages with the real storefront. app.vue (topnav with locale + currency switchers) already exists — keep it; you may refine but not regress it. Currency selection comes from usePrefs().currency (cookie) and is fed by the switcher in app.vue.

Pages to implement (apps/mall/pages/)

  1. index.vue — catalog home. Category filter (select, from $api.listCategories()), search input (q), paging (prev/next). Product grid (.grid.products): card with first image (or placeholder div), localized name via t(name, locale), price = lowest active-SKU price, displayed in the SELECTED currency. Price display helper (make composables/usePrice.ts): given amount_minor + sku currency, if it equals selected currency use as-is, else $api.convert(...); cache results in a reactive Map keyed amount:from:to to avoid duplicate calls; format with formatMoney(converted, selected, exponentOfSelected, locale). Load currency list once (store it in the composable via useState) for exponents.
  2. products/[id].vue — detail. $api.getProduct(route.params.id): images (main + thumbs), localized name/description, SKU picker (radio list: sku_code + attributes JSON rendered as key: value pairs + stock), qty input (1..stock), converted price, add-to-cart button. Not logged in → redirect /login (preserve intent not required). Success → navigate to /cart.
  3. cart.vue (auth) — $api.getCart(): line items (localized name, sku_code, unit price converted, qty editor with update on change $api.updateCartItem, remove $api.removeCartItem), line totals, subtotal (sum of converted lines), checkout button → /checkout. Empty state.
  4. checkout.vue (auth) — shipping address form (recipient/phone/country/region/city/line1/postal_code, all validated non-empty except region/postal optional-but-shown), order preview (lines + subtotal in selected currency), place order → $api.checkout(address, currency) → success page state: show created order numbers + link to /orders.
  5. orders/index.vue (auth) — $api.listMyOrders(page): table (order_no, created, item count, total formatted in order.currency, status badge), link to detail. Paging.
  6. orders/[id].vue (auth) — $api.getOrder(id): items table, address card, status badge, total. Actions by status: pending_payment → Pay ($api.payOrder) and Cancel ($api.cancelOrder). Shipments section: from $api.listMyShipments() filtered by order_id — carrier/tracking/status, confirm-delivery button when shipped ($api.confirmDelivered). Invoice section: request form (kind select personal/company, title, tax_no shown when company) → $api.requestInvoice; show existing invoice for this order from $api.listMyInvoices().
  7. invoices.vue (auth) — $api.listMyInvoices(): table (invoice_no or —, order_no, title, kind, amount formatted, status badge, issued_at).
  8. login.vue, register.vue — .form-narrow cards. Register: $api.register(email, password, displayName) then setAuth and go home. Link between the two.

Middleware

middleware/auth.ts per conventions; apply to cart, checkout, orders/*, invoices.

Verify

pnpm --filter @vmall/mall build MUST pass. Fix all type errors properly (no any, no @ts-ignore).

Report back

Pages built, any deviations, anything missing from the API client contract (e.g. a pay endpoint) — list precisely.