Implement tender workflows and dashboard integration
continuous-integration/drone/push Build encountered an error
continuous-integration/drone/push Build encountered an error
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
# Home Page API — Response Time Benchmarks
|
||||
|
||||
Measured from the client network (curl) against production.
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Date** | 2026-07-07 |
|
||||
| **Base URL** | `https://app.opplenz.com` |
|
||||
| **Runs per endpoint** | 10 |
|
||||
| **Account** | `test@email.com` |
|
||||
| **Company ID** | `69dbef4e0917c63d28787737` |
|
||||
|
||||
All requests returned **HTTP 200** on every run.
|
||||
|
||||
---
|
||||
|
||||
## Average response time
|
||||
|
||||
| # | Endpoint | Method | Avg (ms) | Min (ms) | Max (ms) | Blocks UI? | % of blocking load |
|
||||
|---|----------|--------|----------|----------|----------|------------|-------------------|
|
||||
| 1 | `/api/v1/tender-approvals/stats` | GET | **614** | 520 | 793 | Yes | 3.4% |
|
||||
| 2 | `/api/v1/tender-approvals?limit=10&offset=0` | GET | **16,688** | 16,503 | 17,061 | Yes | **93.6%** |
|
||||
| 3 | `/api/v1/feedback?feedback_type=like&limit=1&offset=0` | GET | **554** | 506 | 668 | Yes | 3.1% |
|
||||
| 4 | `/api/v1/notifications?seen=false&limit=1&event_type=PUSH` | GET | **548** | 483 | 642 | No | — |
|
||||
| 5 | `/api/v1/tenders/recommend?limit=10&offset=0` | GET | **5,716** | 4,010 | 8,361 | No* | — |
|
||||
|
||||
\* Call #5 only fires when call #2 returns an empty tender list. It does not block the initial loading spinner, but still adds server load and may update the list shortly after render.
|
||||
|
||||
---
|
||||
|
||||
## Totals
|
||||
|
||||
| Metric | Time |
|
||||
|--------|------|
|
||||
| **Sequential blocking total** (calls 1 + 2 + 3) | **~17.9 s** |
|
||||
| **Slowest single endpoint** | `GET /api/v1/tender-approvals` — **16.7 s avg** |
|
||||
| **Fastest endpoint** | `GET /api/v1/notifications` — **0.5 s avg** |
|
||||
|
||||
The home page waits on calls 1 → 2 → 3 in series before rendering. **`/api/v1/tender-approvals` accounts for ~94% of that wait.**
|
||||
|
||||
---
|
||||
|
||||
## Raw timings (seconds)
|
||||
|
||||
### 1. Tender approval stats
|
||||
`GET /api/v1/tender-approvals/stats`
|
||||
|
||||
| Run | Time (s) |
|
||||
|-----|----------|
|
||||
| 1 | 0.779 |
|
||||
| 2 | 0.662 |
|
||||
| 3 | 0.644 |
|
||||
| 4 | 0.793 |
|
||||
| 5 | 0.597 |
|
||||
| 6 | 0.536 |
|
||||
| 7 | 0.562 |
|
||||
| 8 | 0.533 |
|
||||
| 9 | 0.520 |
|
||||
| 10 | 0.520 |
|
||||
| **Avg** | **0.614** |
|
||||
|
||||
### 2. Your tenders
|
||||
`GET /api/v1/tender-approvals?limit=10&offset=0`
|
||||
|
||||
| Run | Time (s) |
|
||||
|-----|----------|
|
||||
| 1 | 16.503 |
|
||||
| 2 | 16.573 |
|
||||
| 3 | 16.873 |
|
||||
| 4 | 16.607 |
|
||||
| 5 | 17.061 |
|
||||
| 6 | 16.600 |
|
||||
| 7 | 16.628 |
|
||||
| 8 | 16.707 |
|
||||
| 9 | 16.727 |
|
||||
| 10 | 16.606 |
|
||||
| **Avg** | **16.688** |
|
||||
|
||||
### 3. Liked tenders count
|
||||
`GET /api/v1/feedback?feedback_type=like&limit=1&offset=0`
|
||||
|
||||
| Run | Time (s) |
|
||||
|-----|----------|
|
||||
| 1 | 0.629 |
|
||||
| 2 | 0.506 |
|
||||
| 3 | 0.515 |
|
||||
| 4 | 0.594 |
|
||||
| 5 | 0.521 |
|
||||
| 6 | 0.509 |
|
||||
| 7 | 0.529 |
|
||||
| 8 | 0.529 |
|
||||
| 9 | 0.542 |
|
||||
| 10 | 0.668 |
|
||||
| **Avg** | **0.554** |
|
||||
|
||||
### 4. Unread notifications (background)
|
||||
`GET /api/v1/notifications?seen=false&limit=1&event_type=PUSH`
|
||||
|
||||
| Run | Time (s) |
|
||||
|-----|----------|
|
||||
| 1 | 0.566 |
|
||||
| 2 | 0.540 |
|
||||
| 3 | 0.486 |
|
||||
| 4 | 0.607 |
|
||||
| 5 | 0.541 |
|
||||
| 6 | 0.498 |
|
||||
| 7 | 0.614 |
|
||||
| 8 | 0.642 |
|
||||
| 9 | 0.506 |
|
||||
| 10 | 0.483 |
|
||||
| **Avg** | **0.548** |
|
||||
|
||||
### 5. Recommended tenders (conditional)
|
||||
`GET /api/v1/tenders/recommend?limit=10&offset=0`
|
||||
|
||||
| Run | Time (s) |
|
||||
|-----|----------|
|
||||
| 1 | 4.364 |
|
||||
| 2 | 5.880 |
|
||||
| 3 | 5.402 |
|
||||
| 4 | 4.010 |
|
||||
| 5 | 6.512 |
|
||||
| 6 | 5.827 |
|
||||
| 7 | 5.584 |
|
||||
| 8 | 5.653 |
|
||||
| 9 | 8.361 |
|
||||
| 10 | 5.575 |
|
||||
| **Avg** | **5.716** |
|
||||
|
||||
---
|
||||
|
||||
## Key finding
|
||||
|
||||
| Priority | Endpoint | Issue |
|
||||
|----------|----------|-------|
|
||||
| **P0** | `GET /api/v1/tender-approvals?limit=10&offset=0` | ~17 s average — dominates home page load |
|
||||
| P1 | `GET /api/v1/tenders/recommend?limit=10&offset=0` | ~5.7 s average — slow when used as fallback |
|
||||
| P2 | `GET /api/v1/tender-approvals/stats` | ~0.6 s — acceptable but called on every home visit |
|
||||
| — | Feedback + notifications | ~0.55 s each — not a bottleneck |
|
||||
|
||||
---
|
||||
|
||||
## How to reproduce
|
||||
|
||||
```bash
|
||||
TOKEN='<access_token>'
|
||||
BASE='https://app.opplenz.com'
|
||||
|
||||
for url in \
|
||||
"$BASE/api/v1/tender-approvals/stats" \
|
||||
"$BASE/api/v1/tender-approvals?limit=10&offset=0" \
|
||||
"$BASE/api/v1/feedback?feedback_type=like&limit=1&offset=0" \
|
||||
"$BASE/api/v1/notifications?seen=false&limit=1&event_type=PUSH" \
|
||||
"$BASE/api/v1/tenders/recommend?limit=10&offset=0"
|
||||
do
|
||||
curl -s -o /dev/null -w "%{time_total}s %{http_code} $url\n" \
|
||||
-H "Authorization: Bearer $TOKEN" "$url"
|
||||
done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
See [home-page-api-calls.md](./home-page-api-calls.md) for full call order, parameters, and optimization suggestions.
|
||||
@@ -0,0 +1,280 @@
|
||||
# Home Page API Calls
|
||||
|
||||
Document for the backend team. Describes every HTTP request the mobile/web client fires when the user opens `/home`, including call order, parameters, and what blocks the UI from rendering.
|
||||
|
||||
**Context:** Home page load feels slow. The client currently waits on **3 sequential API calls** before showing content. A 4th call runs in the background; a 5th may fire when the user has no "your tenders".
|
||||
|
||||
**Base URL:** `https://app.opplenz.com`
|
||||
**Auth:** All requests include `Authorization: Bearer <access_token>`.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| # | Endpoint | Method | Blocking UI? | When |
|
||||
|---|----------|--------|--------------|------|
|
||||
| 1 | `/api/v1/tender-approvals/stats` | GET | **Yes** | Always on home load |
|
||||
| 2 | `/api/v1/tender-approvals?limit=10&offset=0` | GET | **Yes** | Always on home load |
|
||||
| 3 | `/api/v1/feedback?feedback_type=like&limit=1&offset=0` | GET | **Yes** | Always on home load |
|
||||
| 4 | `/api/v1/notifications?seen=false&limit=1&event_type=PUSH` | GET | No (background) | App shell mount (parallel) |
|
||||
| 5 | `/api/v1/tenders/recommend?limit=10&offset=0` | GET | No | Only when call #2 returns an empty tender list |
|
||||
|
||||
**Minimum calls on first load:** 4 (calls 1–3 + 4)
|
||||
**Maximum calls on first load:** 5 (adds call 5 when user has no your-tenders)
|
||||
|
||||
**Perceived load time (client-side):** Sum of response times for calls 1 → 2 → 3 (sequential `await` chain).
|
||||
|
||||
---
|
||||
|
||||
## Call sequence
|
||||
|
||||
```
|
||||
User opens /home
|
||||
│
|
||||
├─ [parallel, background] App shell
|
||||
│ └─ GET /api/v1/notifications?seen=false&limit=1&event_type=PUSH
|
||||
│
|
||||
└─ [blocking] HomeViewModel.init()
|
||||
│
|
||||
├─ 1. GET /api/v1/tender-approvals/stats ← await
|
||||
├─ 2. GET /api/v1/tender-approvals?limit=10&offset=0 ← await
|
||||
│ └─ if tenders[] is empty (fire-and-forget, not awaited):
|
||||
│ GET /api/v1/tenders/recommend?limit=10&offset=0
|
||||
└─ 3. GET /api/v1/feedback?feedback_type=like&limit=1&offset=0 ← await
|
||||
└─ UI renders (_isLoading = false)
|
||||
```
|
||||
|
||||
Source: `lib/view_models/home_view_model.dart` → `init()`
|
||||
|
||||
---
|
||||
|
||||
## API details
|
||||
|
||||
### 1. Tender approval stats
|
||||
|
||||
Partnership / self-apply progress rings on the home dashboard.
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Method** | `GET` |
|
||||
| **Path** | `/api/v1/tender-approvals/stats` |
|
||||
| **Query params** | None |
|
||||
| **Blocking** | Yes — first call in `init()` |
|
||||
| **Response fields used** | `data.partnership_count`, `data.self_apply_count` |
|
||||
|
||||
**Example:**
|
||||
```http
|
||||
GET /api/v1/tender-approvals/stats
|
||||
Authorization: Bearer <token>
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Your tenders (horizontal list)
|
||||
|
||||
Main tender carousel and "Your Tenders" section.
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Method** | `GET` |
|
||||
| **Path** | `/api/v1/tender-approvals` |
|
||||
| **Query params** | `limit=10`, `offset=0` |
|
||||
| **Optional filters** | Not sent from home (`status`, `created_from`, `created_to` are empty) |
|
||||
| **Blocking** | Yes — second call in `init()` |
|
||||
| **Response fields used** | `data.tenders[].tender` (full tender objects), `data.metadata.pages` (pagination) |
|
||||
|
||||
**Example:**
|
||||
```http
|
||||
GET /api/v1/tender-approvals?limit=10&offset=0
|
||||
Authorization: Bearer <token>
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**Note:** This is likely the heaviest call — it returns up to 10 full tender objects with nested data.
|
||||
|
||||
---
|
||||
|
||||
### 3. Liked tenders count
|
||||
|
||||
"Liked Tenders" stat card count.
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Method** | `GET` |
|
||||
| **Path** | `/api/v1/feedback` |
|
||||
| **Query params** | `feedback_type=like`, `limit=1`, `offset=0` |
|
||||
| **Blocking** | Yes — third call in `init()` |
|
||||
| **Response fields used** | `data.meta.total` only (client ignores the single returned item) |
|
||||
|
||||
**Example:**
|
||||
```http
|
||||
GET /api/v1/feedback?feedback_type=like&limit=1&offset=0
|
||||
Authorization: Bearer <token>
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
**Optimization opportunity:** Client only needs a count. A dedicated count endpoint (or `meta.total` without loading feedback records) would be faster than a full feedback query.
|
||||
|
||||
---
|
||||
|
||||
### 4. Unread notification check (background)
|
||||
|
||||
Red dot on the notification tab icon.
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Method** | `GET` |
|
||||
| **Path** | `/api/v1/notifications` |
|
||||
| **Query params** | `seen=false`, `limit=1`, `event_type=PUSH` |
|
||||
| **Blocking** | No — fired from app shell `postFrameCallback` |
|
||||
| **Response fields used** | Presence of any result (updates `NotificationStateService`) |
|
||||
|
||||
**Example:**
|
||||
```http
|
||||
GET /api/v1/notifications?seen=false&limit=1&event_type=PUSH
|
||||
Authorization: Bearer <token>
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
Source: `lib/core/routes/app_shell/app_shell_screen.dart`
|
||||
|
||||
---
|
||||
|
||||
### 5. Recommended tenders (conditional fallback)
|
||||
|
||||
Shown when the user has no "your tenders". Replaces the horizontal list label with "Recommended Tenders".
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Method** | `GET` |
|
||||
| **Path** | `/api/v1/tenders/recommend` |
|
||||
| **Query params** | `limit=10`, `offset=0` |
|
||||
| **Blocking** | No — triggered inside `getYourTenders()` without `await` |
|
||||
| **Condition** | Fires only when call #2 returns `data.tenders` empty |
|
||||
| **Response fields used** | `data.tenders[]` (full tender objects) |
|
||||
|
||||
**Example:**
|
||||
```http
|
||||
GET /api/v1/tenders/recommend?limit=10&offset=0
|
||||
Authorization: Bearer <token>
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pagination (scroll, not initial load)
|
||||
|
||||
After the home page renders, scrolling the horizontal tender list near the end triggers additional calls:
|
||||
|
||||
| Mode | Endpoint | Params |
|
||||
|------|----------|--------|
|
||||
| Your tenders | `GET /api/v1/tender-approvals` | `limit=10`, `offset=<page * 10>` |
|
||||
| Recommended (fallback mode) | `GET /api/v1/tenders/recommend` | `limit=10`, `offset=<page * 10>` |
|
||||
|
||||
Page size is always **10**.
|
||||
|
||||
---
|
||||
|
||||
## Re-fetch triggers
|
||||
|
||||
The full `init()` sequence (calls 1–3, and possibly 5) runs again when:
|
||||
|
||||
- User switches away from Home tab and returns to it
|
||||
- User navigates to Liked Tenders and comes back (`homeViewModel.init()` on return)
|
||||
|
||||
This means the same 3–5 API calls can repeat on every tab switch back to Home.
|
||||
|
||||
---
|
||||
|
||||
## APIs defined but NOT called on home load
|
||||
|
||||
| Endpoint | Notes |
|
||||
|----------|-------|
|
||||
| `GET /api/v1/feedback/stats/company` | Defined in client (`HomeApi.statsCompany`) but never called from home |
|
||||
|
||||
Some home stat cards show hardcoded `0` values (approved tenders count, tender value) — no API backing today.
|
||||
|
||||
---
|
||||
|
||||
## Performance notes for backend
|
||||
|
||||
### Client-side bottleneck
|
||||
|
||||
The client **does not parallelize** calls 1, 2, and 3. Total time-to-interactive ≈ `T(stats) + T(your-tenders) + T(liked-count)`.
|
||||
|
||||
### Suggested backend investigations
|
||||
|
||||
1. **`GET /api/v1/tender-approvals?limit=10&offset=0`** — Likely the slowest call. Check query plans, N+1 joins, and payload size (10 full tender objects).
|
||||
2. **`GET /api/v1/tender-approvals/stats`** — Runs before every home load; consider caching per company/user.
|
||||
3. **`GET /api/v1/feedback?feedback_type=like&limit=1&offset=0`** — Client only reads `meta.total`. A lightweight `GET /api/v1/feedback/count?feedback_type=like` would avoid loading feedback rows.
|
||||
4. **`GET /api/v1/tenders/recommend`** — May involve recommendation/AI logic; profile separately from tender-approvals.
|
||||
|
||||
### Suggested backend optimizations
|
||||
|
||||
| Idea | Benefit |
|
||||
|------|---------|
|
||||
| Single aggregated `GET /api/v1/home` endpoint | One round-trip instead of 3 sequential |
|
||||
| Return stats + liked count alongside tender-approvals list | Eliminate calls 1 and 3 |
|
||||
| Slim tender DTO for list views (id, title, deadline, status only) | Smaller payloads on call #2 and #5 |
|
||||
| Cache stats and liked count with short TTL | Faster repeat visits / tab switches |
|
||||
|
||||
### Suggested client optimizations (for reference)
|
||||
|
||||
These are frontend changes the mobile team could make independently:
|
||||
|
||||
- Run calls 1, 2, and 3 in parallel (`Future.wait`)
|
||||
- Do not re-run full `init()` on every tab switch — refresh only stale data
|
||||
- Await the recommend fallback before clearing loading state (avoids UI flicker)
|
||||
|
||||
---
|
||||
|
||||
## Source files (client)
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `lib/view_models/home_view_model.dart` | Orchestrates all home API calls |
|
||||
| `lib/data/repositories/home_repository.dart` | Repository layer |
|
||||
| `lib/data/services/home_service.dart` | Stats, recommend, notifications |
|
||||
| `lib/data/services/your_tenders_service.dart` | Your tenders list |
|
||||
| `lib/data/services/liked_tenders_service.dart` | Liked count |
|
||||
| `lib/data/services/api/home_api.dart` | Endpoint path constants |
|
||||
| `lib/data/services/api/your_tenders_api.dart` | Tender-approvals URL builder |
|
||||
| `lib/data/services/api/liked_tenders_api.dart` | Feedback URL builder |
|
||||
| `lib/core/routes/app_shell/app_shell_screen.dart` | Background notification check |
|
||||
| `lib/views/home/pages/home_screen.dart` | Re-init on tab return |
|
||||
|
||||
---
|
||||
|
||||
## Quick test commands
|
||||
|
||||
Replace `$TOKEN` with a valid access token.
|
||||
|
||||
```bash
|
||||
# 1. Stats
|
||||
curl -s -w "\n%{http_code} %{time_total}s\n" \
|
||||
'https://app.opplenz.com/api/v1/tender-approvals/stats' \
|
||||
-H "Authorization: Bearer $TOKEN" | tail -1
|
||||
|
||||
# 2. Your tenders
|
||||
curl -s -w "\n%{http_code} %{time_total}s\n" \
|
||||
'https://app.opplenz.com/api/v1/tender-approvals?limit=10&offset=0' \
|
||||
-H "Authorization: Bearer $TOKEN" | tail -1
|
||||
|
||||
# 3. Liked count
|
||||
curl -s -w "\n%{http_code} %{time_total}s\n" \
|
||||
'https://app.opplenz.com/api/v1/feedback?feedback_type=like&limit=1&offset=0' \
|
||||
-H "Authorization: Bearer $TOKEN" | tail -1
|
||||
|
||||
# 4. Notifications (background)
|
||||
curl -s -w "\n%{http_code} %{time_total}s\n" \
|
||||
'https://app.opplenz.com/api/v1/notifications?seen=false&limit=1&event_type=PUSH' \
|
||||
-H "Authorization: Bearer $TOKEN" | tail -1
|
||||
|
||||
# 5. Recommended (only when your tenders empty)
|
||||
curl -s -w "\n%{http_code} %{time_total}s\n" \
|
||||
'https://app.opplenz.com/api/v1/tenders/recommend?limit=10&offset=0' \
|
||||
-H "Authorization: Bearer $TOKEN" | tail -1
|
||||
```
|
||||
|
||||
Run all five and compare `time_total` to identify the slowest endpoint.
|
||||
Reference in New Issue
Block a user