Implement tender workflows and dashboard integration
continuous-integration/drone/push Build encountered an error

This commit is contained in:
AmirReza Jamali
2026-07-14 11:12:09 +03:30
parent ce170f9bc8
commit 72881df210
29 changed files with 1820 additions and 159 deletions
+165
View File
@@ -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.
+280
View File
@@ -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 13 + 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 13, 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 35 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.