feat: paginate customer orders and polish the dashboard hero

Add paged orders API and infinite scroll, short order numbers, and align the guest invite card with the logged-in hero shell.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
s.esmaeili
2026-10-17 13:11:42 +03:30
co-authored by Cursor
parent f550172567
commit afb068c643
89 changed files with 4518 additions and 1325 deletions
@@ -0,0 +1,65 @@
// Server-side catalogue paging (STUB-03 step 4 / docs/CATALOG-PAGINATION-PLAN.md).
// Replaces "download everything, slice in the browser" for list screens.
import { computed, ref } from 'vue'
import { api, type ProductQuery, type ProductSummary } from '../api/client'
export const CATALOG_PAGE_SIZE = 20
export function usePagedProducts() {
const rows = ref<ProductSummary[]>([])
const total = ref(0)
const page = ref(0)
const loading = ref(false)
const loadingMore = ref(false)
const hasMore = computed(() => rows.value.length < total.value)
/**
* @param opts.preserveRows — pull-to-refresh: keep the current list on screen
* until the new page arrives (avoids an empty flash / stuck full-page spinner).
*/
async function resetAndLoad(query: ProductQuery, opts: { preserveRows?: boolean } = {}) {
const showFullSpinner = !opts.preserveRows || rows.value.length === 0
if (showFullSpinner) loading.value = true
page.value = 1
try {
const res = await api.listProductsPaged({
...query,
page: 1,
limit: query.limit ?? CATALOG_PAGE_SIZE,
})
rows.value = res.rows
total.value = res.total
} finally {
loading.value = false
}
}
async function loadMore(query: ProductQuery) {
if (loading.value || loadingMore.value || !hasMore.value) return
loadingMore.value = true
const next = page.value + 1
try {
const res = await api.listProductsPaged({
...query,
page: next,
limit: query.limit ?? CATALOG_PAGE_SIZE,
})
page.value = next
// Append; drop accidental duplicates if the catalogue shifted between pages.
const seen = new Set(rows.value.map((p) => p.id))
rows.value = [...rows.value, ...res.rows.filter((p) => !seen.has(p.id))]
total.value = res.total
} finally {
loadingMore.value = false
}
}
function clear() {
rows.value = []
total.value = 0
page.value = 0
}
return { rows, total, page, loading, loadingMore, hasMore, resetAndLoad, loadMore, clear }
}
+30 -24
View File
@@ -1,13 +1,9 @@
// Adapted from legacy henza_pwa/src/Mixins/getProducts.ts. Legacy paginated
// against POST /get-products (page/limit/count). The new API's GET
// /catalog/products returns the full active catalog in one flat response with
// no pagination — so `isPull` (legacy's infinite-scroll "next page" trigger)
// is a no-op here, and `totalPages` is always 0. A caller that wants new
// products at the end of the list passes `force` to refresh the cached
// catalogue instead of fabricating fake pages.
import { reactive, watch } from 'vue'
// Newest / list screens: true server paging (STUB-03 step 4).
// Legacy paginated against POST /get-products; the early Nest port returned the
// whole catalogue and treated `@fetch` as a no-op. This restores real pages.
import { reactive } from 'vue'
import type { ProductSummary } from '../api/client'
import { useCatalogStore } from '../stores/catalogStore'
import { usePagedProducts, CATALOG_PAGE_SIZE } from './usePagedProducts'
export interface ProductsData {
totalPages: number
@@ -16,28 +12,38 @@ export interface ProductsData {
}
export function useProducts() {
const catalog = useCatalogStore()
const paged = usePagedProducts()
const productsData: ProductsData = reactive({
totalPages: 0,
productList: catalog.products,
productList: [] as ProductSummary[],
flag: false,
})
// A refresh from another screen (pull-to-refresh) lands here too.
watch(
() => catalog.products,
(list) => {
productsData.productList = list
productsData.flag = list.length < 1
},
)
function sync() {
productsData.productList = paged.rows.value
productsData.totalPages = Math.ceil(paged.total.value / CATALOG_PAGE_SIZE) || 0
productsData.flag = paged.total.value < 1 && !paged.loading.value
}
const getProducts = async (loading: { value: boolean }, isPull = false, force = false) => {
if (isPull) return // no further pages exist server-side yet
loading.value = force || !catalog.loaded
void force
if (isPull) {
if (!paged.hasMore.value || paged.loadingMore.value) return
loading.value = true
try {
await paged.loadMore({ sort: 'newest' })
sync()
} catch (e) {
console.log(e)
} finally {
loading.value = false
}
return
}
loading.value = true
try {
productsData.productList = await catalog.load(force)
productsData.flag = productsData.productList.length < 1
await paged.resetAndLoad({ sort: 'newest' })
sync()
} catch (e) {
console.log(e)
} finally {
@@ -45,5 +51,5 @@ export function useProducts() {
}
}
return { productsData, getProducts }
return { productsData, getProducts, hasMore: paged.hasMore }
}