Files

25 lines
3.5 KiB
Markdown

# 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.