Home / Help Center / pos-app-garment / garment / Bestseller by Size
🏆 garment

Bestseller by Size

Bestseller by Size groups sales by product and then by size variant, ranking which sizes sell fastest (or slowest) within each style — a quick way to see, for example, that a shirt's Medium and Large sell out while Small and XXL barely move, informing your next reorder.

📍 Menu path: /garment/bestseller-by-size
👤 Who uses it: Owner,Manager

Overview

Sales in the chosen date range are grouped first by product, then by size — size is resolved by matching each sale item's product SKU to a ProductVariant record and reading a "size" (or "Size") key out of that variant's attributes JSON; if no matching variant or size attribute is found, the size shows as "—".

A Bestsellers / Slow Movers toggle re-sorts the flat list of product+size rows — Bestsellers shows highest units-sold first, Slow Movers shows lowest units-sold first — then the sorted rows are grouped back into per-product cards for display.

Each product card lists its sizes as rows with Units Sold, Revenue, and a proportional bar under a "Rank" column.

The default date range is January 1st of the current year through today, and only completed sales (payment status COMPLETED, type SALE) are counted, same as Style Contribution.

Before You Start

Step-by-Step Guide

1 Find your fastest and slowest selling sizes

  1. Set the From/To dates (defaults to Jan 1st this year through today) and click Apply.
  2. Use the Bestsellers / Slow Movers toggle in the top-right of the filter row to switch ranking direction.
  3. Scan each product card — sizes are listed in rank order for that toggle, with a units-sold figure, revenue, and a proportional bar.
💡 Top-3 rows show a medal emoji (🥇🥈🥉). Be aware of a display quirk: only the very first-ranked row is conditioned on being in Bestsellers view for the gold medal — the 2nd and 3rd-ranked rows within each product still show silver/bronze medals even while viewing Slow Movers, which can misleadingly suggest a slow-moving size is a runner-up bestseller.

2 Export the size breakdown

  1. Click Export CSV (disabled until there is at least one row of data) to download Product, Size, Units Sold and Revenue as a CSV, in whichever sort order (Bestsellers/Slow Movers) is currently selected.

Every Field & Button, Explained

Field / ButtonWhat it does
From / To + ApplyDate range for the sales data; defaults to Jan 1st of the current year through today.
Bestsellers / Slow Movers toggleRe-sorts all product+size rows by units sold descending (Bestsellers) or ascending (Slow Movers) before regrouping them by product.
Export CSVDownloads Product, Size, Units Sold, Revenue for every row in the current sort order; disabled until at least one row exists.
Product group headerGroups all size rows for one product under its name.
SizeResolved from the matching ProductVariant's attributes.size/Size; shows "—" if no matching variant or size attribute is found for that sale item's SKU.
Units Sold / RevenueTotal quantity and revenue for that specific product+size combination in the selected period.
Rank (bar)A proportional bar scaled against the current view's reference row — see the known bar-scaling quirk in Troubleshooting for Slow Movers view.
Medal icons🥇/🥈/🥉 shown on the top 3 rows within a product's size list — see the workflow note above for a display quirk affecting the 2nd/3rd medals in Slow Movers view.

Tips & Best Practices

Troubleshooting & FAQ

A size shows as "—" instead of S/M/L/XL.
The size is resolved by matching the sale item's product SKU to a ProductVariant record and reading a size attribute from its attributes JSON — if that SKU has no matching variant, or the variant has no size key set, it falls back to "—". Check the product's variant matrix has size attributes filled in.
In Slow Movers view, the 2nd and 3rd rows under a product show medal emojis even though they aren't the true worst sellers.
This is a known display bug — the medal logic only special-cases the very top row for Bestsellers view, but unconditionally shows silver/bronze medals on the 2nd/3rd-ranked rows in both views. Ignore the medal icons when reading Slow Movers and rely on the Units Sold numbers instead.
In Slow Movers view, most of the progress bars look maxed out or clipped.
This is a known display bug — the bar-width scale is calculated from the first row of whichever sort order is active, which in Slow Movers view is actually the smallest units-sold value (since the list is sorted ascending), not the largest. That makes most other rows compute a bar width well over 100%, which gets visually clipped. The Units Sold figures themselves are still accurate; only the bar length is misleading in this view.
A product is missing from the report entirely.
Only completed sales (payment status COMPLETED, type SALE) within the selected date range are counted — check the sale was not cancelled or refunded, and that it falls inside your From/To dates.

🧑‍💻 Developer Notes

Source component(s):

  • frontend-app/pos-app-garment/src/pages/garment/BestsellerBySizePage.jsx

Backend endpoints used:

  • GET /garment/analytics/bestseller-by-size?from=&to=

Related tables (db-core repositories):

  • Sale
  • SaleItem
  • Product
  • ProductVariant

Redux slices:

  • None — local component state only

Feature flag key: reports

Backed by packages/pos-core/src/garment.js getBestsellerBySize (~line 696): joins Sale/SaleItem/Product for COMPLETED SALE-type transactions in range, then separately loads ProductVariant rows (filtered by tenantId when available) and builds a sku -> size lookup by JSON-parsing each variant's attributes column for an "size"/"Size" key. Rows are keyed by `${productId}_${size}` so the same product with an unresolved size ("—") is aggregated into one bucket. Two client-side bugs found in BestsellerBySizePage.jsx worth fixing: (1) the medal ternary `idx === 0 && view === 'bestseller' ? '🥇' : idx === 1 ? '🥈' : idx === 2 ? '🥉' : null` only gates the gold medal on view, so silver/bronze still render in Slow Movers mode; (2) `maxQty = sorted.length > 0 ? sorted[0].qtySold : 1` is taken from the head of the already mode-sorted array, which is the minimum (not the maximum) qtySold when view === 'slowmover', so bar-width percentages exceed 100% and clip visually for most rows in that view.