# BP Panel — List sorting (frontend) Guide for **Business Partner (BP)** list screens under `/api/v1`. Use these query parameters to control item order on paginated GET endpoints. This does **not** apply to Kanban board card order (see [board-api.md](./board-api.md) — cards use `order` on move/reorder APIs). --- ## Quick reference | Parameter | Type | Default | Allowed values | |-----------|------|---------|----------------| | `sort_by` | string | `created_at` | MongoDB field name (see per-endpoint tables) | | `sort_order` | string | `desc` | `asc` or `desc` | **Example (newest tenders first — also the default if omitted):** ```http GET /api/v1/tenders?limit=20&sort_by=created_at&sort_order=desc Authorization: Bearer ``` **Example (soonest deadline first):** ```http GET /api/v1/tenders?limit=20&sort_by=tender_deadline&sort_order=asc ``` --- ## Authentication All BP list endpoints below require a **customer** JWT from: ```http POST /api/v1/profile/login ``` ```http Authorization: Bearer ``` Base URL (local): `http://localhost:8081/api/v1` --- ## How sorting works 1. **`sort_by`** must match the **BSON field name** stored in MongoDB (snake_case), not the JSON display name in the UI. 2. **`sort_order`** - `desc` — highest / latest values first (default). - `asc` — lowest / earliest values first. 3. If you omit both parameters, the API uses **`sort_by=created_at`** and **`sort_order=desc`**. 4. Lists use **keyset (cursor) pagination**. When requesting page 2+ with `cursor`, you **must keep the same** `sort_by`, `sort_order`, `limit`, and filters as the request that produced `meta.next_cursor`. Changing sort mid-pagination returns **400** (`cursor does not match requested sort`). 5. Do not send **`cursor` and `offset` together** — the API returns **400**. ### Pagination parameters (used with sorting) | Parameter | Default | Max | Notes | |-----------|---------|-----|--------| | `limit` | `10` | `100` | Page size | | `offset` | `0` | — | First page only; prefer `cursor` for deep pages | | `cursor` | — | — | Copy from `meta.next_cursor` on the previous response | ### Response metadata Sorted list responses include `meta` on the envelope: ```json { "success": true, "message": "Tenders retrieved successfully", "data": { "tenders": [] }, "meta": { "total": 240, "limit": 20, "offset": 0, "page": 1, "pages": 12, "has_more": true, "next_cursor": "eyJ2IjoxLCJzb3J0X2ZpZWxkIjoiY3JlYXRlZF9hdCIs..." } } ``` --- ## Endpoints that support `sort_by` / `sort_order` ### Tenders — main panel list | | | |--|--| | **Method / path** | `GET /api/v1/tenders` | | **Also** | `GET /api/v1/tenders/recommend` (same sorting rules) | | **Default sort** | `created_at` `desc` | **Recommended `sort_by` values** (field exists on tender documents): | `sort_by` | Use case | |-----------|----------| | `created_at` | Newest ingested / default | | `updated_at` | Recently changed tenders | | `publication_date` | Publication date on TED notice | | `tender_deadline` | Submission / tender deadline | | `submission_deadline` | Alternative deadline field when set | | `estimated_value` | Value high → low or low → high | | `title` | Alphabetical (locale-dependent) | | `status` | Group by lifecycle status | | `country_code` | Group by country | > The backend does not whitelist tender `sort_by` values. Use only documented fields; unknown fields may sort incorrectly or behave unpredictably. **Example with filters + sort:** ```http GET /api/v1/tenders?limit=20&country_codes=DE,FR&sort_by=publication_date&sort_order=desc&deadline_from=1717200000 ``` **TypeScript helper:** ```typescript type SortOrder = "asc" | "desc"; interface ListSortParams { sort_by?: string; sort_order?: SortOrder; limit?: number; offset?: number; cursor?: string; } function buildTenderListQuery( filters: Record, sort: ListSortParams = { sort_by: "created_at", sort_order: "desc", limit: 20 } ): string { const params = new URLSearchParams(); for (const [key, value] of Object.entries(filters)) { if (value === undefined) continue; if (Array.isArray(value)) value.forEach((v) => params.append(key, v)); else params.set(key, String(value)); } params.set("sort_by", sort.sort_by ?? "created_at"); params.set("sort_order", sort.sort_order ?? "desc"); if (sort.limit) params.set("limit", String(sort.limit)); if (sort.cursor) params.set("cursor", sort.cursor); else if (sort.offset) params.set("offset", String(sort.offset)); return params.toString(); } ``` --- ### Notifications | | | |--|--| | **Method / path** | `GET /api/v1/notifications` | | **Default sort** | `created_at` `desc` | **Recommended `sort_by` values:** | `sort_by` | Use case | |-----------|----------| | `created_at` | Newest notifications (default) | | `updated_at` | Last updated | | `seen_at` | Seen / unread ordering | | `priority` | Important first (with `sort_order=desc`) | | `schedule_at` | Scheduled notifications | --- ### Feedback (company list) | | | |--|--| | **Method / path** | `GET /api/v1/feedback` | | **Default sort** | `created_at` `desc` | | **Scope** | Company-scoped from the logged-in customer | **Recommended `sort_by` values:** | `sort_by` | Use case | |-----------|----------| | `created_at` | Newest feedback (default) | | `updated_at` | Recently changed | | `feedback_type` | Group likes / dislikes | --- ## Endpoints without `sort_by` / `sort_order` (BP) | Endpoint | Notes | |----------|--------| | `GET /api/v1/tender-approvals` | Uses `limit` / `offset` only; **no** `sort_by` on the public handler today | | `GET /api/v1/companies` | Single “my company” resource, not a sortable list | | Kanban (`/admin/v1/kanban/...`) | Admin-only; card order is `order` + move/reorder APIs | If you need sorted tender approvals in BP, coordinate with backend to add `sort_by` / `sort_order` to `PublicGetTenderApprovals` (admin list already supports them). --- ## UI patterns ### Column header → query params | UI label | `sort_by` | Typical `sort_order` | |----------|-----------|----------------------| | Date added | `created_at` | `desc` | | Deadline | `tender_deadline` | `asc` | | Value | `estimated_value` | `desc` | | Published | `publication_date` | `desc` | Toggle the same column: flip `asc` ↔ `desc`. Changing column: set new `sort_by` and reset to page 1 (`offset=0`, drop `cursor`). ### Infinite scroll / “Load more” 1. First request: `sort_by`, `sort_order`, `limit`, filters — no `cursor`. 2. Next page: repeat the **same** sort and filters; pass `cursor=meta.next_cursor`. 3. Stop when `meta.has_more === false`. --- ## Errors | Situation | HTTP | `error.code` (typical) | |-----------|------|-------------------------| | Invalid `sort_order` (not `asc`/`desc`) | 400 | `BAD_REQUEST` | | `cursor` + `offset` together | 400 | `BAD_REQUEST` | | `cursor` with different `sort_by` / `sort_order` | 400 | `BAD_REQUEST` | | `limit` < 1 or > 100 | 400 | `BAD_REQUEST` | --- ## Admin panel (reference) Admin list routes under `/admin/v1/*` use the same `sort_by` / `sort_order` conventions where `response.NewPagination` is used. Some admin forms restrict values (e.g. tender approvals: `created_at`, `updated_at`, `status`, `submission_mode`). BP tender lists are **less restricted** than admin tender-approval lists. --- **Source:** `pkg/response/pagination.go`, `pkg/mongo/list_pagination.go`, `internal/tender`, `internal/notification`, `internal/feedback` · **Last updated:** 2026-05-31