feat(docs): migrate and reorganize frontend guides for BP panel and file uploads
- Deleted outdated documentation files for BP panel list sorting and Filestore frontend guide. - Created new documentation files for BP panel list sorting and Filestore frontend guide in the client directory. - Updated the structure and content to enhance clarity and accessibility for frontend integration with file upload and profile image APIs. - Ensured consistency in documentation style and improved examples for better developer experience.
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
# Frontend Guide: File Upload + Profile Image APIs
|
||||
|
||||
This is a simple reference for frontend integration with file upload, file retrieval, and setting profile image.
|
||||
|
||||
## Base URLs
|
||||
|
||||
- Admin APIs: `/admin/v1`
|
||||
- Customer/Public APIs: `/api/v1`
|
||||
|
||||
All protected endpoints require:
|
||||
|
||||
- `Authorization: Bearer <access_token>`
|
||||
|
||||
---
|
||||
|
||||
## 1) Login (Admin)
|
||||
|
||||
Use this when you want to update admin user profile image.
|
||||
|
||||
### Request
|
||||
|
||||
- `POST /admin/v1/profile/login`
|
||||
- `Content-Type: application/json`
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "Admin1234"
|
||||
}
|
||||
```
|
||||
|
||||
### Response (success)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Login successful",
|
||||
"data": {
|
||||
"user": {
|
||||
"id": "65bc1234567890abcdef1234",
|
||||
"profile_image": null
|
||||
},
|
||||
"access_token": "<jwt>",
|
||||
"refresh_token": "<jwt>",
|
||||
"expires_at": 1776783486
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2) Upload File (GridFS)
|
||||
|
||||
Use multipart form upload.
|
||||
|
||||
### Admin route
|
||||
|
||||
- `POST /admin/v1/files/upload`
|
||||
|
||||
### Customer route
|
||||
|
||||
- `POST /api/v1/files/upload`
|
||||
|
||||
### Form fields
|
||||
|
||||
- `file` (required, File)
|
||||
- `category` (optional, string), example: `profile_images`
|
||||
- `tags` (optional, repeatable), example: `user`, `profile`
|
||||
- `description` (optional, string)
|
||||
|
||||
### Response (success)
|
||||
|
||||
```json
|
||||
{
|
||||
"file_id": "69e7899c2053abd3fa5433e0",
|
||||
"filename": "avatar.png",
|
||||
"content_type": "image/png",
|
||||
"size": 123456,
|
||||
"message": "File uploaded successfully"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3) Get File Info
|
||||
|
||||
- `GET /admin/v1/files/{file_id}/info`
|
||||
- `GET /api/v1/files/{file_id}/info`
|
||||
|
||||
### Response (success)
|
||||
|
||||
```json
|
||||
{
|
||||
"file_id": "69e7899c2053abd3fa5433e0",
|
||||
"filename": "avatar.png",
|
||||
"content_type": "image/png",
|
||||
"size": 123456,
|
||||
"uploaded_by": "65bc1234567890abcdef1234",
|
||||
"uploaded_at": "2026-04-21T14:28:44.787Z",
|
||||
"category": "profile_images",
|
||||
"tags": ["user", "profile"],
|
||||
"description": "sample profile photo"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4) Download File
|
||||
|
||||
- `GET /admin/v1/files/{file_id}/download`
|
||||
- `GET /api/v1/files/{file_id}/download`
|
||||
|
||||
Returns file binary stream.
|
||||
|
||||
Useful for image URL:
|
||||
|
||||
- `http://localhost:8080/admin/v1/files/<file_id>/download`
|
||||
- or `http://localhost:8080/api/v1/files/<file_id>/download`
|
||||
|
||||
---
|
||||
|
||||
## 5) Set Admin User Profile Image
|
||||
|
||||
Upload does not update user profile automatically.
|
||||
After upload, call profile update endpoint with the download URL.
|
||||
|
||||
### Request
|
||||
|
||||
- `PUT /admin/v1/profile`
|
||||
- `Content-Type: application/json`
|
||||
|
||||
```json
|
||||
{
|
||||
"profile_image": "http://localhost:8080/admin/v1/files/69e7899c2053abd3fa5433e0/download"
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
- Success: standard success envelope with updated user data
|
||||
- Failure example:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Bad Request",
|
||||
"error": {
|
||||
"code": "BAD_REQUEST",
|
||||
"message": "failed to update user"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6) Verify Profile
|
||||
|
||||
- `GET /admin/v1/profile`
|
||||
|
||||
Check:
|
||||
|
||||
- `data.profile_image` is now populated with file URL.
|
||||
|
||||
---
|
||||
|
||||
## Common Errors
|
||||
|
||||
- `401 Unauthorized`: missing/expired token
|
||||
- `413 Request Entity Too Large`: file too big
|
||||
- `400 Unsupported file type` or validation error
|
||||
- `404 File not found`: wrong `file_id`
|
||||
|
||||
---
|
||||
|
||||
## Suggested Frontend Flow (Admin)
|
||||
|
||||
1. Login -> save `access_token`
|
||||
2. Upload image -> get `file_id`
|
||||
3. Build URL `.../admin/v1/files/{file_id}/download`
|
||||
4. Update profile with `profile_image` URL
|
||||
5. Refresh profile screen via `GET /admin/v1/profile`
|
||||
|
||||
---
|
||||
|
||||
## Customer: Company Documents Upload
|
||||
|
||||
Customers can upload company documents using customer auth token.
|
||||
|
||||
### Upload endpoint
|
||||
|
||||
- `POST /api/v1/files/upload`
|
||||
|
||||
### Recommended form-data for company docs
|
||||
|
||||
- `file`: actual document file
|
||||
- `category`: `company_documents`
|
||||
- `tags`: repeatable. Suggested:
|
||||
- `company`
|
||||
- `document`
|
||||
- `<doc_type>` (example: `tax_certificate`, `registration`, `license`)
|
||||
- `description`: human-readable note, example: `Company tax certificate 2026`
|
||||
|
||||
### Example curl
|
||||
|
||||
```bash
|
||||
CUSTOMER_TOKEN="<customer_access_token>"
|
||||
|
||||
curl -X POST "http://localhost:8080/api/v1/files/upload" \
|
||||
-H "Authorization: Bearer ${CUSTOMER_TOKEN}" \
|
||||
-F "file=@/absolute/path/to/company-doc.pdf" \
|
||||
-F "category=company_documents" \
|
||||
-F "tags=company" \
|
||||
-F "tags=document" \
|
||||
-F "tags=tax_certificate" \
|
||||
-F "description=Company tax certificate 2026"
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"file_id": "69e8abcd2053abd3fa5433e1",
|
||||
"filename": "company-doc.pdf",
|
||||
"content_type": "application/pdf",
|
||||
"size": 245632,
|
||||
"message": "File uploaded successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Fetch company docs list
|
||||
|
||||
Use list with category filter:
|
||||
|
||||
- `GET /api/v1/files?category=company_documents&limit=50&skip=0`
|
||||
|
||||
You can also filter by tags:
|
||||
|
||||
- `GET /api/v1/files?category=company_documents&tags=tax_certificate`
|
||||
|
||||
### Download company doc
|
||||
|
||||
- `GET /api/v1/files/{file_id}/download`
|
||||
|
||||
### Important current behavior
|
||||
|
||||
- Upload works for customers and stores `uploaded_by` as `customer_id`.
|
||||
- Company now supports `document_file_ids` for mapping GridFS files.
|
||||
|
||||
### Attach uploaded documents to company
|
||||
|
||||
Use admin company update endpoint after collecting uploaded `file_id` values:
|
||||
|
||||
- `PUT /admin/v1/companies/{company_id}`
|
||||
|
||||
Request body includes regular company fields plus `document_file_ids`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Opplens",
|
||||
"type": "private",
|
||||
"registration_number": "1234567890",
|
||||
"tax_id": "1234567890",
|
||||
"industry": "Technology",
|
||||
"document_file_ids": [
|
||||
"69e8abcd2053abd3fa5433e1",
|
||||
"69e8abcd2053abd3fa5433e2"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`document_file_ids` is also returned in:
|
||||
|
||||
- `GET /admin/v1/companies/{id}`
|
||||
- `GET /api/v1/companies` (customer "my company" profile)
|
||||
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
# 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 <customer_access_token>
|
||||
```
|
||||
|
||||
**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 <access_token>
|
||||
```
|
||||
|
||||
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<string, string | number | string[] | undefined>,
|
||||
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
|
||||
Reference in New Issue
Block a user