Compare commits

...

26 Commits

Author SHA1 Message Date
AmirReza Jamali 72881df210 Implement tender workflows and dashboard integration
continuous-integration/drone/push Build encountered an error
2026-07-14 11:12:09 +03:30
AmirReza Jamali ce170f9bc8 Wrap long toast messages instead of truncating
continuous-integration/drone/push Build is passing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:36:40 +03:30
AmirReza Jamali f43ca4913c Show notification link in detail dialog
continuous-integration/drone/push Build is passing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:32:51 +03:30
AmirReza Jamali 727dadecf8 Add tender card shimmer loading
continuous-integration/drone/push Build is passing
2026-06-29 16:12:18 +03:30
AmirReza Jamali 7b5b6b85f8 Remove tender value chevron
continuous-integration/drone/push Build is passing
2026-06-29 15:08:40 +03:30
AmirReza Jamali 6cd8d7cc7f Update tenders recommendations view
continuous-integration/drone/push Build is passing
2026-06-29 13:27:21 +03:30
AmirReza Jamali 17ccd9d464 Navigate recommended tenders label to tab
continuous-integration/drone/push Build is passing
2026-06-28 11:42:45 +03:30
AmirReza Jamali 984f2e5139 refactor: use GET tenders/recommend and improve tender title UX
continuous-integration/drone/push Build is passing
Load the Recommended tab from GET /api/v1/tenders/recommend instead of the
AI POST flow with 503 fallback. Fix notification pagination offset, make
tender titles selectable across list and detail views, and ignore .cursor.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-27 12:23:53 +03:30
amirreza.jamali 7c26288c01 Merge pull request 'feat: add AI recommendations flow for tenders' (#1) from ai-recommendations into dev
continuous-integration/drone/push Build is passing
Reviewed-on: #1
2026-06-17 10:03:56 +00:00
AmirReza Jamali d4a07fd774 test: add AI recommendations fallback tests
Cover repository and view model fallback behavior and fix load state handling for 503 fallback responses.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 13:24:28 +03:30
AmirReza Jamali 50e4f43738 feat: add AI recommendations flow for tenders
Implement company AI onboarding/recommendation models, services, repository, and tender UI integration with supporting tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 11:47:52 +03:30
mazyar b73ec98fef Update Dockerfile
continuous-integration/drone/push Build is passing
2026-06-15 13:55:48 +00:00
mazyar d30051cae1 Update .drone.yml
continuous-integration/drone/push Build was killed
2026-06-15 13:55:32 +00:00
mazyar e3e6545743 Update .drone.yml
continuous-integration/drone/push Build is failing
2026-06-15 13:49:32 +00:00
mazyar 2ad85e361b Update Dockerfile
continuous-integration/drone/push Build was killed
2026-06-15 13:49:19 +00:00
mazyar bd160748cd Update .drone.yml
continuous-integration/drone/push Build is failing
2026-06-15 13:29:20 +00:00
a.jamali 83b32549ce Merge pull request 'Refactor network request handling by introducing AuthInterceptor for improved authentication management. Removed the previous request interceptor logic to streamline the code and enhance maintainability.' (#227) from auth-interceptor into develop
Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/227
Reviewed-by: Hadi Barzegar <barzagarhadi@gmail.com>
2026-06-13 16:27:49 +03:30
AmirReza Jamali 231d800b47 Fix AuthInterceptor replay logout-on-transient-error + add tests
Address PR re-review findings:
- Only a 401 on the replay falls through to logout; other DioExceptions
  (timeout, connection drop, 5xx) now propagate so a flaky network can't
  wipe a valid session and callers see the real error.
- Skip the refresh when the failed request's bearer no longer matches the
  stored one (a concurrent refresh already rotated it) and replay directly.
- Guard _logout() with a _loggingOut flag so the auth-failed callback fires
  once per burst rather than once per concurrent caller.

Add unit tests (fake Dio adapter + mocked prefs) covering the retry guard,
single-flight refresh, and non-401 replay propagation paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 16:20:23 +03:30
AmirReza Jamali deda384aaa Harden AuthInterceptor: retry guard, single-flight refresh, shared pref keys
Addresses PR review on the auth interceptor:

- Guard against unbounded retry recursion: tag the replayed request with
  extra['auth_retried'] and skip the refresh/replay path on a second 401 so
  it can't recurse refresh -> replay indefinitely. Drop the redundant manual
  Authorization header on the replay (onRequest re-injects it).
- Fix the concurrent-refresh race: coalesce concurrent 401s onto a single
  in-flight refresh future so the rotating refresh token is only consumed
  once, instead of later refreshes posting an already-consumed token and
  spuriously logging out. (Chose the shared-future approach over
  QueuedInterceptor, which deadlocks with the replay-via-same-Dio pattern.)
- Centralise the 'bearer' / 'refresh_token' / 'customer_data' pref keys in
  a PrefKeys constants class used by the interceptor, network manager, auth
  service and router so they can't silently diverge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 12:21:03 +03:30
AmirReza Jamali 633f0ba881 Refactor network request handling by introducing AuthInterceptor for improved authentication management. Removed the previous request interceptor logic to streamline the code and enhance maintainability. 2026-06-10 11:34:45 +03:30
a.jamali 7c123905b3 Merge pull request 'Add Docker support and enhance data models for tender details' (#226) from milliseconds-timestamps into develop
Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/226
2026-06-06 14:37:42 +03:30
AmirReza Jamali aa9c9444fa Remove unused DetailDropDown and TenderDetailCard components to streamline codebase and improve maintainability. These components were previously disabled and retained for potential future use. 2026-06-06 12:25:49 +03:30
AmirReza Jamali 86ed7bc665 Refactor tender detail models and UI components for improved clarity and functionality
- Updated the `Lot`, `Organization`, and `OrganizationAddress` models to enhance data mapping for tender details.
- Revised the `TenderData` model to include additional fields reflecting the API response structure.
- Improved UI components across desktop, mobile, and tablet views by commenting out unused sections for future use.
- Added new string constants for lot details in the tender information section.
- Enhanced unit tests to cover new model fields and ensure resilience to partial API payloads.
2026-06-05 19:42:09 +03:30
AmirReza Jamali dde66521f6 Add Docker support and enhance data models for tender details
- Introduced a .dockerignore file to optimize Docker builds by excluding unnecessary files.
- Updated the Dockerfile to use a Flutter base image, enabling web builds for the application.
- Added new data models for `Lot` and `OrganizationAddress` to represent tender details more accurately.
- Enhanced the `Organization` model to include `companyId` and a nested `address` field.
- Updated the `TenderData` model to include new fields such as `noticeTypeCode`, `formType`, and `lots`, reflecting the API response structure.
- Regenerated necessary code for data models to ensure compatibility with the updated structures.
2026-06-03 13:32:42 +03:30
a.jamali 8762ea7d62 Merge pull request 'Refactor date handling in tender data model and UI components. Added unixTimestampFromJson for parsing timestamps and updated timeConvertor to handle null values. Updated relevant model fields and UI to use the new parsing logic.' (#225) from milliseconds-timestamps into develop
Reviewed-on: https://repo.ravanertebat.com/TM/tm_app/pulls/225
Reviewed-by: Hadi Barzegar <barzagarhadi@gmail.com>
2026-05-30 18:47:21 +03:30
AmirReza Jamali 83b77c05ef Refactor card movement logic in BoardViewModel to improve error handling and success validation
- Updated the handling of card movement results to differentiate between successful and unsuccessful API responses.
- Implemented rollback of board state and error messaging for failed moves, ensuring a more robust user experience.
- Enhanced the notification system to surface relevant error messages when card movements fail.
2026-05-30 18:46:13 +03:30
113 changed files with 6483 additions and 1559 deletions
+16
View File
@@ -0,0 +1,16 @@
build
.dart_tool
.git
.gitignore
.drone.yml
.vscode
.idea
.flutter-plugins-dependencies
README.md
*.md
Dockerfile
Dockerfile-old
.dockerignore
artifacts
ios
android
+41 -28
View File
@@ -1,43 +1,56 @@
########### MOODIO TEST ###########
###################################
kind: pipeline kind: pipeline
type: docker type: docker
name: (pwa-test) build pwa and scp to test server name: build-and-push-pwa
steps: steps:
- name: build flutter - name: build flutter
image: docker-mirror.ravanertebat.ir/ghcr/cirruslabs/flutter:3.38.10 image: ghcr.io/cirruslabs/flutter:3.38.10
commands: commands:
- flutter --version - flutter --version
- | - |
set -e set -e
case "$DRONE_BRANCH" in SHORT_SHA=$(echo "$DRONE_COMMIT_SHA" | cut -c1-8)
master) echo prod;; IMG_TAG="${DRONE_BUILD_NUMBER}-${DRONE_BRANCH}"
uat) echo uat;; echo -n "$IMG_TAG,$SHORT_SHA" > .tags
dev) echo test;; echo "Image tags -> $IMG_TAG , $SHORT_SHA"
*) echo dev;; - rm -rf pwa_dist
esac > .environment
- echo "Branch ${DRONE_BRANCH}, Environment $(cat .environment)"
- APP_VERSION=$(cat .environment)-${DRONE_BUILD_NUMBER}
- echo "APP Version tags $APP_VERSION"
- echo -n "$APP_VERSION,latest" >> .tags
- mkdir -p ./artifacts/flutter
- rm -rf build
- flutter pub get - flutter pub get
- flutter build web --release --output=build/tm_pwa - flutter build web --release --output=pwa_dist
- ls -ltrh build/tm_pwa - ls -ltrh pwa_dist
- name: build admin and push to artifactory - name: push pwa
image: plugins/docker image: plugins/docker
settings: settings:
# dockerfile: Dockerfile-admin registry: git.opplens.se
username: cicd repo: git.opplens.se/mazyar/pwa
username:
from_secret: registry_username
password: password:
from_secret: REGISTRY_PASSWORD from_secret: registry_password
repo: artifactory.ravanertebat.ir/tm/pwa
registry: artifactory.ravanertebat.ir - name: deploy-uat
node: image: appleboy/drone-ssh
tag: aecde-docker-runner settings:
host: 10.0.0.3
username: mazyar
port: 22
key:
from_secret: ssh_deploy_key
envs:
- DRONE_BUILD_NUMBER
- DRONE_BRANCH
script:
- set -e
- cd /data/tm-uat
- NEW_TAG="${DRONE_BUILD_NUMBER}-${DRONE_BRANCH}"
- echo "Deploying pwa tag $NEW_TAG"
- sed -i "s/^PWA_IMG_TAG=.*/PWA_IMG_TAG=$NEW_TAG/" .env
- docker compose pull pwa
- docker compose up -d pwa
- docker image prune -f
when:
branch:
- dev
trigger: trigger:
branch: branch:
@@ -45,4 +58,4 @@ trigger:
- uat - uat
- dev - dev
event: event:
- custom - push
+3
View File
@@ -53,3 +53,6 @@ app.*.map.json
**/android/app/google-services.json **/android/app/google-services.json
**/ios/Runner/GoogleService-Info.plist **/ios/Runner/GoogleService-Info.plist
**/lib/firebase_options.dart **/lib/firebase_options.dart
# IDE
.cursor
+6 -2
View File
@@ -1,3 +1,7 @@
FROM docker-mirror.ravanertebat.ir/hub/nginx:1.25.2-alpine3.18-slim FROM nginx:1.25-alpine
COPY ./artifacts/flutter/tm_pwa /usr/share/nginx/html COPY pwa_dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+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.
+22
View File
@@ -3,6 +3,7 @@ import 'package:provider/single_child_widget.dart';
import 'package:tm_app/core/network/network_manager.dart'; import 'package:tm_app/core/network/network_manager.dart';
import 'package:tm_app/core/services/tab_navigation_service.dart'; import 'package:tm_app/core/services/tab_navigation_service.dart';
import 'package:tm_app/core/theme/theme_provider.dart'; import 'package:tm_app/core/theme/theme_provider.dart';
import 'package:tm_app/data/repositories/company_ai_repository.dart';
import 'package:tm_app/data/repositories/home_repository.dart'; import 'package:tm_app/data/repositories/home_repository.dart';
import 'package:tm_app/data/repositories/liked_tenders_repository.dart'; import 'package:tm_app/data/repositories/liked_tenders_repository.dart';
import 'package:tm_app/data/repositories/notification_repository.dart'; import 'package:tm_app/data/repositories/notification_repository.dart';
@@ -13,9 +14,11 @@ import 'package:tm_app/data/services/home_service.dart';
import 'package:tm_app/data/services/liked_tenders_service.dart'; import 'package:tm_app/data/services/liked_tenders_service.dart';
import 'package:tm_app/data/services/notification_check_service.dart'; import 'package:tm_app/data/services/notification_check_service.dart';
import 'package:tm_app/data/services/notification_service.dart'; import 'package:tm_app/data/services/notification_service.dart';
import 'package:tm_app/data/services/company_ai_service.dart';
import 'package:tm_app/data/services/notification_state_service.dart'; import 'package:tm_app/data/services/notification_state_service.dart';
import 'package:tm_app/data/services/tender_detail_service.dart'; import 'package:tm_app/data/services/tender_detail_service.dart';
import 'package:tm_app/data/services/your_tenders_service.dart'; import 'package:tm_app/data/services/your_tenders_service.dart';
import 'package:tm_app/data/services/tender_submissions_service.dart';
import 'package:tm_app/view_models/final_completion_of_documents_view_model.dart'; import 'package:tm_app/view_models/final_completion_of_documents_view_model.dart';
import '../../data/repositories/auth_repository.dart'; import '../../data/repositories/auth_repository.dart';
@@ -79,6 +82,11 @@ List<SingleChildWidget> get apiClients {
create: (context) => YourTendersService(networkManager: context.read()), create: (context) => YourTendersService(networkManager: context.read()),
lazy: true, lazy: true,
), ),
Provider(
create:
(context) => TenderSubmissionsService(networkManager: context.read()),
lazy: true,
),
Provider( Provider(
create: (context) => LikedTendersService(networkManager: context.read()), create: (context) => LikedTendersService(networkManager: context.read()),
lazy: true, lazy: true,
@@ -99,6 +107,10 @@ List<SingleChildWidget> get apiClients {
create: (context) => BoardService(networkManager: context.read()), create: (context) => BoardService(networkManager: context.read()),
lazy: true, lazy: true,
), ),
Provider(
create: (context) => CompanyAiService(networkManager: context.read()),
lazy: true,
),
]; ];
} }
@@ -113,6 +125,7 @@ List<SingleChildWidget> get repositories {
(context) => YourTendersRepository( (context) => YourTendersRepository(
yourTendersService: context.read(), yourTendersService: context.read(),
likedTendersService: context.read(), likedTendersService: context.read(),
tenderSubmissionsService: context.read(),
), ),
lazy: true, lazy: true,
), ),
@@ -122,6 +135,7 @@ List<SingleChildWidget> get repositories {
homeService: context.read(), homeService: context.read(),
yourTendersService: context.read(), yourTendersService: context.read(),
likedTendersService: context.read(), likedTendersService: context.read(),
tenderSubmissionsService: context.read(),
), ),
lazy: true, lazy: true,
), ),
@@ -155,6 +169,14 @@ List<SingleChildWidget> get repositories {
create: (context) => BoardRepository(boardService: context.read()), create: (context) => BoardRepository(boardService: context.read()),
lazy: true, lazy: true,
), ),
Provider(
create:
(context) => CompanyAiRepository(
companyAiService: context.read(),
prefs: context.read(),
),
lazy: true,
),
]; ];
} }
+21
View File
@@ -0,0 +1,21 @@
/// Keys used for values persisted in [SharedPreferences].
///
/// Centralised here so the auth flow, the network layer and the router all
/// agree on the same key. Renaming a key in one place would otherwise silently
/// diverge from the others (e.g. the interceptor's defensive logout fallback).
class PrefKeys {
PrefKeys._();
static const String bearer = 'bearer';
static const String refreshToken = 'refresh_token';
static const String customerData = 'customer_data';
/// Fingerprint (hash of the company's document ids + website) of the last
/// profile version we triggered AI onboarding for. Lets us skip redundant
/// `POST /api/v1/onboarding` calls until the company's data actually changes.
static const String onboardingFingerprint = 'ai_onboarding_fingerprint';
/// Epoch milliseconds when AI onboarding was last started. Used by the
/// Recommended tab to show a "still learning" hint for a short grace period.
static const String onboardingStartedAt = 'ai_onboarding_started_at';
}
+189
View File
@@ -0,0 +1,189 @@
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../constants/pref_keys.dart';
import '../utils/logger.dart';
/// Interceptor responsible for everything authentication-related on the wire:
///
/// * injects the stored bearer token into every outgoing request, and
/// * reacts to `401 Unauthorized` responses by attempting a single token
/// refresh, replaying the original request, and—if that fails—logging the
/// user out and navigating them to the login screen via [onAuthFailed].
///
/// The refresh / login endpoints are skipped so a failed login or a dead
/// refresh token does not recurse back into the refresh flow.
///
/// When several in-flight requests `401` at once after the token expires, only
/// the first one performs the refresh; the rest await the same in-flight
/// [_refreshTokenOnce] future. This is important because [refreshToken] rotates
/// the refresh token, so two concurrent refreshes would have the second post an
/// already-consumed token, fail, and spuriously log the user out.
///
/// (Dio's [QueuedInterceptor] would serialize error handling, but it deadlocks
/// with the replay-via-the-same-Dio pattern used here: the suspended `onError`
/// keeps the error queue busy while it awaits `_dio.fetch`, so a replay that
/// also 401s can never be dequeued. Sharing the refresh future avoids that.)
class AuthInterceptor extends Interceptor {
AuthInterceptor({
required Dio dio,
required SharedPreferences prefs,
required Future<String?> Function() refreshToken,
required Future<void> Function()? Function() onAuthFailed,
}) : _dio = dio,
_prefs = prefs,
_refreshToken = refreshToken,
_onAuthFailed = onAuthFailed;
final Dio _dio;
final SharedPreferences _prefs;
/// Refreshes the access token, returning the new token or `null` on failure.
final Future<String?> Function() _refreshToken;
/// Resolves the (lazily-wired) logout/navigation callback. It is a getter so
/// that the callback can be registered after the interceptor is created,
/// without leaving the interceptor holding a stale `null` reference.
final Future<void> Function()? Function() _onAuthFailed;
/// Endpoints for which a 401 must NOT trigger a token refresh.
static const List<String> _authPaths = <String>[
'/api/v1/profile/login',
'/api/v1/profile/refresh-token',
];
/// Marks a request that has already been replayed once after a refresh, so a
/// second 401 on the replay does not recurse into another refresh + replay.
static const String _retriedFlag = 'auth_retried';
/// The refresh currently in flight, shared across all requests that 401 while
/// it is running so the token is only refreshed (and rotated) once. Cleared
/// when the refresh completes, allowing a later expiry to refresh again.
Future<String?>? _ongoingRefresh;
/// Guards against the auth-failed callback running once per concurrent caller.
/// When the shared refresh fails, every request awaiting it falls through to
/// [_logout]; only the first should fire the (possibly non-idempotent)
/// callback. Set synchronously before the first `await` so the rest of the
/// burst short-circuits, and reset afterwards so a later session can log out.
bool _loggingOut = false;
/// Refreshes the token, coalescing concurrent callers onto a single refresh.
Future<String?> _refreshTokenOnce() {
return _ongoingRefresh ??= _refreshToken().whenComplete(() {
_ongoingRefresh = null;
});
}
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
appLogger.info('🌐 Request: ${options.method} ${options.path}');
final String? token = _prefs.getString(PrefKeys.bearer);
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
appLogger.info(
'✅ Response: ${response.statusCode} ${response.requestOptions.path}',
);
handler.next(response);
}
@override
Future<void> onError(
DioException error,
ErrorInterceptorHandler handler,
) async {
appLogger.error('❌ Error: ${error.message} - ${error.requestOptions.path}');
appLogger.error('❌ Error type: ${error.type}');
final bool isUnauthorized = error.response?.statusCode == 401;
final String path = error.requestOptions.path;
final bool isAuthEndpoint = _authPaths.any(path.contains);
final bool alreadyRetried =
error.requestOptions.extra[_retriedFlag] == true;
// Only the first 401 on a protected endpoint is interesting here. A second
// 401 on the already-replayed request means refreshing won't help (revoked
// permissions, disabled account, clock skew), so just propagate the error.
// The outer onError that issued the replay catches it below and performs the
// logout once — handling it here too would log the user out twice.
if (!isUnauthorized || isAuthEndpoint || alreadyRetried) {
if (isAuthEndpoint) {
appLogger.info('🔄 Skipping token refresh for authentication endpoint');
} else if (alreadyRetried) {
appLogger.error('❌ Still 401 after refresh + replay, giving up');
}
return handler.next(error);
}
// Attempt a single refresh and replay the original request. The replay is
// tagged so a fresh 401 on it short-circuits the block above rather than
// refreshing again. The Authorization header is intentionally not set here:
// [onRequest] re-injects the (now refreshed) bearer token on the replay.
//
// Refresh-window straggler guard: a request sent with the old token can 401
// just after a concurrent refresh already finished (so [_ongoingRefresh] is
// cleared and the single-flight no longer coalesces it). If the stored token
// no longer matches the one this request was sent with, it was already
// rotated by someone else — replay with the fresh token instead of starting
// a second refresh that would needlessly rotate the just-issued token again.
final String? storedToken = _prefs.getString(PrefKeys.bearer);
final String? sentAuthHeader =
error.requestOptions.headers['Authorization'] as String?;
final bool alreadyRefreshed =
storedToken != null && sentAuthHeader != 'Bearer $storedToken';
final String? newAccessToken =
alreadyRefreshed ? storedToken : await _refreshTokenOnce();
if (newAccessToken != null) {
try {
final RequestOptions options = error.requestOptions
..extra[_retriedFlag] = true;
final Response<dynamic> response = await _dio.fetch(options);
return handler.resolve(response);
} on DioException catch (e) {
// Only a fresh 401 means refreshing didn't help (token revoked, account
// disabled) — fall through to logout. Anything else (timeout, connection
// drop, 5xx) is a transient/server failure unrelated to auth: propagate
// it so a flaky network doesn't wipe a perfectly valid session, and so
// the caller sees the real error instead of the stale pre-refresh 401.
if (e.response?.statusCode != 401) {
return handler.next(e);
}
appLogger.error('❌ Retry after token refresh failed: ${e.message}');
}
}
// Refresh (or the replay) failed → force logout and go to login.
appLogger.error('❌ Authentication failed, logging out');
await _logout();
return handler.next(error);
}
Future<void> _logout() async {
if (_loggingOut) return;
_loggingOut = true;
try {
final Future<void> Function()? callback = _onAuthFailed();
if (callback != null) {
await callback();
return;
}
// Defensive fallback: if no logout/navigation callback has been wired yet,
// at least clear the stored credentials so the router's auth guard can
// redirect the user away from protected routes.
appLogger.error('⚠️ No auth-failed callback registered; clearing tokens');
await _prefs.remove(PrefKeys.bearer);
await _prefs.remove(PrefKeys.refreshToken);
await _prefs.remove(PrefKeys.customerData);
} finally {
_loggingOut = false;
}
}
}
+28 -62
View File
@@ -6,9 +6,11 @@ import 'package:tm_app/data/services/model/error/error_model.dart';
import 'package:tm_app/data/services/model/login_response/login_response_model.dart'; import 'package:tm_app/data/services/model/login_response/login_response_model.dart';
import '../config/app_config.dart'; import '../config/app_config.dart';
import '../constants/pref_keys.dart';
import '../utils/logger.dart'; import '../utils/logger.dart';
import '../utils/result.dart'; import '../utils/result.dart';
import 'app_exceptions.dart'; import 'app_exceptions.dart';
import 'auth_interceptor.dart';
import 'network_connectivity.dart'; import 'network_connectivity.dart';
class NetworkManager { class NetworkManager {
@@ -45,54 +47,15 @@ class NetworkManager {
return; return;
} }
final requestInterceptor = InterceptorsWrapper( mainDio.interceptors.add(
onRequest: (options, handler) { AuthInterceptor(
appLogger.info('🌐 Request: ${options.method} ${options.path}'); dio: mainDio,
String? token = _prefs.getString('bearer'); prefs: _prefs,
if (token != null) { refreshToken: refreshToken,
options.headers['Authorization'] = 'Bearer $token'; // Resolved lazily so the callback can be wired up after construction.
} onAuthFailed: () => _onAuthFailed,
return handler.next(options); ),
},
onResponse: (response, handler) {
appLogger.info(
'✅ Response: ${response.statusCode} ${response.requestOptions.path}',
); );
return handler.next(response);
},
onError: (error, handler) async {
appLogger.error(
'❌ Error: ${error.message} - ${error.requestOptions.path}',
);
appLogger.error('❌ Error type: ${error.type}');
if (error.response?.statusCode == 401) {
// Skip refresh token for login and refresh token endpoints
final path = error.requestOptions.path;
if (path.contains('/api/v1/profile/login') ||
path.contains('/api/v1/profile/refresh-token')) {
appLogger.info(
'🔄 Skipping token refresh for authentication endpoints',
);
return handler.next(error);
}
final newAccessToken = await refreshToken();
if (newAccessToken != null) {
mainDio.options.headers['Authorization'] = 'Bearer $newAccessToken';
return handler.resolve(await mainDio.fetch(error.requestOptions));
}
// Refresh token failed force logout
appLogger.error('❌ Refresh token failed, triggering logout');
await _onAuthFailed!();
}
return handler.next(error);
},
);
mainDio.interceptors.add(requestInterceptor);
_interceptorsAdded = true; _interceptorsAdded = true;
} }
@@ -167,7 +130,7 @@ class NetworkManager {
} }
} }
String? bearerToken = _prefs.getString('bearer'); String? bearerToken = _prefs.getString(PrefKeys.bearer);
// String? url = prefs.getString('url'); // String? url = prefs.getString('url');
// appLogger.info('🔗 Base URL: ${url ?? 'NOT SET'}'); // appLogger.info('🔗 Base URL: ${url ?? 'NOT SET'}');
@@ -216,7 +179,16 @@ class NetworkManager {
/// Handle HTTP error responses /// Handle HTTP error responses
Result<T> _handleHttpError<T>(Response response) { Result<T> _handleHttpError<T>(Response response) {
final statusCode = response.statusCode; final statusCode = response.statusCode;
final message = _extractErrorMessage(response.data); var message = _extractErrorMessage(response.data);
if (statusCode == 422 && response.data is Map<String, dynamic>) {
final envelope = response.data as Map<String, dynamic>;
final error = envelope['error'];
if (error is Map<String, dynamic> &&
error['details'] is String &&
(error['details'] as String).isNotEmpty) {
message = error['details'] as String;
}
}
switch (statusCode) { switch (statusCode) {
case 400: case 400:
@@ -275,16 +247,10 @@ class NetworkManager {
); );
case DioExceptionType.badResponse: case DioExceptionType.badResponse:
if (e.response?.statusCode == 401) { if (e.response != null) {
// Handle token refresh logic here if needed // Preserve the backend envelope (including validation details) for
// For now, return authentication error // non-2xx responses such as 409 and 422.
return Result.error( return _handleHttpError(e.response!);
AuthenticationException(
'Authentication failed',
e.response?.statusCode,
e,
),
);
} }
return Result.error( return Result.error(
FetchDataException( FetchDataException(
@@ -344,7 +310,7 @@ class NetworkManager {
/// Refresh the access token using the refresh token /// Refresh the access token using the refresh token
Future<String?> refreshToken() async { Future<String?> refreshToken() async {
try { try {
final refreshToken = _prefs.getString('refresh_token'); final refreshToken = _prefs.getString(PrefKeys.refreshToken);
if (refreshToken == null) { if (refreshToken == null) {
appLogger.error('❌ No refresh token available'); appLogger.error('❌ No refresh token available');
return null; return null;
@@ -363,9 +329,9 @@ class NetworkManager {
final newRefreshToken = loginResponse.data.refreshToken; final newRefreshToken = loginResponse.data.refreshToken;
if (newAccessToken != null) { if (newAccessToken != null) {
await _prefs.setString('bearer', newAccessToken); await _prefs.setString(PrefKeys.bearer, newAccessToken);
if (newRefreshToken != null) { if (newRefreshToken != null) {
await _prefs.setString('refresh_token', newRefreshToken); await _prefs.setString(PrefKeys.refreshToken, newRefreshToken);
} }
appLogger.info('✅ Token refreshed successfully'); appLogger.info('✅ Token refreshed successfully');
return newAccessToken; return newAccessToken;
+2
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../data/repositories/auth_repository.dart'; import '../../data/repositories/auth_repository.dart';
import '../../data/repositories/company_ai_repository.dart';
import '../../data/repositories/profile_repository.dart'; import '../../data/repositories/profile_repository.dart';
import '../../view_models/profile_view_model.dart'; import '../../view_models/profile_view_model.dart';
@@ -13,6 +14,7 @@ Widget profileProvider({required Widget child}) {
(context) => ProfileViewModel( (context) => ProfileViewModel(
profileRepository: context.read<ProfileRepository>(), profileRepository: context.read<ProfileRepository>(),
authRepository: context.read<AuthRepository>(), authRepository: context.read<AuthRepository>(),
companyAiRepository: context.read<CompanyAiRepository>(),
), ),
child: child, child: child,
); );
+19 -3
View File
@@ -1,17 +1,33 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../data/repositories/company_ai_repository.dart';
import '../../data/repositories/home_repository.dart';
import '../../data/repositories/tenders_repository.dart'; import '../../data/repositories/tenders_repository.dart';
import '../../view_models/ai_recommendations_view_model.dart';
import '../../view_models/tenders_view_model.dart'; import '../../view_models/tenders_view_model.dart';
/// Lazy TendersViewModel provider /// Lazy providers for the tenders section.
/// Wraps screens that need TendersViewModel ///
/// Provides [TendersViewModel] (the "All" tab) and [AiRecommendationsViewModel]
/// (the "Recommended" AI tab) to screens that need them.
Widget tendersProvider({required Widget child}) { Widget tendersProvider({required Widget child}) {
return ChangeNotifierProvider( return MultiProvider(
providers: [
ChangeNotifierProvider(
create: create:
(context) => TendersViewModel( (context) => TendersViewModel(
tendersRepository: context.read<TendersRepository>(), tendersRepository: context.read<TendersRepository>(),
), ),
),
ChangeNotifierProvider(
create:
(context) => AiRecommendationsViewModel(
companyAiRepository: context.read<CompanyAiRepository>(),
homeRepository: context.read<HomeRepository>(),
),
),
],
child: child, child: child,
); );
} }
+5 -2
View File
@@ -18,6 +18,7 @@ import '../../views/profile/pages/profile_screen.dart';
import '../../views/splash/pages/splash_screen.dart'; import '../../views/splash/pages/splash_screen.dart';
import '../../views/tenders/pages/tenders_screen.dart'; import '../../views/tenders/pages/tenders_screen.dart';
import '../../views/your_tenders/pages/your_tenders_screen.dart'; import '../../views/your_tenders/pages/your_tenders_screen.dart';
import '../constants/pref_keys.dart';
import '../providers/board_provider.dart'; import '../providers/board_provider.dart';
import '../providers/final_completion_provider.dart'; import '../providers/final_completion_provider.dart';
import '../providers/forgot_password_provider.dart'; import '../providers/forgot_password_provider.dart';
@@ -51,7 +52,7 @@ final GoRouter appRouter = GoRouter(
navigatorKey: rootNavigatorKey, navigatorKey: rootNavigatorKey,
redirect: (context, state) { redirect: (context, state) {
final prefs = context.read<SharedPreferences>(); final prefs = context.read<SharedPreferences>();
final hasToken = prefs.getString('bearer') != null; final hasToken = prefs.getString(PrefKeys.bearer) != null;
final path = state.uri.path; final path = state.uri.path;
final location = state.matchedLocation; final location = state.matchedLocation;
final isSplash = path == '/splash' || location == '/splash'; final isSplash = path == '/splash' || location == '/splash';
@@ -199,7 +200,9 @@ class TendersRouteData extends GoRouteData with _$TendersRouteData {
@override @override
Widget build(BuildContext context, GoRouterState state) { Widget build(BuildContext context, GoRouterState state) {
return tendersProvider(child: const TendersScreen()); return tendersProvider(
child: TendersScreen(initialTab: state.uri.queryParameters['tab']),
);
} }
} }
+1 -19
View File
@@ -47,7 +47,6 @@ mixin _$LoginScreenRoute on GoRouteData {
RouteBase get $splashScreenRoute => GoRouteData.$route( RouteBase get $splashScreenRoute => GoRouteData.$route(
path: '/splash', path: '/splash',
factory: _$SplashScreenRoute._fromState, factory: _$SplashScreenRoute._fromState,
); );
@@ -111,40 +110,33 @@ RouteBase get $appShellRouteData => StatefulShellRouteData.$route(
branches: [ branches: [
StatefulShellBranchData.$branch( StatefulShellBranchData.$branch(
navigatorKey: HomeBranch.$navigatorKey, navigatorKey: HomeBranch.$navigatorKey,
routes: [ routes: [
GoRouteData.$route(path: '/home', factory: _$HomeRouteData._fromState), GoRouteData.$route(path: '/home', factory: _$HomeRouteData._fromState),
], ],
), ),
StatefulShellBranchData.$branch( StatefulShellBranchData.$branch(
navigatorKey: TendersBranch.$navigatorKey, navigatorKey: TendersBranch.$navigatorKey,
routes: [ routes: [
GoRouteData.$route( GoRouteData.$route(
path: '/tenders', path: '/tenders',
factory: _$TendersRouteData._fromState, factory: _$TendersRouteData._fromState,
), ),
], ],
), ),
StatefulShellBranchData.$branch( StatefulShellBranchData.$branch(
navigatorKey: NotificationBranch.$navigatorKey, navigatorKey: NotificationBranch.$navigatorKey,
routes: [ routes: [
GoRouteData.$route( GoRouteData.$route(
path: '/notification', path: '/notification',
factory: _$NotificationRouteData._fromState, factory: _$NotificationRouteData._fromState,
), ),
], ],
), ),
StatefulShellBranchData.$branch( StatefulShellBranchData.$branch(
navigatorKey: ProfileBranch.$navigatorKey, navigatorKey: ProfileBranch.$navigatorKey,
routes: [ routes: [
GoRouteData.$route( GoRouteData.$route(
path: '/profile', path: '/profile',
factory: _$ProfileRouteData._fromState, factory: _$ProfileRouteData._fromState,
), ),
], ],
@@ -242,7 +234,6 @@ mixin _$ProfileRouteData on GoRouteData {
RouteBase get $yourTendersRouteData => GoRouteData.$route( RouteBase get $yourTendersRouteData => GoRouteData.$route(
path: '/your-tenders', path: '/your-tenders',
factory: _$YourTendersRouteData._fromState, factory: _$YourTendersRouteData._fromState,
); );
@@ -274,7 +265,6 @@ mixin _$YourTendersRouteData on GoRouteData {
RouteBase get $finalCompletionOfDocumentsRouteData => GoRouteData.$route( RouteBase get $finalCompletionOfDocumentsRouteData => GoRouteData.$route(
path: '/final_completion_of_documents', path: '/final_completion_of_documents',
factory: _$FinalCompletionOfDocumentsRouteData._fromState, factory: _$FinalCompletionOfDocumentsRouteData._fromState,
); );
@@ -302,7 +292,6 @@ mixin _$FinalCompletionOfDocumentsRouteData on GoRouteData {
RouteBase get $completionOfDocumentsRouteData => GoRouteData.$route( RouteBase get $completionOfDocumentsRouteData => GoRouteData.$route(
path: '/completion_of_documents', path: '/completion_of_documents',
factory: _$CompletionOfDocumentsRouteData._fromState, factory: _$CompletionOfDocumentsRouteData._fromState,
); );
@@ -318,9 +307,7 @@ mixin _$CompletionOfDocumentsRouteData on GoRouteData {
@override @override
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/completion_of_documents', '/completion_of_documents',
queryParams: { queryParams: {if (_self.tenderId != null) 'tender-id': _self.tenderId},
if (_self.tenderId != null) 'tender-id': _self.tenderId!,
},
); );
@override @override
@@ -339,7 +326,6 @@ mixin _$CompletionOfDocumentsRouteData on GoRouteData {
RouteBase get $likedTendersRouteData => GoRouteData.$route( RouteBase get $likedTendersRouteData => GoRouteData.$route(
path: '/like-tenders', path: '/like-tenders',
factory: _$LikedTendersRouteData._fromState, factory: _$LikedTendersRouteData._fromState,
); );
@@ -390,7 +376,6 @@ mixin _$BoardRouteData on GoRouteData {
RouteBase get $tenderDetailRouteData => GoRouteData.$route( RouteBase get $tenderDetailRouteData => GoRouteData.$route(
path: '/tender-detail', path: '/tender-detail',
factory: _$TenderDetailRouteData._fromState, factory: _$TenderDetailRouteData._fromState,
); );
@@ -422,7 +407,6 @@ mixin _$TenderDetailRouteData on GoRouteData {
RouteBase get $forgotPasswordRouteData => GoRouteData.$route( RouteBase get $forgotPasswordRouteData => GoRouteData.$route(
path: '/forgot-password', path: '/forgot-password',
factory: _$ForgotPasswordRouteData._fromState, factory: _$ForgotPasswordRouteData._fromState,
); );
@@ -449,7 +433,6 @@ mixin _$ForgotPasswordRouteData on GoRouteData {
RouteBase get $forgotPasswordOtpRouteData => GoRouteData.$route( RouteBase get $forgotPasswordOtpRouteData => GoRouteData.$route(
path: '/forgot-password-otp', path: '/forgot-password-otp',
factory: _$ForgotPasswordOtpRouteData._fromState, factory: _$ForgotPasswordOtpRouteData._fromState,
); );
@@ -481,7 +464,6 @@ mixin _$ForgotPasswordOtpRouteData on GoRouteData {
RouteBase get $forgotPasswordCreateRouteData => GoRouteData.$route( RouteBase get $forgotPasswordCreateRouteData => GoRouteData.$route(
path: '/forgot-password-create', path: '/forgot-password-create',
factory: _$ForgotPasswordCreateRouteData._fromState, factory: _$ForgotPasswordCreateRouteData._fromState,
); );
+24 -4
View File
@@ -7,7 +7,12 @@ class AppToast {
context: context, context: context,
type: ToastificationType.success, type: ToastificationType.success,
style: ToastificationStyle.flatColored, style: ToastificationStyle.flatColored,
title: Text(message), title: Text(
message,
maxLines: 5,
softWrap: true,
overflow: TextOverflow.ellipsis,
),
autoCloseDuration: const Duration(seconds: 3), autoCloseDuration: const Duration(seconds: 3),
); );
} }
@@ -17,7 +22,12 @@ class AppToast {
context: context, context: context,
type: ToastificationType.error, type: ToastificationType.error,
style: ToastificationStyle.flatColored, style: ToastificationStyle.flatColored,
title: Text(message), title: Text(
message,
maxLines: 5,
softWrap: true,
overflow: TextOverflow.ellipsis,
),
autoCloseDuration: const Duration(seconds: 3), autoCloseDuration: const Duration(seconds: 3),
); );
} }
@@ -31,8 +41,18 @@ class AppToast {
context: context, context: context,
type: ToastificationType.info, type: ToastificationType.info,
style: ToastificationStyle.flatColored, style: ToastificationStyle.flatColored,
title: Text(title), title: Text(
description: Text(description), title,
maxLines: 2,
softWrap: true,
overflow: TextOverflow.ellipsis,
),
description: Text(
description,
maxLines: 5,
softWrap: true,
overflow: TextOverflow.ellipsis,
),
autoCloseDuration: const Duration(seconds: 6), autoCloseDuration: const Duration(seconds: 6),
); );
} }
@@ -0,0 +1,88 @@
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tm_app/core/constants/pref_keys.dart';
import 'package:tm_app/core/utils/logger.dart';
import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/services/company_ai_service.dart';
import 'package:tm_app/data/services/model/onboarding_response/onboarding_response.dart';
import 'package:tm_app/data/services/model/recommend_response/recommend_response.dart';
/// Outcome of [CompanyAiRepository.maybeStartOnboarding].
enum AiOnboardingOutcome {
/// Onboarding was started against the AI service.
started,
/// Skipped: the company has no documents and no website yet.
skippedNoData,
/// Skipped: this profile version was already onboarded.
skippedUnchanged,
/// The onboarding request failed (network/server). Safe to retry later.
failed,
}
/// Coordinates AI onboarding and recommendation calls.
///
/// Onboarding is asynchronous on the AI side and only needs to run when the
/// company's documents or website change, so this repository fingerprints that
/// data in [SharedPreferences] and skips redundant calls — mirroring the
/// `onCompanyProfileReady` guidance in the mobile integration guide.
class CompanyAiRepository {
CompanyAiRepository({
required CompanyAiService companyAiService,
required SharedPreferences prefs,
}) : _companyAiService = companyAiService,
_prefs = prefs;
final CompanyAiService _companyAiService;
final SharedPreferences _prefs;
/// Starts onboarding only when the company has data to index and its
/// fingerprint differs from the last successful run.
Future<AiOnboardingOutcome> maybeStartOnboarding({
required List<String> documentFileIds,
required String? website,
}) async {
final hasWebsite = website != null && website.trim().isNotEmpty;
if (documentFileIds.isEmpty && !hasWebsite) {
return AiOnboardingOutcome.skippedNoData;
}
final fingerprint = _fingerprint(documentFileIds, website);
if (fingerprint == _prefs.getString(PrefKeys.onboardingFingerprint)) {
return AiOnboardingOutcome.skippedUnchanged;
}
final result = await _companyAiService.startOnboarding();
switch (result) {
case Ok<OnboardingResponse>():
await _prefs.setString(PrefKeys.onboardingFingerprint, fingerprint);
await _prefs.setInt(
PrefKeys.onboardingStartedAt,
DateTime.now().millisecondsSinceEpoch,
);
AppLogger().info('✅ AI onboarding started for fingerprint $fingerprint');
return AiOnboardingOutcome.started;
case Error<OnboardingResponse>():
// Don't persist the fingerprint so the next profile load retries.
AppLogger().logWarning('⚠️ AI onboarding failed: ${result.error}');
return AiOnboardingOutcome.failed;
}
}
Future<Result<RecommendResponse>> getRecommendations() {
return _companyAiService.getRecommendations();
}
/// Epoch milliseconds of the last started onboarding, or null if none.
int? lastOnboardingStartedAt() =>
_prefs.getInt(PrefKeys.onboardingStartedAt);
/// Stable, order-independent fingerprint of the data the AI service indexes.
/// Stored verbatim (it is not sensitive) so it compares reliably across app
/// launches, unlike [String.hashCode].
String _fingerprint(List<String> documentFileIds, String? website) {
final ids = [...documentFileIds]..sort();
return '${ids.join(',')}|${website?.trim() ?? ''}';
}
}
+13 -1
View File
@@ -4,6 +4,9 @@ import 'package:tm_app/data/services/liked_tenders_service.dart';
import 'package:tm_app/data/services/model/recommended_tenders_response/recommended_tenders_response.dart'; import 'package:tm_app/data/services/model/recommended_tenders_response/recommended_tenders_response.dart';
import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart'; import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart';
import 'package:tm_app/data/services/model/tender_approvals_stats_response/tender_approvals_stats_response.dart'; import 'package:tm_app/data/services/model/tender_approvals_stats_response/tender_approvals_stats_response.dart';
import 'package:tm_app/data/services/model/feedback_stats_response/feedback_stat_response.dart';
import 'package:tm_app/data/services/model/tender_submission/tender_submission.dart';
import 'package:tm_app/data/services/tender_submissions_service.dart';
import '../services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart'; import '../services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart';
import '../services/model/request_models/tender_feedback_request_model/tender_feedback_request_model.dart'; import '../services/model/request_models/tender_feedback_request_model/tender_feedback_request_model.dart';
@@ -14,12 +17,15 @@ class HomeRepository {
required HomeService homeService, required HomeService homeService,
required YourTendersService yourTendersService, required YourTendersService yourTendersService,
required LikedTendersService likedTendersService, required LikedTendersService likedTendersService,
required TenderSubmissionsService tenderSubmissionsService,
}) : _homeService = homeService, }) : _homeService = homeService,
_yourTendersService = yourTendersService, _yourTendersService = yourTendersService,
_likedTendersService = likedTendersService; _likedTendersService = likedTendersService,
_tenderSubmissionsService = tenderSubmissionsService;
final HomeService _homeService; final HomeService _homeService;
final YourTendersService _yourTendersService; final YourTendersService _yourTendersService;
final LikedTendersService _likedTendersService; final LikedTendersService _likedTendersService;
final TenderSubmissionsService _tenderSubmissionsService;
Future<Result<RecommendedTendersResponse>> getHomeRecommendedTenders({ Future<Result<RecommendedTendersResponse>> getHomeRecommendedTenders({
required int limit, required int limit,
@@ -38,6 +44,12 @@ class HomeRepository {
return _homeService.getTendersApprovalsStats(); return _homeService.getTendersApprovalsStats();
} }
Future<Result<FeedbackStatResponse>> getCustomerFeedbackStats() =>
_homeService.getFeedbackStats();
Future<Result<TenderSubmissionStatsResponse>> getSubmissionStats() =>
_tenderSubmissionsService.getStats();
/// Returns how many tenders the current user has liked (not company-wide). /// Returns how many tenders the current user has liked (not company-wide).
Future<Result<int>> getUserLikedTendersCount() async { Future<Result<int>> getUserLikedTendersCount() async {
final result = await _likedTendersService.getLikedTenders( final result = await _likedTendersService.getLikedTenders(
@@ -4,17 +4,22 @@ import 'package:tm_app/data/services/model/liked_tenders_response/liked_tenders_
import 'package:tm_app/data/services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart'; import 'package:tm_app/data/services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart';
import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart'; import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart';
import 'package:tm_app/data/services/your_tenders_service.dart'; import 'package:tm_app/data/services/your_tenders_service.dart';
import 'package:tm_app/data/services/tender_submissions_service.dart';
import 'package:tm_app/data/services/model/tender_submission/tender_submission.dart';
import '../services/model/request_models/tender_feedback_request_model/tender_feedback_request_model.dart'; import '../services/model/request_models/tender_feedback_request_model/tender_feedback_request_model.dart';
class YourTendersRepository { class YourTendersRepository {
final YourTendersService _yourTendersService; final YourTendersService _yourTendersService;
final LikedTendersService _likedTendersService; final LikedTendersService _likedTendersService;
final TenderSubmissionsService _tenderSubmissionsService;
YourTendersRepository({ YourTendersRepository({
required YourTendersService yourTendersService, required YourTendersService yourTendersService,
required LikedTendersService likedTendersService, required LikedTendersService likedTendersService,
required TenderSubmissionsService tenderSubmissionsService,
}) : _yourTendersService = yourTendersService, }) : _yourTendersService = yourTendersService,
_likedTendersService = likedTendersService; _likedTendersService = likedTendersService,
_tenderSubmissionsService = tenderSubmissionsService;
Future<Result<TenderApprovalsResponse>> getTenders({ Future<Result<TenderApprovalsResponse>> getTenders({
required GetTendersRequestModel requestModel, required GetTendersRequestModel requestModel,
@@ -27,4 +32,41 @@ class YourTendersRepository {
}) { }) {
return _likedTendersService.getLikedTenders(requestModel: requestModel); return _likedTendersService.getLikedTenders(requestModel: requestModel);
} }
Future<Result<TenderSubmissionsResponse>> getSubmissions({
required String stage,
int limit = 20,
int offset = 0,
}) => _tenderSubmissionsService.getSubmissions(
stages: [stage],
limit: limit,
offset: offset,
);
Future<Result<TenderSubmissionResponse>> getSubmissionById(String id) =>
_tenderSubmissionsService.getById(id);
Future<Result<TenderSubmissionResponse>> getSubmissionByTender(
String tenderId,
) => _tenderSubmissionsService.getByTender(tenderId);
Future<Result<TenderSubmissionResponse>> ensureSubmission(
String tenderId, {
String? companyId,
}) => _tenderSubmissionsService.ensure(tenderId, companyId: companyId);
Future<Result<TenderSubmissionResponse>> updateSubmissionStatus({
required String id,
required String status,
String? reason,
String? description,
}) => _tenderSubmissionsService.updateStatus(
id: id,
status: status,
reason: reason,
description: description,
);
Future<Result<TenderSubmissionStatsResponse>> getSubmissionStats() =>
_tenderSubmissionsService.getStats();
} }
+14
View File
@@ -0,0 +1,14 @@
/// Backend endpoints that power AI-based tender recommendations.
///
/// The app talks only to the backend; it gathers company data, forwards it to
/// the AI service and returns results in the standard API envelope. The
/// `company_id` is read from the JWT, so neither endpoint takes a request body.
class CompanyAiApi {
CompanyAiApi._();
/// Registers the company with the AI service. Asynchronous on the AI side.
static const String onboarding = '/api/v1/onboarding';
/// Fetches ranked AI tender recommendations for the company.
static const String recommend = '/api/v1/recommend';
}
+1 -1
View File
@@ -14,7 +14,7 @@ class HomeApi {
} }
static const String tenderApprovalStats = '/api/v1/tender-approvals/stats'; static const String tenderApprovalStats = '/api/v1/tender-approvals/stats';
static const String statsCompany = '/api/v1/feedback/stats/company'; static const String statsCustomer = '/api/v1/feedback/stats/customer';
static const String checkUnreadNotifications = static const String checkUnreadNotifications =
'/api/v1/notifications?seen=false&limit=1&event_type=PUSH'; '/api/v1/notifications?seen=false&limit=1&event_type=PUSH';
} }
+2 -2
View File
@@ -9,8 +9,8 @@ class LikedTendersApi {
required String dateTo, required String dateTo,
}) { }) {
if (dateFrom.isNotEmpty && dateTo.isNotEmpty) { if (dateFrom.isNotEmpty && dateTo.isNotEmpty) {
return '$tendersFeedback?feedback_type=$feedbackType&limit=$limit&offset=$offset&date_from=$dateFrom&date_to=$dateTo'; return '$tendersFeedback?feedback_type=$feedbackType&limit=$limit&offset=$offset&include_total=true&date_from=$dateFrom&date_to=$dateTo';
} }
return '$tendersFeedback?feedback_type=$feedbackType&limit=$limit&offset=$offset'; return '$tendersFeedback?feedback_type=$feedbackType&limit=$limit&offset=$offset&include_total=true';
} }
} }
@@ -0,0 +1,10 @@
class TenderSubmissionsApi {
static const String base = '/api/v1/tender-submissions';
static const String list = base;
static const String stats = '$base/stats';
static String byId(String id) => '$base/$id';
static String byTender(String tenderId) => '$base/tender/$tenderId';
static String ensure(String tenderId) => '$base/tender/$tenderId/ensure';
static String status(String id) => '$base/$id/status';
}
+9 -8
View File
@@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:tm_app/core/constants/pref_keys.dart';
import 'package:tm_app/core/utils/logger.dart'; import 'package:tm_app/core/utils/logger.dart';
import 'package:tm_app/data/services/api/auth_api.dart'; import 'package:tm_app/data/services/api/auth_api.dart';
import 'package:tm_app/data/services/model/customer/customer.dart'; import 'package:tm_app/data/services/model/customer/customer.dart';
@@ -58,13 +59,13 @@ class AuthService {
final refreshToken = result.value.data.refreshToken; final refreshToken = result.value.data.refreshToken;
final customer = result.value.data.customer; final customer = result.value.data.customer;
await prefs.setString('bearer', token!); await prefs.setString(PrefKeys.bearer, token!);
await prefs.setString('refresh_token', refreshToken!); await prefs.setString(PrefKeys.refreshToken, refreshToken!);
// Save customer data as JSON string // Save customer data as JSON string
if (customer != null) { if (customer != null) {
final customerJson = jsonEncode(customer.toJson()); final customerJson = jsonEncode(customer.toJson());
await prefs.setString('customer_data', customerJson); await prefs.setString(PrefKeys.customerData, customerJson);
AppLogger().info('👤 Customer data saved to preferences'); AppLogger().info('👤 Customer data saved to preferences');
} }
} }
@@ -102,9 +103,9 @@ class AuthService {
Future<void> localLogout() async { Future<void> localLogout() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.remove('bearer'); await prefs.remove(PrefKeys.bearer);
await prefs.remove('refresh_token'); await prefs.remove(PrefKeys.refreshToken);
await prefs.remove('customer_data'); await prefs.remove(PrefKeys.customerData);
appLogger.info('tokens and customer data cleared!'); appLogger.info('tokens and customer data cleared!');
} }
@@ -141,7 +142,7 @@ class AuthService {
Future<Result<bool>> checkIsLoggedIn() async { Future<Result<bool>> checkIsLoggedIn() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('bearer'); final token = prefs.getString(PrefKeys.bearer);
if (token == null) { if (token == null) {
return const Result.ok(false); return const Result.ok(false);
} }
@@ -150,7 +151,7 @@ class AuthService {
Future<Customer?> getStoredCustomer() async { Future<Customer?> getStoredCustomer() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final customerJson = prefs.getString('customer_data'); final customerJson = prefs.getString(PrefKeys.customerData);
if (customerJson == null) { if (customerJson == null) {
return null; return null;
} }
+34
View File
@@ -0,0 +1,34 @@
import 'dart:convert';
import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/services/api/company_ai_api.dart';
import 'package:tm_app/data/services/model/onboarding_response/onboarding_response.dart';
import 'package:tm_app/data/services/model/recommend_response/recommend_response.dart';
import '../../core/network/network_manager.dart';
/// Thin wrapper over the AI recommendation endpoints. The backend reads
/// `company_id` from the JWT, so both calls send an empty JSON body.
class CompanyAiService {
CompanyAiService({required NetworkManager networkManager})
: _networkManager = networkManager;
final NetworkManager _networkManager;
Future<Result<OnboardingResponse>> startOnboarding() async {
return _networkManager.makeRequest(
CompanyAiApi.onboarding,
method: 'POST',
(json) => OnboardingResponse.fromJson(json),
data: jsonEncode(const <String, dynamic>{}),
);
}
Future<Result<RecommendResponse>> getRecommendations() async {
return _networkManager.makeRequest(
CompanyAiApi.recommend,
method: 'POST',
(json) => RecommendResponse.fromJson(json),
data: jsonEncode(const <String, dynamic>{}),
);
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ class HomeService {
Future<Result<FeedbackStatResponse>> getFeedbackStats() async { Future<Result<FeedbackStatResponse>> getFeedbackStats() async {
final result = await _networkManager.makeRequest( final result = await _networkManager.makeRequest(
HomeApi.statsCompany, HomeApi.statsCustomer,
(json) => FeedbackStatResponse.fromJson(json), (json) => FeedbackStatResponse.fromJson(json),
method: 'GET', method: 'GET',
); );
+14 -5
View File
@@ -22,11 +22,20 @@ class LikedTendersService {
dateTo: requestModel.dateTo, dateTo: requestModel.dateTo,
); );
final result = await _networkManager.makeRequest( final result = await _networkManager.makeRequest(uri, (json) {
uri, // Feedback pagination is returned at the envelope root, while the
(json) => LikedTendersResponse.fromJson(json), // legacy app model stores it beside data.feedback.
method: 'GET', final normalized = Map<String, dynamic>.from(json);
); final data = normalized['data'];
final meta = normalized['meta'];
if (data is Map && meta is Map) {
normalized['data'] = {
...Map<String, dynamic>.from(data),
'meta': Map<String, dynamic>.from(meta),
};
}
return LikedTendersResponse.fromJson(normalized);
}, method: 'GET');
return result; return result;
} }
@@ -14,6 +14,11 @@ abstract class CompanyProfileData with _$CompanyProfileData {
required String? industry, required String? industry,
@JsonKey(name: 'founded_year') required int? foundedYear, @JsonKey(name: 'founded_year') required int? foundedYear,
@JsonKey(name: 'created_at') required int? createdAt, @JsonKey(name: 'created_at') required int? createdAt,
// Drives the AI onboarding fingerprint (see CompanyAiRepository): onboarding
// re-runs only when the company's documents or website change. Optional so
// existing payloads that omit them keep deserializing.
@JsonKey(name: 'document_file_ids') List<String>? documentFileIds,
String? website,
}) = _CompanyProfileData; }) = _CompanyProfileData;
factory CompanyProfileData.fromJson(Map<String, dynamic> json) => factory CompanyProfileData.fromJson(Map<String, dynamic> json) =>
@@ -15,7 +15,10 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$CompanyProfileData { mixin _$CompanyProfileData {
String? get id; String? get name;@JsonKey(name: 'registration_number') String? get registrationNumber; String? get industry;@JsonKey(name: 'founded_year') int? get foundedYear;@JsonKey(name: 'created_at') int? get createdAt; String? get id; String? get name;@JsonKey(name: 'registration_number') String? get registrationNumber; String? get industry;@JsonKey(name: 'founded_year') int? get foundedYear;@JsonKey(name: 'created_at') int? get createdAt;// Drives the AI onboarding fingerprint (see CompanyAiRepository): onboarding
// re-runs only when the company's documents or website change. Optional so
// existing payloads that omit them keep deserializing.
@JsonKey(name: 'document_file_ids') List<String>? get documentFileIds; String? get website;
/// Create a copy of CompanyProfileData /// Create a copy of CompanyProfileData
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +31,16 @@ $CompanyProfileDataCopyWith<CompanyProfileData> get copyWith => _$CompanyProfile
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CompanyProfileData&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.registrationNumber, registrationNumber) || other.registrationNumber == registrationNumber)&&(identical(other.industry, industry) || other.industry == industry)&&(identical(other.foundedYear, foundedYear) || other.foundedYear == foundedYear)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)); return identical(this, other) || (other.runtimeType == runtimeType&&other is CompanyProfileData&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.registrationNumber, registrationNumber) || other.registrationNumber == registrationNumber)&&(identical(other.industry, industry) || other.industry == industry)&&(identical(other.foundedYear, foundedYear) || other.foundedYear == foundedYear)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&const DeepCollectionEquality().equals(other.documentFileIds, documentFileIds)&&(identical(other.website, website) || other.website == website));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,name,registrationNumber,industry,foundedYear,createdAt); int get hashCode => Object.hash(runtimeType,id,name,registrationNumber,industry,foundedYear,createdAt,const DeepCollectionEquality().hash(documentFileIds),website);
@override @override
String toString() { String toString() {
return 'CompanyProfileData(id: $id, name: $name, registrationNumber: $registrationNumber, industry: $industry, foundedYear: $foundedYear, createdAt: $createdAt)'; return 'CompanyProfileData(id: $id, name: $name, registrationNumber: $registrationNumber, industry: $industry, foundedYear: $foundedYear, createdAt: $createdAt, documentFileIds: $documentFileIds, website: $website)';
} }
@@ -48,7 +51,7 @@ abstract mixin class $CompanyProfileDataCopyWith<$Res> {
factory $CompanyProfileDataCopyWith(CompanyProfileData value, $Res Function(CompanyProfileData) _then) = _$CompanyProfileDataCopyWithImpl; factory $CompanyProfileDataCopyWith(CompanyProfileData value, $Res Function(CompanyProfileData) _then) = _$CompanyProfileDataCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String? id, String? name,@JsonKey(name: 'registration_number') String? registrationNumber, String? industry,@JsonKey(name: 'founded_year') int? foundedYear,@JsonKey(name: 'created_at') int? createdAt String? id, String? name,@JsonKey(name: 'registration_number') String? registrationNumber, String? industry,@JsonKey(name: 'founded_year') int? foundedYear,@JsonKey(name: 'created_at') int? createdAt,@JsonKey(name: 'document_file_ids') List<String>? documentFileIds, String? website
}); });
@@ -65,7 +68,7 @@ class _$CompanyProfileDataCopyWithImpl<$Res>
/// Create a copy of CompanyProfileData /// Create a copy of CompanyProfileData
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = freezed,Object? name = freezed,Object? registrationNumber = freezed,Object? industry = freezed,Object? foundedYear = freezed,Object? createdAt = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? id = freezed,Object? name = freezed,Object? registrationNumber = freezed,Object? industry = freezed,Object? foundedYear = freezed,Object? createdAt = freezed,Object? documentFileIds = freezed,Object? website = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String?,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String?,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@@ -73,7 +76,9 @@ as String?,registrationNumber: freezed == registrationNumber ? _self.registratio
as String?,industry: freezed == industry ? _self.industry : industry // ignore: cast_nullable_to_non_nullable as String?,industry: freezed == industry ? _self.industry : industry // ignore: cast_nullable_to_non_nullable
as String?,foundedYear: freezed == foundedYear ? _self.foundedYear : foundedYear // ignore: cast_nullable_to_non_nullable as String?,foundedYear: freezed == foundedYear ? _self.foundedYear : foundedYear // ignore: cast_nullable_to_non_nullable
as int?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as int?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as int?, as int?,documentFileIds: freezed == documentFileIds ? _self.documentFileIds : documentFileIds // ignore: cast_nullable_to_non_nullable
as List<String>?,website: freezed == website ? _self.website : website // ignore: cast_nullable_to_non_nullable
as String?,
)); ));
} }
@@ -158,10 +163,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? id, String? name, @JsonKey(name: 'registration_number') String? registrationNumber, String? industry, @JsonKey(name: 'founded_year') int? foundedYear, @JsonKey(name: 'created_at') int? createdAt)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? id, String? name, @JsonKey(name: 'registration_number') String? registrationNumber, String? industry, @JsonKey(name: 'founded_year') int? foundedYear, @JsonKey(name: 'created_at') int? createdAt, @JsonKey(name: 'document_file_ids') List<String>? documentFileIds, String? website)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _CompanyProfileData() when $default != null: case _CompanyProfileData() when $default != null:
return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_that.foundedYear,_that.createdAt);case _: return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_that.foundedYear,_that.createdAt,_that.documentFileIds,_that.website);case _:
return orElse(); return orElse();
} }
@@ -179,10 +184,10 @@ return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_tha
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? id, String? name, @JsonKey(name: 'registration_number') String? registrationNumber, String? industry, @JsonKey(name: 'founded_year') int? foundedYear, @JsonKey(name: 'created_at') int? createdAt) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? id, String? name, @JsonKey(name: 'registration_number') String? registrationNumber, String? industry, @JsonKey(name: 'founded_year') int? foundedYear, @JsonKey(name: 'created_at') int? createdAt, @JsonKey(name: 'document_file_ids') List<String>? documentFileIds, String? website) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _CompanyProfileData(): case _CompanyProfileData():
return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_that.foundedYear,_that.createdAt);case _: return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_that.foundedYear,_that.createdAt,_that.documentFileIds,_that.website);case _:
throw StateError('Unexpected subclass'); throw StateError('Unexpected subclass');
} }
@@ -199,10 +204,10 @@ return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_tha
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? id, String? name, @JsonKey(name: 'registration_number') String? registrationNumber, String? industry, @JsonKey(name: 'founded_year') int? foundedYear, @JsonKey(name: 'created_at') int? createdAt)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? id, String? name, @JsonKey(name: 'registration_number') String? registrationNumber, String? industry, @JsonKey(name: 'founded_year') int? foundedYear, @JsonKey(name: 'created_at') int? createdAt, @JsonKey(name: 'document_file_ids') List<String>? documentFileIds, String? website)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _CompanyProfileData() when $default != null: case _CompanyProfileData() when $default != null:
return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_that.foundedYear,_that.createdAt);case _: return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_that.foundedYear,_that.createdAt,_that.documentFileIds,_that.website);case _:
return null; return null;
} }
@@ -214,7 +219,7 @@ return $default(_that.id,_that.name,_that.registrationNumber,_that.industry,_tha
@JsonSerializable() @JsonSerializable()
class _CompanyProfileData implements CompanyProfileData { class _CompanyProfileData implements CompanyProfileData {
const _CompanyProfileData({required this.id, required this.name, @JsonKey(name: 'registration_number') required this.registrationNumber, required this.industry, @JsonKey(name: 'founded_year') required this.foundedYear, @JsonKey(name: 'created_at') required this.createdAt}); const _CompanyProfileData({required this.id, required this.name, @JsonKey(name: 'registration_number') required this.registrationNumber, required this.industry, @JsonKey(name: 'founded_year') required this.foundedYear, @JsonKey(name: 'created_at') required this.createdAt, @JsonKey(name: 'document_file_ids') final List<String>? documentFileIds, this.website}): _documentFileIds = documentFileIds;
factory _CompanyProfileData.fromJson(Map<String, dynamic> json) => _$CompanyProfileDataFromJson(json); factory _CompanyProfileData.fromJson(Map<String, dynamic> json) => _$CompanyProfileDataFromJson(json);
@override final String? id; @override final String? id;
@@ -223,6 +228,22 @@ class _CompanyProfileData implements CompanyProfileData {
@override final String? industry; @override final String? industry;
@override@JsonKey(name: 'founded_year') final int? foundedYear; @override@JsonKey(name: 'founded_year') final int? foundedYear;
@override@JsonKey(name: 'created_at') final int? createdAt; @override@JsonKey(name: 'created_at') final int? createdAt;
// Drives the AI onboarding fingerprint (see CompanyAiRepository): onboarding
// re-runs only when the company's documents or website change. Optional so
// existing payloads that omit them keep deserializing.
final List<String>? _documentFileIds;
// Drives the AI onboarding fingerprint (see CompanyAiRepository): onboarding
// re-runs only when the company's documents or website change. Optional so
// existing payloads that omit them keep deserializing.
@override@JsonKey(name: 'document_file_ids') List<String>? get documentFileIds {
final value = _documentFileIds;
if (value == null) return null;
if (_documentFileIds is EqualUnmodifiableListView) return _documentFileIds;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override final String? website;
/// Create a copy of CompanyProfileData /// Create a copy of CompanyProfileData
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -237,16 +258,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CompanyProfileData&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.registrationNumber, registrationNumber) || other.registrationNumber == registrationNumber)&&(identical(other.industry, industry) || other.industry == industry)&&(identical(other.foundedYear, foundedYear) || other.foundedYear == foundedYear)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _CompanyProfileData&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.registrationNumber, registrationNumber) || other.registrationNumber == registrationNumber)&&(identical(other.industry, industry) || other.industry == industry)&&(identical(other.foundedYear, foundedYear) || other.foundedYear == foundedYear)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&const DeepCollectionEquality().equals(other._documentFileIds, _documentFileIds)&&(identical(other.website, website) || other.website == website));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,id,name,registrationNumber,industry,foundedYear,createdAt); int get hashCode => Object.hash(runtimeType,id,name,registrationNumber,industry,foundedYear,createdAt,const DeepCollectionEquality().hash(_documentFileIds),website);
@override @override
String toString() { String toString() {
return 'CompanyProfileData(id: $id, name: $name, registrationNumber: $registrationNumber, industry: $industry, foundedYear: $foundedYear, createdAt: $createdAt)'; return 'CompanyProfileData(id: $id, name: $name, registrationNumber: $registrationNumber, industry: $industry, foundedYear: $foundedYear, createdAt: $createdAt, documentFileIds: $documentFileIds, website: $website)';
} }
@@ -257,7 +278,7 @@ abstract mixin class _$CompanyProfileDataCopyWith<$Res> implements $CompanyProfi
factory _$CompanyProfileDataCopyWith(_CompanyProfileData value, $Res Function(_CompanyProfileData) _then) = __$CompanyProfileDataCopyWithImpl; factory _$CompanyProfileDataCopyWith(_CompanyProfileData value, $Res Function(_CompanyProfileData) _then) = __$CompanyProfileDataCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String? id, String? name,@JsonKey(name: 'registration_number') String? registrationNumber, String? industry,@JsonKey(name: 'founded_year') int? foundedYear,@JsonKey(name: 'created_at') int? createdAt String? id, String? name,@JsonKey(name: 'registration_number') String? registrationNumber, String? industry,@JsonKey(name: 'founded_year') int? foundedYear,@JsonKey(name: 'created_at') int? createdAt,@JsonKey(name: 'document_file_ids') List<String>? documentFileIds, String? website
}); });
@@ -274,7 +295,7 @@ class __$CompanyProfileDataCopyWithImpl<$Res>
/// Create a copy of CompanyProfileData /// Create a copy of CompanyProfileData
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = freezed,Object? name = freezed,Object? registrationNumber = freezed,Object? industry = freezed,Object? foundedYear = freezed,Object? createdAt = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? id = freezed,Object? name = freezed,Object? registrationNumber = freezed,Object? industry = freezed,Object? foundedYear = freezed,Object? createdAt = freezed,Object? documentFileIds = freezed,Object? website = freezed,}) {
return _then(_CompanyProfileData( return _then(_CompanyProfileData(
id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String?,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable as String?,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
@@ -282,7 +303,9 @@ as String?,registrationNumber: freezed == registrationNumber ? _self.registratio
as String?,industry: freezed == industry ? _self.industry : industry // ignore: cast_nullable_to_non_nullable as String?,industry: freezed == industry ? _self.industry : industry // ignore: cast_nullable_to_non_nullable
as String?,foundedYear: freezed == foundedYear ? _self.foundedYear : foundedYear // ignore: cast_nullable_to_non_nullable as String?,foundedYear: freezed == foundedYear ? _self.foundedYear : foundedYear // ignore: cast_nullable_to_non_nullable
as int?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable as int?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as int?, as int?,documentFileIds: freezed == documentFileIds ? _self._documentFileIds : documentFileIds // ignore: cast_nullable_to_non_nullable
as List<String>?,website: freezed == website ? _self.website : website // ignore: cast_nullable_to_non_nullable
as String?,
)); ));
} }
@@ -14,6 +14,11 @@ _CompanyProfileData _$CompanyProfileDataFromJson(Map<String, dynamic> json) =>
industry: json['industry'] as String?, industry: json['industry'] as String?,
foundedYear: (json['founded_year'] as num?)?.toInt(), foundedYear: (json['founded_year'] as num?)?.toInt(),
createdAt: (json['created_at'] as num?)?.toInt(), createdAt: (json['created_at'] as num?)?.toInt(),
documentFileIds:
(json['document_file_ids'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
website: json['website'] as String?,
); );
Map<String, dynamic> _$CompanyProfileDataToJson(_CompanyProfileData instance) => Map<String, dynamic> _$CompanyProfileDataToJson(_CompanyProfileData instance) =>
@@ -24,4 +29,6 @@ Map<String, dynamic> _$CompanyProfileDataToJson(_CompanyProfileData instance) =>
'industry': instance.industry, 'industry': instance.industry,
'founded_year': instance.foundedYear, 'founded_year': instance.foundedYear,
'created_at': instance.createdAt, 'created_at': instance.createdAt,
'document_file_ids': instance.documentFileIds,
'website': instance.website,
}; };
+20
View File
@@ -0,0 +1,20 @@
// ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart';
part 'lot.freezed.dart';
part 'lot.g.dart';
// Model for entries in the tender-details `lots` array. Only lot_id, title and
// description are mapped — the fields the tender-details "Lots" section renders.
// Add more fields here if the UI grows.
@freezed
abstract class Lot with _$Lot {
const factory Lot({
@JsonKey(name: 'lot_id') required String? lotId,
required String? title,
required String? description,
}) = _Lot;
factory Lot.fromJson(Map<String, Object?> json) => _$LotFromJson(json);
}
@@ -0,0 +1,283 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'lot.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$Lot {
@JsonKey(name: 'lot_id') String? get lotId; String? get title; String? get description;
/// Create a copy of Lot
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$LotCopyWith<Lot> get copyWith => _$LotCopyWithImpl<Lot>(this as Lot, _$identity);
/// Serializes this Lot to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is Lot&&(identical(other.lotId, lotId) || other.lotId == lotId)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,lotId,title,description);
@override
String toString() {
return 'Lot(lotId: $lotId, title: $title, description: $description)';
}
}
/// @nodoc
abstract mixin class $LotCopyWith<$Res> {
factory $LotCopyWith(Lot value, $Res Function(Lot) _then) = _$LotCopyWithImpl;
@useResult
$Res call({
@JsonKey(name: 'lot_id') String? lotId, String? title, String? description
});
}
/// @nodoc
class _$LotCopyWithImpl<$Res>
implements $LotCopyWith<$Res> {
_$LotCopyWithImpl(this._self, this._then);
final Lot _self;
final $Res Function(Lot) _then;
/// Create a copy of Lot
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? lotId = freezed,Object? title = freezed,Object? description = freezed,}) {
return _then(_self.copyWith(
lotId: freezed == lotId ? _self.lotId : lotId // ignore: cast_nullable_to_non_nullable
as String?,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// Adds pattern-matching-related methods to [Lot].
extension LotPatterns on Lot {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _Lot value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _Lot() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _Lot value) $default,){
final _that = this;
switch (_that) {
case _Lot():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _Lot value)? $default,){
final _that = this;
switch (_that) {
case _Lot() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(name: 'lot_id') String? lotId, String? title, String? description)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _Lot() when $default != null:
return $default(_that.lotId,_that.title,_that.description);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(name: 'lot_id') String? lotId, String? title, String? description) $default,) {final _that = this;
switch (_that) {
case _Lot():
return $default(_that.lotId,_that.title,_that.description);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(name: 'lot_id') String? lotId, String? title, String? description)? $default,) {final _that = this;
switch (_that) {
case _Lot() when $default != null:
return $default(_that.lotId,_that.title,_that.description);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _Lot implements Lot {
const _Lot({@JsonKey(name: 'lot_id') required this.lotId, required this.title, required this.description});
factory _Lot.fromJson(Map<String, dynamic> json) => _$LotFromJson(json);
@override@JsonKey(name: 'lot_id') final String? lotId;
@override final String? title;
@override final String? description;
/// Create a copy of Lot
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$LotCopyWith<_Lot> get copyWith => __$LotCopyWithImpl<_Lot>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$LotToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Lot&&(identical(other.lotId, lotId) || other.lotId == lotId)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,lotId,title,description);
@override
String toString() {
return 'Lot(lotId: $lotId, title: $title, description: $description)';
}
}
/// @nodoc
abstract mixin class _$LotCopyWith<$Res> implements $LotCopyWith<$Res> {
factory _$LotCopyWith(_Lot value, $Res Function(_Lot) _then) = __$LotCopyWithImpl;
@override @useResult
$Res call({
@JsonKey(name: 'lot_id') String? lotId, String? title, String? description
});
}
/// @nodoc
class __$LotCopyWithImpl<$Res>
implements _$LotCopyWith<$Res> {
__$LotCopyWithImpl(this._self, this._then);
final _Lot _self;
final $Res Function(_Lot) _then;
/// Create a copy of Lot
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? lotId = freezed,Object? title = freezed,Object? description = freezed,}) {
return _then(_Lot(
lotId: freezed == lotId ? _self.lotId : lotId // ignore: cast_nullable_to_non_nullable
as String?,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
// dart format on
+19
View File
@@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'lot.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_Lot _$LotFromJson(Map<String, dynamic> json) => _Lot(
lotId: json['lot_id'] as String?,
title: json['title'] as String?,
description: json['description'] as String?,
);
Map<String, dynamic> _$LotToJson(_Lot instance) => <String, dynamic>{
'lot_id': instance.lotId,
'title': instance.title,
'description': instance.description,
};
@@ -0,0 +1,19 @@
// ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart';
part 'onboarding_data.freezed.dart';
part 'onboarding_data.g.dart';
/// Payload returned by `POST /api/v1/onboarding`.
///
/// Onboarding is asynchronous on the AI side, so the only field is a coarse
/// [status] (e.g. `started`). Recommendations may stay empty until the AI
/// service finishes indexing.
@freezed
abstract class OnboardingData with _$OnboardingData {
const factory OnboardingData({required String? status}) = _OnboardingData;
factory OnboardingData.fromJson(Map<String, dynamic> json) =>
_$OnboardingDataFromJson(json);
}
@@ -0,0 +1,277 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'onboarding_data.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$OnboardingData {
String? get status;
/// Create a copy of OnboardingData
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$OnboardingDataCopyWith<OnboardingData> get copyWith => _$OnboardingDataCopyWithImpl<OnboardingData>(this as OnboardingData, _$identity);
/// Serializes this OnboardingData to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is OnboardingData&&(identical(other.status, status) || other.status == status));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,status);
@override
String toString() {
return 'OnboardingData(status: $status)';
}
}
/// @nodoc
abstract mixin class $OnboardingDataCopyWith<$Res> {
factory $OnboardingDataCopyWith(OnboardingData value, $Res Function(OnboardingData) _then) = _$OnboardingDataCopyWithImpl;
@useResult
$Res call({
String? status
});
}
/// @nodoc
class _$OnboardingDataCopyWithImpl<$Res>
implements $OnboardingDataCopyWith<$Res> {
_$OnboardingDataCopyWithImpl(this._self, this._then);
final OnboardingData _self;
final $Res Function(OnboardingData) _then;
/// Create a copy of OnboardingData
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? status = freezed,}) {
return _then(_self.copyWith(
status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// Adds pattern-matching-related methods to [OnboardingData].
extension OnboardingDataPatterns on OnboardingData {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _OnboardingData value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _OnboardingData() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _OnboardingData value) $default,){
final _that = this;
switch (_that) {
case _OnboardingData():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _OnboardingData value)? $default,){
final _that = this;
switch (_that) {
case _OnboardingData() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? status)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _OnboardingData() when $default != null:
return $default(_that.status);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? status) $default,) {final _that = this;
switch (_that) {
case _OnboardingData():
return $default(_that.status);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? status)? $default,) {final _that = this;
switch (_that) {
case _OnboardingData() when $default != null:
return $default(_that.status);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _OnboardingData implements OnboardingData {
const _OnboardingData({required this.status});
factory _OnboardingData.fromJson(Map<String, dynamic> json) => _$OnboardingDataFromJson(json);
@override final String? status;
/// Create a copy of OnboardingData
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$OnboardingDataCopyWith<_OnboardingData> get copyWith => __$OnboardingDataCopyWithImpl<_OnboardingData>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$OnboardingDataToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _OnboardingData&&(identical(other.status, status) || other.status == status));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,status);
@override
String toString() {
return 'OnboardingData(status: $status)';
}
}
/// @nodoc
abstract mixin class _$OnboardingDataCopyWith<$Res> implements $OnboardingDataCopyWith<$Res> {
factory _$OnboardingDataCopyWith(_OnboardingData value, $Res Function(_OnboardingData) _then) = __$OnboardingDataCopyWithImpl;
@override @useResult
$Res call({
String? status
});
}
/// @nodoc
class __$OnboardingDataCopyWithImpl<$Res>
implements _$OnboardingDataCopyWith<$Res> {
__$OnboardingDataCopyWithImpl(this._self, this._then);
final _OnboardingData _self;
final $Res Function(_OnboardingData) _then;
/// Create a copy of OnboardingData
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? status = freezed,}) {
return _then(_OnboardingData(
status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
// dart format on
@@ -0,0 +1,13 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'onboarding_data.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_OnboardingData _$OnboardingDataFromJson(Map<String, dynamic> json) =>
_OnboardingData(status: json['status'] as String?);
Map<String, dynamic> _$OnboardingDataToJson(_OnboardingData instance) =>
<String, dynamic>{'status': instance.status};
@@ -0,0 +1,24 @@
// ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart';
import '../error/error_model.dart';
import '../onboarding_data/onboarding_data.dart';
part 'onboarding_response.freezed.dart';
part 'onboarding_response.g.dart';
/// Standard API envelope for `POST /api/v1/onboarding`.
@freezed
abstract class OnboardingResponse with _$OnboardingResponse {
@JsonSerializable(explicitToJson: true)
const factory OnboardingResponse({
@JsonKey(name: 'data') required OnboardingData? data,
@JsonKey(name: 'error') required ErrorModel? error,
required String? message,
required bool? success,
}) = _OnboardingResponse;
factory OnboardingResponse.fromJson(Map<String, dynamic> json) =>
_$OnboardingResponseFromJson(json);
}
@@ -0,0 +1,334 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'onboarding_response.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$OnboardingResponse {
@JsonKey(name: 'data') OnboardingData? get data;@JsonKey(name: 'error') ErrorModel? get error; String? get message; bool? get success;
/// Create a copy of OnboardingResponse
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$OnboardingResponseCopyWith<OnboardingResponse> get copyWith => _$OnboardingResponseCopyWithImpl<OnboardingResponse>(this as OnboardingResponse, _$identity);
/// Serializes this OnboardingResponse to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is OnboardingResponse&&(identical(other.data, data) || other.data == data)&&(identical(other.error, error) || other.error == error)&&(identical(other.message, message) || other.message == message)&&(identical(other.success, success) || other.success == success));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,data,error,message,success);
@override
String toString() {
return 'OnboardingResponse(data: $data, error: $error, message: $message, success: $success)';
}
}
/// @nodoc
abstract mixin class $OnboardingResponseCopyWith<$Res> {
factory $OnboardingResponseCopyWith(OnboardingResponse value, $Res Function(OnboardingResponse) _then) = _$OnboardingResponseCopyWithImpl;
@useResult
$Res call({
@JsonKey(name: 'data') OnboardingData? data,@JsonKey(name: 'error') ErrorModel? error, String? message, bool? success
});
$OnboardingDataCopyWith<$Res>? get data;$ErrorModelCopyWith<$Res>? get error;
}
/// @nodoc
class _$OnboardingResponseCopyWithImpl<$Res>
implements $OnboardingResponseCopyWith<$Res> {
_$OnboardingResponseCopyWithImpl(this._self, this._then);
final OnboardingResponse _self;
final $Res Function(OnboardingResponse) _then;
/// Create a copy of OnboardingResponse
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? data = freezed,Object? error = freezed,Object? message = freezed,Object? success = freezed,}) {
return _then(_self.copyWith(
data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as OnboardingData?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as ErrorModel?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String?,success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable
as bool?,
));
}
/// Create a copy of OnboardingResponse
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$OnboardingDataCopyWith<$Res>? get data {
if (_self.data == null) {
return null;
}
return $OnboardingDataCopyWith<$Res>(_self.data!, (value) {
return _then(_self.copyWith(data: value));
});
}/// Create a copy of OnboardingResponse
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ErrorModelCopyWith<$Res>? get error {
if (_self.error == null) {
return null;
}
return $ErrorModelCopyWith<$Res>(_self.error!, (value) {
return _then(_self.copyWith(error: value));
});
}
}
/// Adds pattern-matching-related methods to [OnboardingResponse].
extension OnboardingResponsePatterns on OnboardingResponse {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _OnboardingResponse value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _OnboardingResponse() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _OnboardingResponse value) $default,){
final _that = this;
switch (_that) {
case _OnboardingResponse():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _OnboardingResponse value)? $default,){
final _that = this;
switch (_that) {
case _OnboardingResponse() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(name: 'data') OnboardingData? data, @JsonKey(name: 'error') ErrorModel? error, String? message, bool? success)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _OnboardingResponse() when $default != null:
return $default(_that.data,_that.error,_that.message,_that.success);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(name: 'data') OnboardingData? data, @JsonKey(name: 'error') ErrorModel? error, String? message, bool? success) $default,) {final _that = this;
switch (_that) {
case _OnboardingResponse():
return $default(_that.data,_that.error,_that.message,_that.success);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(name: 'data') OnboardingData? data, @JsonKey(name: 'error') ErrorModel? error, String? message, bool? success)? $default,) {final _that = this;
switch (_that) {
case _OnboardingResponse() when $default != null:
return $default(_that.data,_that.error,_that.message,_that.success);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable(explicitToJson: true)
class _OnboardingResponse implements OnboardingResponse {
const _OnboardingResponse({@JsonKey(name: 'data') required this.data, @JsonKey(name: 'error') required this.error, required this.message, required this.success});
factory _OnboardingResponse.fromJson(Map<String, dynamic> json) => _$OnboardingResponseFromJson(json);
@override@JsonKey(name: 'data') final OnboardingData? data;
@override@JsonKey(name: 'error') final ErrorModel? error;
@override final String? message;
@override final bool? success;
/// Create a copy of OnboardingResponse
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$OnboardingResponseCopyWith<_OnboardingResponse> get copyWith => __$OnboardingResponseCopyWithImpl<_OnboardingResponse>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$OnboardingResponseToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _OnboardingResponse&&(identical(other.data, data) || other.data == data)&&(identical(other.error, error) || other.error == error)&&(identical(other.message, message) || other.message == message)&&(identical(other.success, success) || other.success == success));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,data,error,message,success);
@override
String toString() {
return 'OnboardingResponse(data: $data, error: $error, message: $message, success: $success)';
}
}
/// @nodoc
abstract mixin class _$OnboardingResponseCopyWith<$Res> implements $OnboardingResponseCopyWith<$Res> {
factory _$OnboardingResponseCopyWith(_OnboardingResponse value, $Res Function(_OnboardingResponse) _then) = __$OnboardingResponseCopyWithImpl;
@override @useResult
$Res call({
@JsonKey(name: 'data') OnboardingData? data,@JsonKey(name: 'error') ErrorModel? error, String? message, bool? success
});
@override $OnboardingDataCopyWith<$Res>? get data;@override $ErrorModelCopyWith<$Res>? get error;
}
/// @nodoc
class __$OnboardingResponseCopyWithImpl<$Res>
implements _$OnboardingResponseCopyWith<$Res> {
__$OnboardingResponseCopyWithImpl(this._self, this._then);
final _OnboardingResponse _self;
final $Res Function(_OnboardingResponse) _then;
/// Create a copy of OnboardingResponse
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? data = freezed,Object? error = freezed,Object? message = freezed,Object? success = freezed,}) {
return _then(_OnboardingResponse(
data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as OnboardingData?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as ErrorModel?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String?,success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable
as bool?,
));
}
/// Create a copy of OnboardingResponse
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$OnboardingDataCopyWith<$Res>? get data {
if (_self.data == null) {
return null;
}
return $OnboardingDataCopyWith<$Res>(_self.data!, (value) {
return _then(_self.copyWith(data: value));
});
}/// Create a copy of OnboardingResponse
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ErrorModelCopyWith<$Res>? get error {
if (_self.error == null) {
return null;
}
return $ErrorModelCopyWith<$Res>(_self.error!, (value) {
return _then(_self.copyWith(error: value));
});
}
}
// dart format on
@@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'onboarding_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_OnboardingResponse _$OnboardingResponseFromJson(Map<String, dynamic> json) =>
_OnboardingResponse(
data:
json['data'] == null
? null
: OnboardingData.fromJson(json['data'] as Map<String, dynamic>),
error:
json['error'] == null
? null
: ErrorModel.fromJson(json['error'] as Map<String, dynamic>),
message: json['message'] as String?,
success: json['success'] as bool?,
);
Map<String, dynamic> _$OnboardingResponseToJson(_OnboardingResponse instance) =>
<String, dynamic>{
'data': instance.data?.toJson(),
'error': instance.error?.toJson(),
'message': instance.message,
'success': instance.success,
};
@@ -1,15 +1,22 @@
// ignore_for_file: invalid_annotation_target // ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:tm_app/data/services/model/organization/organization_address.dart';
part 'organization.freezed.dart'; part 'organization.freezed.dart';
part 'organization.g.dart'; part 'organization.g.dart';
// company_id and a nested address (OrganizationAddress) surface buyer details
// on the tender-details page. A dedicated OrganizationAddress type is used
// instead of the existing `Address` model because the buyer address shape
// differs (city_name + country_code only) and is unrelated to the user-profile
// Address.
@freezed @freezed
abstract class Organization with _$Organization { abstract class Organization with _$Organization {
const factory Organization({ const factory Organization({
required String? name, required String? name,
@JsonKey(name: 'company_id') required String? companyId,
required OrganizationAddress? address,
}) = _Organization; }) = _Organization;
factory Organization.fromJson(Map<String, Object?> json) => factory Organization.fromJson(Map<String, Object?> json) =>
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$Organization { mixin _$Organization {
String? get name; String? get name;@JsonKey(name: 'company_id') String? get companyId; OrganizationAddress? get address;
/// Create a copy of Organization /// Create a copy of Organization
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $OrganizationCopyWith<Organization> get copyWith => _$OrganizationCopyWithImpl<O
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is Organization&&(identical(other.name, name) || other.name == name)); return identical(this, other) || (other.runtimeType == runtimeType&&other is Organization&&(identical(other.name, name) || other.name == name)&&(identical(other.companyId, companyId) || other.companyId == companyId)&&(identical(other.address, address) || other.address == address));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,name); int get hashCode => Object.hash(runtimeType,name,companyId,address);
@override @override
String toString() { String toString() {
return 'Organization(name: $name)'; return 'Organization(name: $name, companyId: $companyId, address: $address)';
} }
@@ -48,11 +48,11 @@ abstract mixin class $OrganizationCopyWith<$Res> {
factory $OrganizationCopyWith(Organization value, $Res Function(Organization) _then) = _$OrganizationCopyWithImpl; factory $OrganizationCopyWith(Organization value, $Res Function(Organization) _then) = _$OrganizationCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
String? name String? name,@JsonKey(name: 'company_id') String? companyId, OrganizationAddress? address
}); });
$OrganizationAddressCopyWith<$Res>? get address;
} }
/// @nodoc /// @nodoc
@@ -65,13 +65,27 @@ class _$OrganizationCopyWithImpl<$Res>
/// Create a copy of Organization /// Create a copy of Organization
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? name = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? name = freezed,Object? companyId = freezed,Object? address = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?, as String?,companyId: freezed == companyId ? _self.companyId : companyId // ignore: cast_nullable_to_non_nullable
as String?,address: freezed == address ? _self.address : address // ignore: cast_nullable_to_non_nullable
as OrganizationAddress?,
)); ));
} }
/// Create a copy of Organization
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$OrganizationAddressCopyWith<$Res>? get address {
if (_self.address == null) {
return null;
}
return $OrganizationAddressCopyWith<$Res>(_self.address!, (value) {
return _then(_self.copyWith(address: value));
});
}
} }
@@ -153,10 +167,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? name)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? name, @JsonKey(name: 'company_id') String? companyId, OrganizationAddress? address)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _Organization() when $default != null: case _Organization() when $default != null:
return $default(_that.name);case _: return $default(_that.name,_that.companyId,_that.address);case _:
return orElse(); return orElse();
} }
@@ -174,10 +188,10 @@ return $default(_that.name);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? name) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? name, @JsonKey(name: 'company_id') String? companyId, OrganizationAddress? address) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _Organization(): case _Organization():
return $default(_that.name);case _: return $default(_that.name,_that.companyId,_that.address);case _:
throw StateError('Unexpected subclass'); throw StateError('Unexpected subclass');
} }
@@ -194,10 +208,10 @@ return $default(_that.name);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? name)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? name, @JsonKey(name: 'company_id') String? companyId, OrganizationAddress? address)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _Organization() when $default != null: case _Organization() when $default != null:
return $default(_that.name);case _: return $default(_that.name,_that.companyId,_that.address);case _:
return null; return null;
} }
@@ -209,10 +223,12 @@ return $default(_that.name);case _:
@JsonSerializable() @JsonSerializable()
class _Organization implements Organization { class _Organization implements Organization {
const _Organization({required this.name}); const _Organization({required this.name, @JsonKey(name: 'company_id') required this.companyId, required this.address});
factory _Organization.fromJson(Map<String, dynamic> json) => _$OrganizationFromJson(json); factory _Organization.fromJson(Map<String, dynamic> json) => _$OrganizationFromJson(json);
@override final String? name; @override final String? name;
@override@JsonKey(name: 'company_id') final String? companyId;
@override final OrganizationAddress? address;
/// Create a copy of Organization /// Create a copy of Organization
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -227,16 +243,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Organization&&(identical(other.name, name) || other.name == name)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _Organization&&(identical(other.name, name) || other.name == name)&&(identical(other.companyId, companyId) || other.companyId == companyId)&&(identical(other.address, address) || other.address == address));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,name); int get hashCode => Object.hash(runtimeType,name,companyId,address);
@override @override
String toString() { String toString() {
return 'Organization(name: $name)'; return 'Organization(name: $name, companyId: $companyId, address: $address)';
} }
@@ -247,11 +263,11 @@ abstract mixin class _$OrganizationCopyWith<$Res> implements $OrganizationCopyWi
factory _$OrganizationCopyWith(_Organization value, $Res Function(_Organization) _then) = __$OrganizationCopyWithImpl; factory _$OrganizationCopyWith(_Organization value, $Res Function(_Organization) _then) = __$OrganizationCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
String? name String? name,@JsonKey(name: 'company_id') String? companyId, OrganizationAddress? address
}); });
@override $OrganizationAddressCopyWith<$Res>? get address;
} }
/// @nodoc /// @nodoc
@@ -264,14 +280,28 @@ class __$OrganizationCopyWithImpl<$Res>
/// Create a copy of Organization /// Create a copy of Organization
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? name = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? name = freezed,Object? companyId = freezed,Object? address = freezed,}) {
return _then(_Organization( return _then(_Organization(
name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String?, as String?,companyId: freezed == companyId ? _self.companyId : companyId // ignore: cast_nullable_to_non_nullable
as String?,address: freezed == address ? _self.address : address // ignore: cast_nullable_to_non_nullable
as OrganizationAddress?,
)); ));
} }
/// Create a copy of Organization
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$OrganizationAddressCopyWith<$Res>? get address {
if (_self.address == null) {
return null;
}
return $OrganizationAddressCopyWith<$Res>(_self.address!, (value) {
return _then(_self.copyWith(address: value));
});
}
} }
// dart format on // dart format on
@@ -7,7 +7,20 @@ part of 'organization.dart';
// ************************************************************************** // **************************************************************************
_Organization _$OrganizationFromJson(Map<String, dynamic> json) => _Organization _$OrganizationFromJson(Map<String, dynamic> json) =>
_Organization(name: json['name'] as String?); _Organization(
name: json['name'] as String?,
companyId: json['company_id'] as String?,
address:
json['address'] == null
? null
: OrganizationAddress.fromJson(
json['address'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$OrganizationToJson(_Organization instance) => Map<String, dynamic> _$OrganizationToJson(_Organization instance) =>
<String, dynamic>{'name': instance.name}; <String, dynamic>{
'name': instance.name,
'company_id': instance.companyId,
'address': instance.address,
};
@@ -0,0 +1,21 @@
// ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart';
part 'organization_address.freezed.dart';
part 'organization_address.g.dart';
// Model for the buyer_organization.address object from the tender-details API.
// Kept separate from the existing profile `Address` model because the shapes
// differ (this one only needs city_name and country_code). Only the fields the
// UI shows are mapped.
@freezed
abstract class OrganizationAddress with _$OrganizationAddress {
const factory OrganizationAddress({
@JsonKey(name: 'city_name') required String? cityName,
@JsonKey(name: 'country_code') required String? countryCode,
}) = _OrganizationAddress;
factory OrganizationAddress.fromJson(Map<String, Object?> json) =>
_$OrganizationAddressFromJson(json);
}
@@ -0,0 +1,280 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'organization_address.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$OrganizationAddress {
@JsonKey(name: 'city_name') String? get cityName;@JsonKey(name: 'country_code') String? get countryCode;
/// Create a copy of OrganizationAddress
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$OrganizationAddressCopyWith<OrganizationAddress> get copyWith => _$OrganizationAddressCopyWithImpl<OrganizationAddress>(this as OrganizationAddress, _$identity);
/// Serializes this OrganizationAddress to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is OrganizationAddress&&(identical(other.cityName, cityName) || other.cityName == cityName)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,cityName,countryCode);
@override
String toString() {
return 'OrganizationAddress(cityName: $cityName, countryCode: $countryCode)';
}
}
/// @nodoc
abstract mixin class $OrganizationAddressCopyWith<$Res> {
factory $OrganizationAddressCopyWith(OrganizationAddress value, $Res Function(OrganizationAddress) _then) = _$OrganizationAddressCopyWithImpl;
@useResult
$Res call({
@JsonKey(name: 'city_name') String? cityName,@JsonKey(name: 'country_code') String? countryCode
});
}
/// @nodoc
class _$OrganizationAddressCopyWithImpl<$Res>
implements $OrganizationAddressCopyWith<$Res> {
_$OrganizationAddressCopyWithImpl(this._self, this._then);
final OrganizationAddress _self;
final $Res Function(OrganizationAddress) _then;
/// Create a copy of OrganizationAddress
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? cityName = freezed,Object? countryCode = freezed,}) {
return _then(_self.copyWith(
cityName: freezed == cityName ? _self.cityName : cityName // ignore: cast_nullable_to_non_nullable
as String?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// Adds pattern-matching-related methods to [OrganizationAddress].
extension OrganizationAddressPatterns on OrganizationAddress {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _OrganizationAddress value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _OrganizationAddress() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _OrganizationAddress value) $default,){
final _that = this;
switch (_that) {
case _OrganizationAddress():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _OrganizationAddress value)? $default,){
final _that = this;
switch (_that) {
case _OrganizationAddress() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(name: 'city_name') String? cityName, @JsonKey(name: 'country_code') String? countryCode)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _OrganizationAddress() when $default != null:
return $default(_that.cityName,_that.countryCode);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(name: 'city_name') String? cityName, @JsonKey(name: 'country_code') String? countryCode) $default,) {final _that = this;
switch (_that) {
case _OrganizationAddress():
return $default(_that.cityName,_that.countryCode);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(name: 'city_name') String? cityName, @JsonKey(name: 'country_code') String? countryCode)? $default,) {final _that = this;
switch (_that) {
case _OrganizationAddress() when $default != null:
return $default(_that.cityName,_that.countryCode);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _OrganizationAddress implements OrganizationAddress {
const _OrganizationAddress({@JsonKey(name: 'city_name') required this.cityName, @JsonKey(name: 'country_code') required this.countryCode});
factory _OrganizationAddress.fromJson(Map<String, dynamic> json) => _$OrganizationAddressFromJson(json);
@override@JsonKey(name: 'city_name') final String? cityName;
@override@JsonKey(name: 'country_code') final String? countryCode;
/// Create a copy of OrganizationAddress
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$OrganizationAddressCopyWith<_OrganizationAddress> get copyWith => __$OrganizationAddressCopyWithImpl<_OrganizationAddress>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$OrganizationAddressToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _OrganizationAddress&&(identical(other.cityName, cityName) || other.cityName == cityName)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,cityName,countryCode);
@override
String toString() {
return 'OrganizationAddress(cityName: $cityName, countryCode: $countryCode)';
}
}
/// @nodoc
abstract mixin class _$OrganizationAddressCopyWith<$Res> implements $OrganizationAddressCopyWith<$Res> {
factory _$OrganizationAddressCopyWith(_OrganizationAddress value, $Res Function(_OrganizationAddress) _then) = __$OrganizationAddressCopyWithImpl;
@override @useResult
$Res call({
@JsonKey(name: 'city_name') String? cityName,@JsonKey(name: 'country_code') String? countryCode
});
}
/// @nodoc
class __$OrganizationAddressCopyWithImpl<$Res>
implements _$OrganizationAddressCopyWith<$Res> {
__$OrganizationAddressCopyWithImpl(this._self, this._then);
final _OrganizationAddress _self;
final $Res Function(_OrganizationAddress) _then;
/// Create a copy of OrganizationAddress
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? cityName = freezed,Object? countryCode = freezed,}) {
return _then(_OrganizationAddress(
cityName: freezed == cityName ? _self.cityName : cityName // ignore: cast_nullable_to_non_nullable
as String?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
// dart format on
@@ -0,0 +1,20 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'organization_address.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_OrganizationAddress _$OrganizationAddressFromJson(Map<String, dynamic> json) =>
_OrganizationAddress(
cityName: json['city_name'] as String?,
countryCode: json['country_code'] as String?,
);
Map<String, dynamic> _$OrganizationAddressToJson(
_OrganizationAddress instance,
) => <String, dynamic>{
'city_name': instance.cityName,
'country_code': instance.countryCode,
};
@@ -0,0 +1,27 @@
// ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart';
import '../error/error_model.dart';
import '../recommendation_item/recommendation_item.dart';
part 'recommend_response.freezed.dart';
part 'recommend_response.g.dart';
/// Standard API envelope for `POST /api/v1/recommend`.
///
/// [data] holds the ranked recommendations and may be empty (or null) until
/// the AI service finishes indexing the company.
@freezed
abstract class RecommendResponse with _$RecommendResponse {
@JsonSerializable(explicitToJson: true)
const factory RecommendResponse({
@JsonKey(name: 'data') required List<RecommendationItem>? data,
@JsonKey(name: 'error') required ErrorModel? error,
required String? message,
required bool? success,
}) = _RecommendResponse;
factory RecommendResponse.fromJson(Map<String, dynamic> json) =>
_$RecommendResponseFromJson(json);
}
@@ -0,0 +1,318 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'recommend_response.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$RecommendResponse {
@JsonKey(name: 'data') List<RecommendationItem>? get data;@JsonKey(name: 'error') ErrorModel? get error; String? get message; bool? get success;
/// Create a copy of RecommendResponse
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$RecommendResponseCopyWith<RecommendResponse> get copyWith => _$RecommendResponseCopyWithImpl<RecommendResponse>(this as RecommendResponse, _$identity);
/// Serializes this RecommendResponse to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RecommendResponse&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.error, error) || other.error == error)&&(identical(other.message, message) || other.message == message)&&(identical(other.success, success) || other.success == success));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(data),error,message,success);
@override
String toString() {
return 'RecommendResponse(data: $data, error: $error, message: $message, success: $success)';
}
}
/// @nodoc
abstract mixin class $RecommendResponseCopyWith<$Res> {
factory $RecommendResponseCopyWith(RecommendResponse value, $Res Function(RecommendResponse) _then) = _$RecommendResponseCopyWithImpl;
@useResult
$Res call({
@JsonKey(name: 'data') List<RecommendationItem>? data,@JsonKey(name: 'error') ErrorModel? error, String? message, bool? success
});
$ErrorModelCopyWith<$Res>? get error;
}
/// @nodoc
class _$RecommendResponseCopyWithImpl<$Res>
implements $RecommendResponseCopyWith<$Res> {
_$RecommendResponseCopyWithImpl(this._self, this._then);
final RecommendResponse _self;
final $Res Function(RecommendResponse) _then;
/// Create a copy of RecommendResponse
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? data = freezed,Object? error = freezed,Object? message = freezed,Object? success = freezed,}) {
return _then(_self.copyWith(
data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as List<RecommendationItem>?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as ErrorModel?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String?,success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable
as bool?,
));
}
/// Create a copy of RecommendResponse
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ErrorModelCopyWith<$Res>? get error {
if (_self.error == null) {
return null;
}
return $ErrorModelCopyWith<$Res>(_self.error!, (value) {
return _then(_self.copyWith(error: value));
});
}
}
/// Adds pattern-matching-related methods to [RecommendResponse].
extension RecommendResponsePatterns on RecommendResponse {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _RecommendResponse value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _RecommendResponse() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _RecommendResponse value) $default,){
final _that = this;
switch (_that) {
case _RecommendResponse():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _RecommendResponse value)? $default,){
final _that = this;
switch (_that) {
case _RecommendResponse() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(name: 'data') List<RecommendationItem>? data, @JsonKey(name: 'error') ErrorModel? error, String? message, bool? success)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _RecommendResponse() when $default != null:
return $default(_that.data,_that.error,_that.message,_that.success);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(name: 'data') List<RecommendationItem>? data, @JsonKey(name: 'error') ErrorModel? error, String? message, bool? success) $default,) {final _that = this;
switch (_that) {
case _RecommendResponse():
return $default(_that.data,_that.error,_that.message,_that.success);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(name: 'data') List<RecommendationItem>? data, @JsonKey(name: 'error') ErrorModel? error, String? message, bool? success)? $default,) {final _that = this;
switch (_that) {
case _RecommendResponse() when $default != null:
return $default(_that.data,_that.error,_that.message,_that.success);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable(explicitToJson: true)
class _RecommendResponse implements RecommendResponse {
const _RecommendResponse({@JsonKey(name: 'data') required final List<RecommendationItem>? data, @JsonKey(name: 'error') required this.error, required this.message, required this.success}): _data = data;
factory _RecommendResponse.fromJson(Map<String, dynamic> json) => _$RecommendResponseFromJson(json);
final List<RecommendationItem>? _data;
@override@JsonKey(name: 'data') List<RecommendationItem>? get data {
final value = _data;
if (value == null) return null;
if (_data is EqualUnmodifiableListView) return _data;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override@JsonKey(name: 'error') final ErrorModel? error;
@override final String? message;
@override final bool? success;
/// Create a copy of RecommendResponse
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$RecommendResponseCopyWith<_RecommendResponse> get copyWith => __$RecommendResponseCopyWithImpl<_RecommendResponse>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$RecommendResponseToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RecommendResponse&&const DeepCollectionEquality().equals(other._data, _data)&&(identical(other.error, error) || other.error == error)&&(identical(other.message, message) || other.message == message)&&(identical(other.success, success) || other.success == success));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_data),error,message,success);
@override
String toString() {
return 'RecommendResponse(data: $data, error: $error, message: $message, success: $success)';
}
}
/// @nodoc
abstract mixin class _$RecommendResponseCopyWith<$Res> implements $RecommendResponseCopyWith<$Res> {
factory _$RecommendResponseCopyWith(_RecommendResponse value, $Res Function(_RecommendResponse) _then) = __$RecommendResponseCopyWithImpl;
@override @useResult
$Res call({
@JsonKey(name: 'data') List<RecommendationItem>? data,@JsonKey(name: 'error') ErrorModel? error, String? message, bool? success
});
@override $ErrorModelCopyWith<$Res>? get error;
}
/// @nodoc
class __$RecommendResponseCopyWithImpl<$Res>
implements _$RecommendResponseCopyWith<$Res> {
__$RecommendResponseCopyWithImpl(this._self, this._then);
final _RecommendResponse _self;
final $Res Function(_RecommendResponse) _then;
/// Create a copy of RecommendResponse
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? data = freezed,Object? error = freezed,Object? message = freezed,Object? success = freezed,}) {
return _then(_RecommendResponse(
data: freezed == data ? _self._data : data // ignore: cast_nullable_to_non_nullable
as List<RecommendationItem>?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as ErrorModel?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String?,success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable
as bool?,
));
}
/// Create a copy of RecommendResponse
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ErrorModelCopyWith<$Res>? get error {
if (_self.error == null) {
return null;
}
return $ErrorModelCopyWith<$Res>(_self.error!, (value) {
return _then(_self.copyWith(error: value));
});
}
}
// dart format on
@@ -0,0 +1,31 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'recommend_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_RecommendResponse _$RecommendResponseFromJson(Map<String, dynamic> json) =>
_RecommendResponse(
data:
(json['data'] as List<dynamic>?)
?.map(
(e) => RecommendationItem.fromJson(e as Map<String, dynamic>),
)
.toList(),
error:
json['error'] == null
? null
: ErrorModel.fromJson(json['error'] as Map<String, dynamic>),
message: json['message'] as String?,
success: json['success'] as bool?,
);
Map<String, dynamic> _$RecommendResponseToJson(_RecommendResponse instance) =>
<String, dynamic>{
'data': instance.data?.map((e) => e.toJson()).toList(),
'error': instance.error?.toJson(),
'message': instance.message,
'success': instance.success,
};
@@ -0,0 +1,24 @@
// ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart';
part 'recommendation_item.freezed.dart';
part 'recommendation_item.g.dart';
/// A single AI tender recommendation returned by `POST /api/v1/recommend`.
///
/// [tenderId] is the backend tender id used with
/// `GET /api/v1/tenders/details/:id`. [rank] is a display order string where
/// `1` is the best match. All fields are nullable to stay resilient to partial
/// API payloads.
@freezed
abstract class RecommendationItem with _$RecommendationItem {
const factory RecommendationItem({
required String? rank,
@JsonKey(name: 'tender_id') required String? tenderId,
required String? analysis,
}) = _RecommendationItem;
factory RecommendationItem.fromJson(Map<String, dynamic> json) =>
_$RecommendationItemFromJson(json);
}
@@ -0,0 +1,283 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'recommendation_item.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$RecommendationItem {
String? get rank;@JsonKey(name: 'tender_id') String? get tenderId; String? get analysis;
/// Create a copy of RecommendationItem
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$RecommendationItemCopyWith<RecommendationItem> get copyWith => _$RecommendationItemCopyWithImpl<RecommendationItem>(this as RecommendationItem, _$identity);
/// Serializes this RecommendationItem to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RecommendationItem&&(identical(other.rank, rank) || other.rank == rank)&&(identical(other.tenderId, tenderId) || other.tenderId == tenderId)&&(identical(other.analysis, analysis) || other.analysis == analysis));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,rank,tenderId,analysis);
@override
String toString() {
return 'RecommendationItem(rank: $rank, tenderId: $tenderId, analysis: $analysis)';
}
}
/// @nodoc
abstract mixin class $RecommendationItemCopyWith<$Res> {
factory $RecommendationItemCopyWith(RecommendationItem value, $Res Function(RecommendationItem) _then) = _$RecommendationItemCopyWithImpl;
@useResult
$Res call({
String? rank,@JsonKey(name: 'tender_id') String? tenderId, String? analysis
});
}
/// @nodoc
class _$RecommendationItemCopyWithImpl<$Res>
implements $RecommendationItemCopyWith<$Res> {
_$RecommendationItemCopyWithImpl(this._self, this._then);
final RecommendationItem _self;
final $Res Function(RecommendationItem) _then;
/// Create a copy of RecommendationItem
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? rank = freezed,Object? tenderId = freezed,Object? analysis = freezed,}) {
return _then(_self.copyWith(
rank: freezed == rank ? _self.rank : rank // ignore: cast_nullable_to_non_nullable
as String?,tenderId: freezed == tenderId ? _self.tenderId : tenderId // ignore: cast_nullable_to_non_nullable
as String?,analysis: freezed == analysis ? _self.analysis : analysis // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// Adds pattern-matching-related methods to [RecommendationItem].
extension RecommendationItemPatterns on RecommendationItem {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _RecommendationItem value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _RecommendationItem() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _RecommendationItem value) $default,){
final _that = this;
switch (_that) {
case _RecommendationItem():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _RecommendationItem value)? $default,){
final _that = this;
switch (_that) {
case _RecommendationItem() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? rank, @JsonKey(name: 'tender_id') String? tenderId, String? analysis)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _RecommendationItem() when $default != null:
return $default(_that.rank,_that.tenderId,_that.analysis);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? rank, @JsonKey(name: 'tender_id') String? tenderId, String? analysis) $default,) {final _that = this;
switch (_that) {
case _RecommendationItem():
return $default(_that.rank,_that.tenderId,_that.analysis);case _:
throw StateError('Unexpected subclass');
}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? rank, @JsonKey(name: 'tender_id') String? tenderId, String? analysis)? $default,) {final _that = this;
switch (_that) {
case _RecommendationItem() when $default != null:
return $default(_that.rank,_that.tenderId,_that.analysis);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _RecommendationItem implements RecommendationItem {
const _RecommendationItem({required this.rank, @JsonKey(name: 'tender_id') required this.tenderId, required this.analysis});
factory _RecommendationItem.fromJson(Map<String, dynamic> json) => _$RecommendationItemFromJson(json);
@override final String? rank;
@override@JsonKey(name: 'tender_id') final String? tenderId;
@override final String? analysis;
/// Create a copy of RecommendationItem
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$RecommendationItemCopyWith<_RecommendationItem> get copyWith => __$RecommendationItemCopyWithImpl<_RecommendationItem>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$RecommendationItemToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RecommendationItem&&(identical(other.rank, rank) || other.rank == rank)&&(identical(other.tenderId, tenderId) || other.tenderId == tenderId)&&(identical(other.analysis, analysis) || other.analysis == analysis));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,rank,tenderId,analysis);
@override
String toString() {
return 'RecommendationItem(rank: $rank, tenderId: $tenderId, analysis: $analysis)';
}
}
/// @nodoc
abstract mixin class _$RecommendationItemCopyWith<$Res> implements $RecommendationItemCopyWith<$Res> {
factory _$RecommendationItemCopyWith(_RecommendationItem value, $Res Function(_RecommendationItem) _then) = __$RecommendationItemCopyWithImpl;
@override @useResult
$Res call({
String? rank,@JsonKey(name: 'tender_id') String? tenderId, String? analysis
});
}
/// @nodoc
class __$RecommendationItemCopyWithImpl<$Res>
implements _$RecommendationItemCopyWith<$Res> {
__$RecommendationItemCopyWithImpl(this._self, this._then);
final _RecommendationItem _self;
final $Res Function(_RecommendationItem) _then;
/// Create a copy of RecommendationItem
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? rank = freezed,Object? tenderId = freezed,Object? analysis = freezed,}) {
return _then(_RecommendationItem(
rank: freezed == rank ? _self.rank : rank // ignore: cast_nullable_to_non_nullable
as String?,tenderId: freezed == tenderId ? _self.tenderId : tenderId // ignore: cast_nullable_to_non_nullable
as String?,analysis: freezed == analysis ? _self.analysis : analysis // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
// dart format on
@@ -0,0 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'recommendation_item.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_RecommendationItem _$RecommendationItemFromJson(Map<String, dynamic> json) =>
_RecommendationItem(
rank: json['rank'] as String?,
tenderId: json['tender_id'] as String?,
analysis: json['analysis'] as String?,
);
Map<String, dynamic> _$RecommendationItemToJson(_RecommendationItem instance) =>
<String, dynamic>{
'rank': instance.rank,
'tender_id': instance.tenderId,
'analysis': instance.analysis,
};
@@ -10,16 +10,13 @@ part 'recommended_tenders_response.g.dart';
@freezed @freezed
abstract class RecommendedTendersResponse with _$RecommendedTendersResponse { abstract class RecommendedTendersResponse with _$RecommendedTendersResponse {
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
const factory const factory RecommendedTendersResponse({
RecommendedTendersResponse({ @JsonKey(name: 'success') required bool? success,
required bool? succes,
required String? message, required String? message,
required ErrorModel? error, required ErrorModel? error,
required RecommendedTendersData? data, required RecommendedTendersData? data,
}) = _RecommendedTendersResponse; }) = _RecommendedTendersResponse;
factory factory RecommendedTendersResponse.fromJson(Map<String, dynamic> json) =>
RecommendedTendersResponse.fromJson(Map<String, dynamic> json) =>
_$RecommendedTendersResponseFromJson(json); _$RecommendedTendersResponseFromJson(json);
} }
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$RecommendedTendersResponse { mixin _$RecommendedTendersResponse {
bool? get succes; String? get message; ErrorModel? get error; RecommendedTendersData? get data; bool? get success; String? get message; ErrorModel? get error; RecommendedTendersData? get data;
/// Create a copy of RecommendedTendersResponse /// Create a copy of RecommendedTendersResponse
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $RecommendedTendersResponseCopyWith<RecommendedTendersResponse> get copyWith =>
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is RecommendedTendersResponse&&(identical(other.succes, succes) || other.succes == succes)&&(identical(other.message, message) || other.message == message)&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)); return identical(this, other) || (other.runtimeType == runtimeType&&other is RecommendedTendersResponse&&(identical(other.success, success) || other.success == success)&&(identical(other.message, message) || other.message == message)&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,succes,message,error,data); int get hashCode => Object.hash(runtimeType,success,message,error,data);
@override @override
String toString() { String toString() {
return 'RecommendedTendersResponse(succes: $succes, message: $message, error: $error, data: $data)'; return 'RecommendedTendersResponse(success: $success, message: $message, error: $error, data: $data)';
} }
@@ -48,7 +48,7 @@ abstract mixin class $RecommendedTendersResponseCopyWith<$Res> {
factory $RecommendedTendersResponseCopyWith(RecommendedTendersResponse value, $Res Function(RecommendedTendersResponse) _then) = _$RecommendedTendersResponseCopyWithImpl; factory $RecommendedTendersResponseCopyWith(RecommendedTendersResponse value, $Res Function(RecommendedTendersResponse) _then) = _$RecommendedTendersResponseCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
bool? succes, String? message, ErrorModel? error, RecommendedTendersData? data bool? success, String? message, ErrorModel? error, RecommendedTendersData? data
}); });
@@ -65,9 +65,9 @@ class _$RecommendedTendersResponseCopyWithImpl<$Res>
/// Create a copy of RecommendedTendersResponse /// Create a copy of RecommendedTendersResponse
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? succes = freezed,Object? message = freezed,Object? error = freezed,Object? data = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? success = freezed,Object? message = freezed,Object? error = freezed,Object? data = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
succes: freezed == succes ? _self.succes : succes // ignore: cast_nullable_to_non_nullable success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable
as bool?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable as bool?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as ErrorModel?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable as ErrorModel?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
@@ -180,10 +180,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool? succes, String? message, ErrorModel? error, RecommendedTendersData? data)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool? success, String? message, ErrorModel? error, RecommendedTendersData? data)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _RecommendedTendersResponse() when $default != null: case _RecommendedTendersResponse() when $default != null:
return $default(_that.succes,_that.message,_that.error,_that.data);case _: return $default(_that.success,_that.message,_that.error,_that.data);case _:
return orElse(); return orElse();
} }
@@ -201,10 +201,10 @@ return $default(_that.succes,_that.message,_that.error,_that.data);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool? succes, String? message, ErrorModel? error, RecommendedTendersData? data) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool? success, String? message, ErrorModel? error, RecommendedTendersData? data) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _RecommendedTendersResponse(): case _RecommendedTendersResponse():
return $default(_that.succes,_that.message,_that.error,_that.data);case _: return $default(_that.success,_that.message,_that.error,_that.data);case _:
throw StateError('Unexpected subclass'); throw StateError('Unexpected subclass');
} }
@@ -221,10 +221,10 @@ return $default(_that.succes,_that.message,_that.error,_that.data);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool? succes, String? message, ErrorModel? error, RecommendedTendersData? data)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool? success, String? message, ErrorModel? error, RecommendedTendersData? data)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _RecommendedTendersResponse() when $default != null: case _RecommendedTendersResponse() when $default != null:
return $default(_that.succes,_that.message,_that.error,_that.data);case _: return $default(_that.success,_that.message,_that.error,_that.data);case _:
return null; return null;
} }
@@ -236,10 +236,10 @@ return $default(_that.succes,_that.message,_that.error,_that.data);case _:
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
class _RecommendedTendersResponse implements RecommendedTendersResponse { class _RecommendedTendersResponse implements RecommendedTendersResponse {
const _RecommendedTendersResponse({required this.succes, required this.message, required this.error, required this.data}); const _RecommendedTendersResponse({required this.success, required this.message, required this.error, required this.data});
factory _RecommendedTendersResponse.fromJson(Map<String, dynamic> json) => _$RecommendedTendersResponseFromJson(json); factory _RecommendedTendersResponse.fromJson(Map<String, dynamic> json) => _$RecommendedTendersResponseFromJson(json);
@override final bool? succes; @override final bool? success;
@override final String? message; @override final String? message;
@override final ErrorModel? error; @override final ErrorModel? error;
@override final RecommendedTendersData? data; @override final RecommendedTendersData? data;
@@ -257,16 +257,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _RecommendedTendersResponse&&(identical(other.succes, succes) || other.succes == succes)&&(identical(other.message, message) || other.message == message)&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _RecommendedTendersResponse&&(identical(other.success, success) || other.success == success)&&(identical(other.message, message) || other.message == message)&&(identical(other.error, error) || other.error == error)&&(identical(other.data, data) || other.data == data));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash(runtimeType,succes,message,error,data); int get hashCode => Object.hash(runtimeType,success,message,error,data);
@override @override
String toString() { String toString() {
return 'RecommendedTendersResponse(succes: $succes, message: $message, error: $error, data: $data)'; return 'RecommendedTendersResponse(success: $success, message: $message, error: $error, data: $data)';
} }
@@ -277,7 +277,7 @@ abstract mixin class _$RecommendedTendersResponseCopyWith<$Res> implements $Reco
factory _$RecommendedTendersResponseCopyWith(_RecommendedTendersResponse value, $Res Function(_RecommendedTendersResponse) _then) = __$RecommendedTendersResponseCopyWithImpl; factory _$RecommendedTendersResponseCopyWith(_RecommendedTendersResponse value, $Res Function(_RecommendedTendersResponse) _then) = __$RecommendedTendersResponseCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
bool? succes, String? message, ErrorModel? error, RecommendedTendersData? data bool? success, String? message, ErrorModel? error, RecommendedTendersData? data
}); });
@@ -294,9 +294,9 @@ class __$RecommendedTendersResponseCopyWithImpl<$Res>
/// Create a copy of RecommendedTendersResponse /// Create a copy of RecommendedTendersResponse
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? succes = freezed,Object? message = freezed,Object? error = freezed,Object? data = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? success = freezed,Object? message = freezed,Object? error = freezed,Object? data = freezed,}) {
return _then(_RecommendedTendersResponse( return _then(_RecommendedTendersResponse(
succes: freezed == succes ? _self.succes : succes // ignore: cast_nullable_to_non_nullable success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable
as bool?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable as bool?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable as String?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
as ErrorModel?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable as ErrorModel?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
@@ -9,7 +9,7 @@ part of 'recommended_tenders_response.dart';
_RecommendedTendersResponse _$RecommendedTendersResponseFromJson( _RecommendedTendersResponse _$RecommendedTendersResponseFromJson(
Map<String, dynamic> json, Map<String, dynamic> json,
) => _RecommendedTendersResponse( ) => _RecommendedTendersResponse(
succes: json['succes'] as bool?, success: json['success'] as bool?,
message: json['message'] as String?, message: json['message'] as String?,
error: error:
json['error'] == null json['error'] == null
@@ -26,7 +26,7 @@ _RecommendedTendersResponse _$RecommendedTendersResponseFromJson(
Map<String, dynamic> _$RecommendedTendersResponseToJson( Map<String, dynamic> _$RecommendedTendersResponseToJson(
_RecommendedTendersResponse instance, _RecommendedTendersResponse instance,
) => <String, dynamic>{ ) => <String, dynamic>{
'succes': instance.succes, 'success': instance.success,
'message': instance.message, 'message': instance.message,
'error': instance.error?.toJson(), 'error': instance.error?.toJson(),
'data': instance.data?.toJson(), 'data': instance.data?.toJson(),
@@ -2,11 +2,18 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:tm_app/core/utils/date_utils.dart'; import 'package:tm_app/core/utils/date_utils.dart';
import 'package:tm_app/data/services/model/lot/lot.dart';
import 'package:tm_app/data/services/model/organization/organization.dart'; import 'package:tm_app/data/services/model/organization/organization.dart';
part 'tender_data.freezed.dart'; part 'tender_data.freezed.dart';
part 'tender_data.g.dart'; part 'tender_data.g.dart';
// Fields mirror the /tenders/details/:id response so the tender-details page
// can show them: notice_type_code, form_type, notice_language_code, issue_date,
// issue_time, lots, official_languages. issue_date/issue_time reuse
// unixTimestampFromJson (epoch seconds, tolerant of ms/string) like the other
// timestamp fields. All fields are nullable to stay resilient to partial API
// payloads.
@freezed @freezed
abstract class TenderData with _$TenderData { abstract class TenderData with _$TenderData {
const factory TenderData({ const factory TenderData({
@@ -16,6 +23,13 @@ abstract class TenderData with _$TenderData {
@JsonKey(name: 'tender_id') required String? tenderId, @JsonKey(name: 'tender_id') required String? tenderId,
@JsonKey(name: 'notice_publication_id') @JsonKey(name: 'notice_publication_id')
required String? noticePublicationId, required String? noticePublicationId,
@JsonKey(name: 'notice_type_code') required String? noticeTypeCode,
@JsonKey(name: 'form_type') required String? formType,
@JsonKey(name: 'notice_language_code') required String? noticeLanguageCode,
@JsonKey(name: 'issue_date', fromJson: unixTimestampFromJson)
required int? issueDate,
@JsonKey(name: 'issue_time', fromJson: unixTimestampFromJson)
required int? issueTime,
@JsonKey(name: 'publication_date', fromJson: unixTimestampFromJson) @JsonKey(name: 'publication_date', fromJson: unixTimestampFromJson)
required int? publicationDate, required int? publicationDate,
required String? title, required String? title,
@@ -37,6 +51,9 @@ abstract class TenderData with _$TenderData {
@JsonKey(name: 'duration_unit') required String? durationUnit, @JsonKey(name: 'duration_unit') required String? durationUnit,
@JsonKey(name: 'buyer_organization') @JsonKey(name: 'buyer_organization')
required Organization? buyerOrganization, required Organization? buyerOrganization,
required List<Lot>? lots,
@JsonKey(name: 'official_languages')
required List<String>? officialLanguages,
required String? status, required String? status,
}) = _TenderData; }) = _TenderData;
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$TenderData { mixin _$TenderData {
bool? get success; String? get message; String? get id;@JsonKey(name: 'tender_id') String? get tenderId;@JsonKey(name: 'notice_publication_id') String? get noticePublicationId;@JsonKey(name: 'publication_date') int? get publicationDate; String? get title; String? get description;@JsonKey(name: 'procurement_type_code') String? get procurementTypeCode;@JsonKey(name: 'procedure_code') String? get procedureCode;@JsonKey(name: 'estimated_value') int? get estimatedValue; String? get currency; String? get duration;@JsonKey(name: 'tender_deadline') int? get tenderDeadline;@JsonKey(name: 'submission_deadline') int? get submissionDeadline;@JsonKey(name: 'application_deadline') int? get applicationDeadline;@JsonKey(name: 'submission_url') String? get submissionUrl;@JsonKey(name: 'country_code') String? get countryCode;@JsonKey(name: 'duration_unit') String? get durationUnit;@JsonKey(name: 'buyer_organization') Organization? get buyerOrganization; String? get status; bool? get success; String? get message; String? get id;@JsonKey(name: 'tender_id') String? get tenderId;@JsonKey(name: 'notice_publication_id') String? get noticePublicationId;@JsonKey(name: 'notice_type_code') String? get noticeTypeCode;@JsonKey(name: 'form_type') String? get formType;@JsonKey(name: 'notice_language_code') String? get noticeLanguageCode;@JsonKey(name: 'issue_date', fromJson: unixTimestampFromJson) int? get issueDate;@JsonKey(name: 'issue_time', fromJson: unixTimestampFromJson) int? get issueTime;@JsonKey(name: 'publication_date', fromJson: unixTimestampFromJson) int? get publicationDate; String? get title; String? get description;@JsonKey(name: 'procurement_type_code') String? get procurementTypeCode;@JsonKey(name: 'procedure_code') String? get procedureCode;@JsonKey(name: 'estimated_value') int? get estimatedValue; String? get currency; String? get duration;@JsonKey(name: 'tender_deadline', fromJson: unixTimestampFromJson) int? get tenderDeadline;@JsonKey(name: 'submission_deadline', fromJson: unixTimestampFromJson) int? get submissionDeadline;@JsonKey(name: 'application_deadline', fromJson: unixTimestampFromJson) int? get applicationDeadline;@JsonKey(name: 'submission_url') String? get submissionUrl;@JsonKey(name: 'country_code') String? get countryCode;@JsonKey(name: 'duration_unit') String? get durationUnit;@JsonKey(name: 'buyer_organization') Organization? get buyerOrganization; List<Lot>? get lots;@JsonKey(name: 'official_languages') List<String>? get officialLanguages; String? get status;
/// Create a copy of TenderData /// Create a copy of TenderData
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $TenderDataCopyWith<TenderData> get copyWith => _$TenderDataCopyWithImpl<TenderD
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is TenderData&&(identical(other.success, success) || other.success == success)&&(identical(other.message, message) || other.message == message)&&(identical(other.id, id) || other.id == id)&&(identical(other.tenderId, tenderId) || other.tenderId == tenderId)&&(identical(other.noticePublicationId, noticePublicationId) || other.noticePublicationId == noticePublicationId)&&(identical(other.publicationDate, publicationDate) || other.publicationDate == publicationDate)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.procurementTypeCode, procurementTypeCode) || other.procurementTypeCode == procurementTypeCode)&&(identical(other.procedureCode, procedureCode) || other.procedureCode == procedureCode)&&(identical(other.estimatedValue, estimatedValue) || other.estimatedValue == estimatedValue)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.tenderDeadline, tenderDeadline) || other.tenderDeadline == tenderDeadline)&&(identical(other.submissionDeadline, submissionDeadline) || other.submissionDeadline == submissionDeadline)&&(identical(other.applicationDeadline, applicationDeadline) || other.applicationDeadline == applicationDeadline)&&(identical(other.submissionUrl, submissionUrl) || other.submissionUrl == submissionUrl)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode)&&(identical(other.durationUnit, durationUnit) || other.durationUnit == durationUnit)&&(identical(other.buyerOrganization, buyerOrganization) || other.buyerOrganization == buyerOrganization)&&(identical(other.status, status) || other.status == status)); return identical(this, other) || (other.runtimeType == runtimeType&&other is TenderData&&(identical(other.success, success) || other.success == success)&&(identical(other.message, message) || other.message == message)&&(identical(other.id, id) || other.id == id)&&(identical(other.tenderId, tenderId) || other.tenderId == tenderId)&&(identical(other.noticePublicationId, noticePublicationId) || other.noticePublicationId == noticePublicationId)&&(identical(other.noticeTypeCode, noticeTypeCode) || other.noticeTypeCode == noticeTypeCode)&&(identical(other.formType, formType) || other.formType == formType)&&(identical(other.noticeLanguageCode, noticeLanguageCode) || other.noticeLanguageCode == noticeLanguageCode)&&(identical(other.issueDate, issueDate) || other.issueDate == issueDate)&&(identical(other.issueTime, issueTime) || other.issueTime == issueTime)&&(identical(other.publicationDate, publicationDate) || other.publicationDate == publicationDate)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.procurementTypeCode, procurementTypeCode) || other.procurementTypeCode == procurementTypeCode)&&(identical(other.procedureCode, procedureCode) || other.procedureCode == procedureCode)&&(identical(other.estimatedValue, estimatedValue) || other.estimatedValue == estimatedValue)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.tenderDeadline, tenderDeadline) || other.tenderDeadline == tenderDeadline)&&(identical(other.submissionDeadline, submissionDeadline) || other.submissionDeadline == submissionDeadline)&&(identical(other.applicationDeadline, applicationDeadline) || other.applicationDeadline == applicationDeadline)&&(identical(other.submissionUrl, submissionUrl) || other.submissionUrl == submissionUrl)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode)&&(identical(other.durationUnit, durationUnit) || other.durationUnit == durationUnit)&&(identical(other.buyerOrganization, buyerOrganization) || other.buyerOrganization == buyerOrganization)&&const DeepCollectionEquality().equals(other.lots, lots)&&const DeepCollectionEquality().equals(other.officialLanguages, officialLanguages)&&(identical(other.status, status) || other.status == status));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hashAll([runtimeType,success,message,id,tenderId,noticePublicationId,publicationDate,title,description,procurementTypeCode,procedureCode,estimatedValue,currency,duration,tenderDeadline,submissionDeadline,applicationDeadline,submissionUrl,countryCode,durationUnit,buyerOrganization,status]); int get hashCode => Object.hashAll([runtimeType,success,message,id,tenderId,noticePublicationId,noticeTypeCode,formType,noticeLanguageCode,issueDate,issueTime,publicationDate,title,description,procurementTypeCode,procedureCode,estimatedValue,currency,duration,tenderDeadline,submissionDeadline,applicationDeadline,submissionUrl,countryCode,durationUnit,buyerOrganization,const DeepCollectionEquality().hash(lots),const DeepCollectionEquality().hash(officialLanguages),status]);
@override @override
String toString() { String toString() {
return 'TenderData(success: $success, message: $message, id: $id, tenderId: $tenderId, noticePublicationId: $noticePublicationId, publicationDate: $publicationDate, title: $title, description: $description, procurementTypeCode: $procurementTypeCode, procedureCode: $procedureCode, estimatedValue: $estimatedValue, currency: $currency, duration: $duration, tenderDeadline: $tenderDeadline, submissionDeadline: $submissionDeadline, applicationDeadline: $applicationDeadline, submissionUrl: $submissionUrl, countryCode: $countryCode, durationUnit: $durationUnit, buyerOrganization: $buyerOrganization, status: $status)'; return 'TenderData(success: $success, message: $message, id: $id, tenderId: $tenderId, noticePublicationId: $noticePublicationId, noticeTypeCode: $noticeTypeCode, formType: $formType, noticeLanguageCode: $noticeLanguageCode, issueDate: $issueDate, issueTime: $issueTime, publicationDate: $publicationDate, title: $title, description: $description, procurementTypeCode: $procurementTypeCode, procedureCode: $procedureCode, estimatedValue: $estimatedValue, currency: $currency, duration: $duration, tenderDeadline: $tenderDeadline, submissionDeadline: $submissionDeadline, applicationDeadline: $applicationDeadline, submissionUrl: $submissionUrl, countryCode: $countryCode, durationUnit: $durationUnit, buyerOrganization: $buyerOrganization, lots: $lots, officialLanguages: $officialLanguages, status: $status)';
} }
@@ -48,7 +48,7 @@ abstract mixin class $TenderDataCopyWith<$Res> {
factory $TenderDataCopyWith(TenderData value, $Res Function(TenderData) _then) = _$TenderDataCopyWithImpl; factory $TenderDataCopyWith(TenderData value, $Res Function(TenderData) _then) = _$TenderDataCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
bool? success, String? message, String? id,@JsonKey(name: 'tender_id') String? tenderId,@JsonKey(name: 'notice_publication_id') String? noticePublicationId,@JsonKey(name: 'publication_date') int? publicationDate, String? title, String? description,@JsonKey(name: 'procurement_type_code') String? procurementTypeCode,@JsonKey(name: 'procedure_code') String? procedureCode,@JsonKey(name: 'estimated_value') int? estimatedValue, String? currency, String? duration,@JsonKey(name: 'tender_deadline') int? tenderDeadline,@JsonKey(name: 'submission_deadline') int? submissionDeadline,@JsonKey(name: 'application_deadline') int? applicationDeadline,@JsonKey(name: 'submission_url') String? submissionUrl,@JsonKey(name: 'country_code') String? countryCode,@JsonKey(name: 'duration_unit') String? durationUnit,@JsonKey(name: 'buyer_organization') Organization? buyerOrganization, String? status bool? success, String? message, String? id,@JsonKey(name: 'tender_id') String? tenderId,@JsonKey(name: 'notice_publication_id') String? noticePublicationId,@JsonKey(name: 'notice_type_code') String? noticeTypeCode,@JsonKey(name: 'form_type') String? formType,@JsonKey(name: 'notice_language_code') String? noticeLanguageCode,@JsonKey(name: 'issue_date', fromJson: unixTimestampFromJson) int? issueDate,@JsonKey(name: 'issue_time', fromJson: unixTimestampFromJson) int? issueTime,@JsonKey(name: 'publication_date', fromJson: unixTimestampFromJson) int? publicationDate, String? title, String? description,@JsonKey(name: 'procurement_type_code') String? procurementTypeCode,@JsonKey(name: 'procedure_code') String? procedureCode,@JsonKey(name: 'estimated_value') int? estimatedValue, String? currency, String? duration,@JsonKey(name: 'tender_deadline', fromJson: unixTimestampFromJson) int? tenderDeadline,@JsonKey(name: 'submission_deadline', fromJson: unixTimestampFromJson) int? submissionDeadline,@JsonKey(name: 'application_deadline', fromJson: unixTimestampFromJson) int? applicationDeadline,@JsonKey(name: 'submission_url') String? submissionUrl,@JsonKey(name: 'country_code') String? countryCode,@JsonKey(name: 'duration_unit') String? durationUnit,@JsonKey(name: 'buyer_organization') Organization? buyerOrganization, List<Lot>? lots,@JsonKey(name: 'official_languages') List<String>? officialLanguages, String? status
}); });
@@ -65,14 +65,19 @@ class _$TenderDataCopyWithImpl<$Res>
/// Create a copy of TenderData /// Create a copy of TenderData
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? success = freezed,Object? message = freezed,Object? id = freezed,Object? tenderId = freezed,Object? noticePublicationId = freezed,Object? publicationDate = freezed,Object? title = freezed,Object? description = freezed,Object? procurementTypeCode = freezed,Object? procedureCode = freezed,Object? estimatedValue = freezed,Object? currency = freezed,Object? duration = freezed,Object? tenderDeadline = freezed,Object? submissionDeadline = freezed,Object? applicationDeadline = freezed,Object? submissionUrl = freezed,Object? countryCode = freezed,Object? durationUnit = freezed,Object? buyerOrganization = freezed,Object? status = freezed,}) { @pragma('vm:prefer-inline') @override $Res call({Object? success = freezed,Object? message = freezed,Object? id = freezed,Object? tenderId = freezed,Object? noticePublicationId = freezed,Object? noticeTypeCode = freezed,Object? formType = freezed,Object? noticeLanguageCode = freezed,Object? issueDate = freezed,Object? issueTime = freezed,Object? publicationDate = freezed,Object? title = freezed,Object? description = freezed,Object? procurementTypeCode = freezed,Object? procedureCode = freezed,Object? estimatedValue = freezed,Object? currency = freezed,Object? duration = freezed,Object? tenderDeadline = freezed,Object? submissionDeadline = freezed,Object? applicationDeadline = freezed,Object? submissionUrl = freezed,Object? countryCode = freezed,Object? durationUnit = freezed,Object? buyerOrganization = freezed,Object? lots = freezed,Object? officialLanguages = freezed,Object? status = freezed,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable
as bool?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable as bool?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String?,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String?,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String?,tenderId: freezed == tenderId ? _self.tenderId : tenderId // ignore: cast_nullable_to_non_nullable as String?,tenderId: freezed == tenderId ? _self.tenderId : tenderId // ignore: cast_nullable_to_non_nullable
as String?,noticePublicationId: freezed == noticePublicationId ? _self.noticePublicationId : noticePublicationId // ignore: cast_nullable_to_non_nullable as String?,noticePublicationId: freezed == noticePublicationId ? _self.noticePublicationId : noticePublicationId // ignore: cast_nullable_to_non_nullable
as String?,publicationDate: freezed == publicationDate ? _self.publicationDate : publicationDate // ignore: cast_nullable_to_non_nullable as String?,noticeTypeCode: freezed == noticeTypeCode ? _self.noticeTypeCode : noticeTypeCode // ignore: cast_nullable_to_non_nullable
as String?,formType: freezed == formType ? _self.formType : formType // ignore: cast_nullable_to_non_nullable
as String?,noticeLanguageCode: freezed == noticeLanguageCode ? _self.noticeLanguageCode : noticeLanguageCode // ignore: cast_nullable_to_non_nullable
as String?,issueDate: freezed == issueDate ? _self.issueDate : issueDate // ignore: cast_nullable_to_non_nullable
as int?,issueTime: freezed == issueTime ? _self.issueTime : issueTime // ignore: cast_nullable_to_non_nullable
as int?,publicationDate: freezed == publicationDate ? _self.publicationDate : publicationDate // ignore: cast_nullable_to_non_nullable
as int?,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable as int?,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,procurementTypeCode: freezed == procurementTypeCode ? _self.procurementTypeCode : procurementTypeCode // ignore: cast_nullable_to_non_nullable as String?,procurementTypeCode: freezed == procurementTypeCode ? _self.procurementTypeCode : procurementTypeCode // ignore: cast_nullable_to_non_nullable
@@ -87,7 +92,9 @@ as int?,submissionUrl: freezed == submissionUrl ? _self.submissionUrl : submissi
as String?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable as String?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
as String?,durationUnit: freezed == durationUnit ? _self.durationUnit : durationUnit // ignore: cast_nullable_to_non_nullable as String?,durationUnit: freezed == durationUnit ? _self.durationUnit : durationUnit // ignore: cast_nullable_to_non_nullable
as String?,buyerOrganization: freezed == buyerOrganization ? _self.buyerOrganization : buyerOrganization // ignore: cast_nullable_to_non_nullable as String?,buyerOrganization: freezed == buyerOrganization ? _self.buyerOrganization : buyerOrganization // ignore: cast_nullable_to_non_nullable
as Organization?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable as Organization?,lots: freezed == lots ? _self.lots : lots // ignore: cast_nullable_to_non_nullable
as List<Lot>?,officialLanguages: freezed == officialLanguages ? _self.officialLanguages : officialLanguages // ignore: cast_nullable_to_non_nullable
as List<String>?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as String?, as String?,
)); ));
} }
@@ -185,10 +192,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool? success, String? message, String? id, @JsonKey(name: 'tender_id') String? tenderId, @JsonKey(name: 'notice_publication_id') String? noticePublicationId, @JsonKey(name: 'publication_date') int? publicationDate, String? title, String? description, @JsonKey(name: 'procurement_type_code') String? procurementTypeCode, @JsonKey(name: 'procedure_code') String? procedureCode, @JsonKey(name: 'estimated_value') int? estimatedValue, String? currency, String? duration, @JsonKey(name: 'tender_deadline') int? tenderDeadline, @JsonKey(name: 'submission_deadline') int? submissionDeadline, @JsonKey(name: 'application_deadline') int? applicationDeadline, @JsonKey(name: 'submission_url') String? submissionUrl, @JsonKey(name: 'country_code') String? countryCode, @JsonKey(name: 'duration_unit') String? durationUnit, @JsonKey(name: 'buyer_organization') Organization? buyerOrganization, String? status)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool? success, String? message, String? id, @JsonKey(name: 'tender_id') String? tenderId, @JsonKey(name: 'notice_publication_id') String? noticePublicationId, @JsonKey(name: 'notice_type_code') String? noticeTypeCode, @JsonKey(name: 'form_type') String? formType, @JsonKey(name: 'notice_language_code') String? noticeLanguageCode, @JsonKey(name: 'issue_date', fromJson: unixTimestampFromJson) int? issueDate, @JsonKey(name: 'issue_time', fromJson: unixTimestampFromJson) int? issueTime, @JsonKey(name: 'publication_date', fromJson: unixTimestampFromJson) int? publicationDate, String? title, String? description, @JsonKey(name: 'procurement_type_code') String? procurementTypeCode, @JsonKey(name: 'procedure_code') String? procedureCode, @JsonKey(name: 'estimated_value') int? estimatedValue, String? currency, String? duration, @JsonKey(name: 'tender_deadline', fromJson: unixTimestampFromJson) int? tenderDeadline, @JsonKey(name: 'submission_deadline', fromJson: unixTimestampFromJson) int? submissionDeadline, @JsonKey(name: 'application_deadline', fromJson: unixTimestampFromJson) int? applicationDeadline, @JsonKey(name: 'submission_url') String? submissionUrl, @JsonKey(name: 'country_code') String? countryCode, @JsonKey(name: 'duration_unit') String? durationUnit, @JsonKey(name: 'buyer_organization') Organization? buyerOrganization, List<Lot>? lots, @JsonKey(name: 'official_languages') List<String>? officialLanguages, String? status)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _TenderData() when $default != null: case _TenderData() when $default != null:
return $default(_that.success,_that.message,_that.id,_that.tenderId,_that.noticePublicationId,_that.publicationDate,_that.title,_that.description,_that.procurementTypeCode,_that.procedureCode,_that.estimatedValue,_that.currency,_that.duration,_that.tenderDeadline,_that.submissionDeadline,_that.applicationDeadline,_that.submissionUrl,_that.countryCode,_that.durationUnit,_that.buyerOrganization,_that.status);case _: return $default(_that.success,_that.message,_that.id,_that.tenderId,_that.noticePublicationId,_that.noticeTypeCode,_that.formType,_that.noticeLanguageCode,_that.issueDate,_that.issueTime,_that.publicationDate,_that.title,_that.description,_that.procurementTypeCode,_that.procedureCode,_that.estimatedValue,_that.currency,_that.duration,_that.tenderDeadline,_that.submissionDeadline,_that.applicationDeadline,_that.submissionUrl,_that.countryCode,_that.durationUnit,_that.buyerOrganization,_that.lots,_that.officialLanguages,_that.status);case _:
return orElse(); return orElse();
} }
@@ -206,10 +213,10 @@ return $default(_that.success,_that.message,_that.id,_that.tenderId,_that.notice
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool? success, String? message, String? id, @JsonKey(name: 'tender_id') String? tenderId, @JsonKey(name: 'notice_publication_id') String? noticePublicationId, @JsonKey(name: 'publication_date') int? publicationDate, String? title, String? description, @JsonKey(name: 'procurement_type_code') String? procurementTypeCode, @JsonKey(name: 'procedure_code') String? procedureCode, @JsonKey(name: 'estimated_value') int? estimatedValue, String? currency, String? duration, @JsonKey(name: 'tender_deadline') int? tenderDeadline, @JsonKey(name: 'submission_deadline') int? submissionDeadline, @JsonKey(name: 'application_deadline') int? applicationDeadline, @JsonKey(name: 'submission_url') String? submissionUrl, @JsonKey(name: 'country_code') String? countryCode, @JsonKey(name: 'duration_unit') String? durationUnit, @JsonKey(name: 'buyer_organization') Organization? buyerOrganization, String? status) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool? success, String? message, String? id, @JsonKey(name: 'tender_id') String? tenderId, @JsonKey(name: 'notice_publication_id') String? noticePublicationId, @JsonKey(name: 'notice_type_code') String? noticeTypeCode, @JsonKey(name: 'form_type') String? formType, @JsonKey(name: 'notice_language_code') String? noticeLanguageCode, @JsonKey(name: 'issue_date', fromJson: unixTimestampFromJson) int? issueDate, @JsonKey(name: 'issue_time', fromJson: unixTimestampFromJson) int? issueTime, @JsonKey(name: 'publication_date', fromJson: unixTimestampFromJson) int? publicationDate, String? title, String? description, @JsonKey(name: 'procurement_type_code') String? procurementTypeCode, @JsonKey(name: 'procedure_code') String? procedureCode, @JsonKey(name: 'estimated_value') int? estimatedValue, String? currency, String? duration, @JsonKey(name: 'tender_deadline', fromJson: unixTimestampFromJson) int? tenderDeadline, @JsonKey(name: 'submission_deadline', fromJson: unixTimestampFromJson) int? submissionDeadline, @JsonKey(name: 'application_deadline', fromJson: unixTimestampFromJson) int? applicationDeadline, @JsonKey(name: 'submission_url') String? submissionUrl, @JsonKey(name: 'country_code') String? countryCode, @JsonKey(name: 'duration_unit') String? durationUnit, @JsonKey(name: 'buyer_organization') Organization? buyerOrganization, List<Lot>? lots, @JsonKey(name: 'official_languages') List<String>? officialLanguages, String? status) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _TenderData(): case _TenderData():
return $default(_that.success,_that.message,_that.id,_that.tenderId,_that.noticePublicationId,_that.publicationDate,_that.title,_that.description,_that.procurementTypeCode,_that.procedureCode,_that.estimatedValue,_that.currency,_that.duration,_that.tenderDeadline,_that.submissionDeadline,_that.applicationDeadline,_that.submissionUrl,_that.countryCode,_that.durationUnit,_that.buyerOrganization,_that.status);case _: return $default(_that.success,_that.message,_that.id,_that.tenderId,_that.noticePublicationId,_that.noticeTypeCode,_that.formType,_that.noticeLanguageCode,_that.issueDate,_that.issueTime,_that.publicationDate,_that.title,_that.description,_that.procurementTypeCode,_that.procedureCode,_that.estimatedValue,_that.currency,_that.duration,_that.tenderDeadline,_that.submissionDeadline,_that.applicationDeadline,_that.submissionUrl,_that.countryCode,_that.durationUnit,_that.buyerOrganization,_that.lots,_that.officialLanguages,_that.status);case _:
throw StateError('Unexpected subclass'); throw StateError('Unexpected subclass');
} }
@@ -226,10 +233,10 @@ return $default(_that.success,_that.message,_that.id,_that.tenderId,_that.notice
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool? success, String? message, String? id, @JsonKey(name: 'tender_id') String? tenderId, @JsonKey(name: 'notice_publication_id') String? noticePublicationId, @JsonKey(name: 'publication_date') int? publicationDate, String? title, String? description, @JsonKey(name: 'procurement_type_code') String? procurementTypeCode, @JsonKey(name: 'procedure_code') String? procedureCode, @JsonKey(name: 'estimated_value') int? estimatedValue, String? currency, String? duration, @JsonKey(name: 'tender_deadline') int? tenderDeadline, @JsonKey(name: 'submission_deadline') int? submissionDeadline, @JsonKey(name: 'application_deadline') int? applicationDeadline, @JsonKey(name: 'submission_url') String? submissionUrl, @JsonKey(name: 'country_code') String? countryCode, @JsonKey(name: 'duration_unit') String? durationUnit, @JsonKey(name: 'buyer_organization') Organization? buyerOrganization, String? status)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool? success, String? message, String? id, @JsonKey(name: 'tender_id') String? tenderId, @JsonKey(name: 'notice_publication_id') String? noticePublicationId, @JsonKey(name: 'notice_type_code') String? noticeTypeCode, @JsonKey(name: 'form_type') String? formType, @JsonKey(name: 'notice_language_code') String? noticeLanguageCode, @JsonKey(name: 'issue_date', fromJson: unixTimestampFromJson) int? issueDate, @JsonKey(name: 'issue_time', fromJson: unixTimestampFromJson) int? issueTime, @JsonKey(name: 'publication_date', fromJson: unixTimestampFromJson) int? publicationDate, String? title, String? description, @JsonKey(name: 'procurement_type_code') String? procurementTypeCode, @JsonKey(name: 'procedure_code') String? procedureCode, @JsonKey(name: 'estimated_value') int? estimatedValue, String? currency, String? duration, @JsonKey(name: 'tender_deadline', fromJson: unixTimestampFromJson) int? tenderDeadline, @JsonKey(name: 'submission_deadline', fromJson: unixTimestampFromJson) int? submissionDeadline, @JsonKey(name: 'application_deadline', fromJson: unixTimestampFromJson) int? applicationDeadline, @JsonKey(name: 'submission_url') String? submissionUrl, @JsonKey(name: 'country_code') String? countryCode, @JsonKey(name: 'duration_unit') String? durationUnit, @JsonKey(name: 'buyer_organization') Organization? buyerOrganization, List<Lot>? lots, @JsonKey(name: 'official_languages') List<String>? officialLanguages, String? status)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _TenderData() when $default != null: case _TenderData() when $default != null:
return $default(_that.success,_that.message,_that.id,_that.tenderId,_that.noticePublicationId,_that.publicationDate,_that.title,_that.description,_that.procurementTypeCode,_that.procedureCode,_that.estimatedValue,_that.currency,_that.duration,_that.tenderDeadline,_that.submissionDeadline,_that.applicationDeadline,_that.submissionUrl,_that.countryCode,_that.durationUnit,_that.buyerOrganization,_that.status);case _: return $default(_that.success,_that.message,_that.id,_that.tenderId,_that.noticePublicationId,_that.noticeTypeCode,_that.formType,_that.noticeLanguageCode,_that.issueDate,_that.issueTime,_that.publicationDate,_that.title,_that.description,_that.procurementTypeCode,_that.procedureCode,_that.estimatedValue,_that.currency,_that.duration,_that.tenderDeadline,_that.submissionDeadline,_that.applicationDeadline,_that.submissionUrl,_that.countryCode,_that.durationUnit,_that.buyerOrganization,_that.lots,_that.officialLanguages,_that.status);case _:
return null; return null;
} }
@@ -241,7 +248,7 @@ return $default(_that.success,_that.message,_that.id,_that.tenderId,_that.notice
@JsonSerializable() @JsonSerializable()
class _TenderData implements TenderData { class _TenderData implements TenderData {
const _TenderData({required this.success, required this.message, required this.id, @JsonKey(name: 'tender_id') required this.tenderId, @JsonKey(name: 'notice_publication_id') required this.noticePublicationId, @JsonKey(name: 'publication_date') required this.publicationDate, required this.title, required this.description, @JsonKey(name: 'procurement_type_code') required this.procurementTypeCode, @JsonKey(name: 'procedure_code') required this.procedureCode, @JsonKey(name: 'estimated_value') required this.estimatedValue, required this.currency, required this.duration, @JsonKey(name: 'tender_deadline') required this.tenderDeadline, @JsonKey(name: 'submission_deadline') required this.submissionDeadline, @JsonKey(name: 'application_deadline') required this.applicationDeadline, @JsonKey(name: 'submission_url') required this.submissionUrl, @JsonKey(name: 'country_code') required this.countryCode, @JsonKey(name: 'duration_unit') required this.durationUnit, @JsonKey(name: 'buyer_organization') required this.buyerOrganization, required this.status}); const _TenderData({required this.success, required this.message, required this.id, @JsonKey(name: 'tender_id') required this.tenderId, @JsonKey(name: 'notice_publication_id') required this.noticePublicationId, @JsonKey(name: 'notice_type_code') required this.noticeTypeCode, @JsonKey(name: 'form_type') required this.formType, @JsonKey(name: 'notice_language_code') required this.noticeLanguageCode, @JsonKey(name: 'issue_date', fromJson: unixTimestampFromJson) required this.issueDate, @JsonKey(name: 'issue_time', fromJson: unixTimestampFromJson) required this.issueTime, @JsonKey(name: 'publication_date', fromJson: unixTimestampFromJson) required this.publicationDate, required this.title, required this.description, @JsonKey(name: 'procurement_type_code') required this.procurementTypeCode, @JsonKey(name: 'procedure_code') required this.procedureCode, @JsonKey(name: 'estimated_value') required this.estimatedValue, required this.currency, required this.duration, @JsonKey(name: 'tender_deadline', fromJson: unixTimestampFromJson) required this.tenderDeadline, @JsonKey(name: 'submission_deadline', fromJson: unixTimestampFromJson) required this.submissionDeadline, @JsonKey(name: 'application_deadline', fromJson: unixTimestampFromJson) required this.applicationDeadline, @JsonKey(name: 'submission_url') required this.submissionUrl, @JsonKey(name: 'country_code') required this.countryCode, @JsonKey(name: 'duration_unit') required this.durationUnit, @JsonKey(name: 'buyer_organization') required this.buyerOrganization, required final List<Lot>? lots, @JsonKey(name: 'official_languages') required final List<String>? officialLanguages, required this.status}): _lots = lots,_officialLanguages = officialLanguages;
factory _TenderData.fromJson(Map<String, dynamic> json) => _$TenderDataFromJson(json); factory _TenderData.fromJson(Map<String, dynamic> json) => _$TenderDataFromJson(json);
@override final bool? success; @override final bool? success;
@@ -249,7 +256,12 @@ class _TenderData implements TenderData {
@override final String? id; @override final String? id;
@override@JsonKey(name: 'tender_id') final String? tenderId; @override@JsonKey(name: 'tender_id') final String? tenderId;
@override@JsonKey(name: 'notice_publication_id') final String? noticePublicationId; @override@JsonKey(name: 'notice_publication_id') final String? noticePublicationId;
@override@JsonKey(name: 'publication_date') final int? publicationDate; @override@JsonKey(name: 'notice_type_code') final String? noticeTypeCode;
@override@JsonKey(name: 'form_type') final String? formType;
@override@JsonKey(name: 'notice_language_code') final String? noticeLanguageCode;
@override@JsonKey(name: 'issue_date', fromJson: unixTimestampFromJson) final int? issueDate;
@override@JsonKey(name: 'issue_time', fromJson: unixTimestampFromJson) final int? issueTime;
@override@JsonKey(name: 'publication_date', fromJson: unixTimestampFromJson) final int? publicationDate;
@override final String? title; @override final String? title;
@override final String? description; @override final String? description;
@override@JsonKey(name: 'procurement_type_code') final String? procurementTypeCode; @override@JsonKey(name: 'procurement_type_code') final String? procurementTypeCode;
@@ -257,13 +269,31 @@ class _TenderData implements TenderData {
@override@JsonKey(name: 'estimated_value') final int? estimatedValue; @override@JsonKey(name: 'estimated_value') final int? estimatedValue;
@override final String? currency; @override final String? currency;
@override final String? duration; @override final String? duration;
@override@JsonKey(name: 'tender_deadline') final int? tenderDeadline; @override@JsonKey(name: 'tender_deadline', fromJson: unixTimestampFromJson) final int? tenderDeadline;
@override@JsonKey(name: 'submission_deadline') final int? submissionDeadline; @override@JsonKey(name: 'submission_deadline', fromJson: unixTimestampFromJson) final int? submissionDeadline;
@override@JsonKey(name: 'application_deadline') final int? applicationDeadline; @override@JsonKey(name: 'application_deadline', fromJson: unixTimestampFromJson) final int? applicationDeadline;
@override@JsonKey(name: 'submission_url') final String? submissionUrl; @override@JsonKey(name: 'submission_url') final String? submissionUrl;
@override@JsonKey(name: 'country_code') final String? countryCode; @override@JsonKey(name: 'country_code') final String? countryCode;
@override@JsonKey(name: 'duration_unit') final String? durationUnit; @override@JsonKey(name: 'duration_unit') final String? durationUnit;
@override@JsonKey(name: 'buyer_organization') final Organization? buyerOrganization; @override@JsonKey(name: 'buyer_organization') final Organization? buyerOrganization;
final List<Lot>? _lots;
@override List<Lot>? get lots {
final value = _lots;
if (value == null) return null;
if (_lots is EqualUnmodifiableListView) return _lots;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
final List<String>? _officialLanguages;
@override@JsonKey(name: 'official_languages') List<String>? get officialLanguages {
final value = _officialLanguages;
if (value == null) return null;
if (_officialLanguages is EqualUnmodifiableListView) return _officialLanguages;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override final String? status; @override final String? status;
/// Create a copy of TenderData /// Create a copy of TenderData
@@ -279,16 +309,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _TenderData&&(identical(other.success, success) || other.success == success)&&(identical(other.message, message) || other.message == message)&&(identical(other.id, id) || other.id == id)&&(identical(other.tenderId, tenderId) || other.tenderId == tenderId)&&(identical(other.noticePublicationId, noticePublicationId) || other.noticePublicationId == noticePublicationId)&&(identical(other.publicationDate, publicationDate) || other.publicationDate == publicationDate)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.procurementTypeCode, procurementTypeCode) || other.procurementTypeCode == procurementTypeCode)&&(identical(other.procedureCode, procedureCode) || other.procedureCode == procedureCode)&&(identical(other.estimatedValue, estimatedValue) || other.estimatedValue == estimatedValue)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.tenderDeadline, tenderDeadline) || other.tenderDeadline == tenderDeadline)&&(identical(other.submissionDeadline, submissionDeadline) || other.submissionDeadline == submissionDeadline)&&(identical(other.applicationDeadline, applicationDeadline) || other.applicationDeadline == applicationDeadline)&&(identical(other.submissionUrl, submissionUrl) || other.submissionUrl == submissionUrl)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode)&&(identical(other.durationUnit, durationUnit) || other.durationUnit == durationUnit)&&(identical(other.buyerOrganization, buyerOrganization) || other.buyerOrganization == buyerOrganization)&&(identical(other.status, status) || other.status == status)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _TenderData&&(identical(other.success, success) || other.success == success)&&(identical(other.message, message) || other.message == message)&&(identical(other.id, id) || other.id == id)&&(identical(other.tenderId, tenderId) || other.tenderId == tenderId)&&(identical(other.noticePublicationId, noticePublicationId) || other.noticePublicationId == noticePublicationId)&&(identical(other.noticeTypeCode, noticeTypeCode) || other.noticeTypeCode == noticeTypeCode)&&(identical(other.formType, formType) || other.formType == formType)&&(identical(other.noticeLanguageCode, noticeLanguageCode) || other.noticeLanguageCode == noticeLanguageCode)&&(identical(other.issueDate, issueDate) || other.issueDate == issueDate)&&(identical(other.issueTime, issueTime) || other.issueTime == issueTime)&&(identical(other.publicationDate, publicationDate) || other.publicationDate == publicationDate)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.procurementTypeCode, procurementTypeCode) || other.procurementTypeCode == procurementTypeCode)&&(identical(other.procedureCode, procedureCode) || other.procedureCode == procedureCode)&&(identical(other.estimatedValue, estimatedValue) || other.estimatedValue == estimatedValue)&&(identical(other.currency, currency) || other.currency == currency)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.tenderDeadline, tenderDeadline) || other.tenderDeadline == tenderDeadline)&&(identical(other.submissionDeadline, submissionDeadline) || other.submissionDeadline == submissionDeadline)&&(identical(other.applicationDeadline, applicationDeadline) || other.applicationDeadline == applicationDeadline)&&(identical(other.submissionUrl, submissionUrl) || other.submissionUrl == submissionUrl)&&(identical(other.countryCode, countryCode) || other.countryCode == countryCode)&&(identical(other.durationUnit, durationUnit) || other.durationUnit == durationUnit)&&(identical(other.buyerOrganization, buyerOrganization) || other.buyerOrganization == buyerOrganization)&&const DeepCollectionEquality().equals(other._lots, _lots)&&const DeepCollectionEquality().equals(other._officialLanguages, _officialLanguages)&&(identical(other.status, status) || other.status == status));
} }
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hashAll([runtimeType,success,message,id,tenderId,noticePublicationId,publicationDate,title,description,procurementTypeCode,procedureCode,estimatedValue,currency,duration,tenderDeadline,submissionDeadline,applicationDeadline,submissionUrl,countryCode,durationUnit,buyerOrganization,status]); int get hashCode => Object.hashAll([runtimeType,success,message,id,tenderId,noticePublicationId,noticeTypeCode,formType,noticeLanguageCode,issueDate,issueTime,publicationDate,title,description,procurementTypeCode,procedureCode,estimatedValue,currency,duration,tenderDeadline,submissionDeadline,applicationDeadline,submissionUrl,countryCode,durationUnit,buyerOrganization,const DeepCollectionEquality().hash(_lots),const DeepCollectionEquality().hash(_officialLanguages),status]);
@override @override
String toString() { String toString() {
return 'TenderData(success: $success, message: $message, id: $id, tenderId: $tenderId, noticePublicationId: $noticePublicationId, publicationDate: $publicationDate, title: $title, description: $description, procurementTypeCode: $procurementTypeCode, procedureCode: $procedureCode, estimatedValue: $estimatedValue, currency: $currency, duration: $duration, tenderDeadline: $tenderDeadline, submissionDeadline: $submissionDeadline, applicationDeadline: $applicationDeadline, submissionUrl: $submissionUrl, countryCode: $countryCode, durationUnit: $durationUnit, buyerOrganization: $buyerOrganization, status: $status)'; return 'TenderData(success: $success, message: $message, id: $id, tenderId: $tenderId, noticePublicationId: $noticePublicationId, noticeTypeCode: $noticeTypeCode, formType: $formType, noticeLanguageCode: $noticeLanguageCode, issueDate: $issueDate, issueTime: $issueTime, publicationDate: $publicationDate, title: $title, description: $description, procurementTypeCode: $procurementTypeCode, procedureCode: $procedureCode, estimatedValue: $estimatedValue, currency: $currency, duration: $duration, tenderDeadline: $tenderDeadline, submissionDeadline: $submissionDeadline, applicationDeadline: $applicationDeadline, submissionUrl: $submissionUrl, countryCode: $countryCode, durationUnit: $durationUnit, buyerOrganization: $buyerOrganization, lots: $lots, officialLanguages: $officialLanguages, status: $status)';
} }
@@ -299,7 +329,7 @@ abstract mixin class _$TenderDataCopyWith<$Res> implements $TenderDataCopyWith<$
factory _$TenderDataCopyWith(_TenderData value, $Res Function(_TenderData) _then) = __$TenderDataCopyWithImpl; factory _$TenderDataCopyWith(_TenderData value, $Res Function(_TenderData) _then) = __$TenderDataCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
bool? success, String? message, String? id,@JsonKey(name: 'tender_id') String? tenderId,@JsonKey(name: 'notice_publication_id') String? noticePublicationId,@JsonKey(name: 'publication_date') int? publicationDate, String? title, String? description,@JsonKey(name: 'procurement_type_code') String? procurementTypeCode,@JsonKey(name: 'procedure_code') String? procedureCode,@JsonKey(name: 'estimated_value') int? estimatedValue, String? currency, String? duration,@JsonKey(name: 'tender_deadline') int? tenderDeadline,@JsonKey(name: 'submission_deadline') int? submissionDeadline,@JsonKey(name: 'application_deadline') int? applicationDeadline,@JsonKey(name: 'submission_url') String? submissionUrl,@JsonKey(name: 'country_code') String? countryCode,@JsonKey(name: 'duration_unit') String? durationUnit,@JsonKey(name: 'buyer_organization') Organization? buyerOrganization, String? status bool? success, String? message, String? id,@JsonKey(name: 'tender_id') String? tenderId,@JsonKey(name: 'notice_publication_id') String? noticePublicationId,@JsonKey(name: 'notice_type_code') String? noticeTypeCode,@JsonKey(name: 'form_type') String? formType,@JsonKey(name: 'notice_language_code') String? noticeLanguageCode,@JsonKey(name: 'issue_date', fromJson: unixTimestampFromJson) int? issueDate,@JsonKey(name: 'issue_time', fromJson: unixTimestampFromJson) int? issueTime,@JsonKey(name: 'publication_date', fromJson: unixTimestampFromJson) int? publicationDate, String? title, String? description,@JsonKey(name: 'procurement_type_code') String? procurementTypeCode,@JsonKey(name: 'procedure_code') String? procedureCode,@JsonKey(name: 'estimated_value') int? estimatedValue, String? currency, String? duration,@JsonKey(name: 'tender_deadline', fromJson: unixTimestampFromJson) int? tenderDeadline,@JsonKey(name: 'submission_deadline', fromJson: unixTimestampFromJson) int? submissionDeadline,@JsonKey(name: 'application_deadline', fromJson: unixTimestampFromJson) int? applicationDeadline,@JsonKey(name: 'submission_url') String? submissionUrl,@JsonKey(name: 'country_code') String? countryCode,@JsonKey(name: 'duration_unit') String? durationUnit,@JsonKey(name: 'buyer_organization') Organization? buyerOrganization, List<Lot>? lots,@JsonKey(name: 'official_languages') List<String>? officialLanguages, String? status
}); });
@@ -316,14 +346,19 @@ class __$TenderDataCopyWithImpl<$Res>
/// Create a copy of TenderData /// Create a copy of TenderData
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? success = freezed,Object? message = freezed,Object? id = freezed,Object? tenderId = freezed,Object? noticePublicationId = freezed,Object? publicationDate = freezed,Object? title = freezed,Object? description = freezed,Object? procurementTypeCode = freezed,Object? procedureCode = freezed,Object? estimatedValue = freezed,Object? currency = freezed,Object? duration = freezed,Object? tenderDeadline = freezed,Object? submissionDeadline = freezed,Object? applicationDeadline = freezed,Object? submissionUrl = freezed,Object? countryCode = freezed,Object? durationUnit = freezed,Object? buyerOrganization = freezed,Object? status = freezed,}) { @override @pragma('vm:prefer-inline') $Res call({Object? success = freezed,Object? message = freezed,Object? id = freezed,Object? tenderId = freezed,Object? noticePublicationId = freezed,Object? noticeTypeCode = freezed,Object? formType = freezed,Object? noticeLanguageCode = freezed,Object? issueDate = freezed,Object? issueTime = freezed,Object? publicationDate = freezed,Object? title = freezed,Object? description = freezed,Object? procurementTypeCode = freezed,Object? procedureCode = freezed,Object? estimatedValue = freezed,Object? currency = freezed,Object? duration = freezed,Object? tenderDeadline = freezed,Object? submissionDeadline = freezed,Object? applicationDeadline = freezed,Object? submissionUrl = freezed,Object? countryCode = freezed,Object? durationUnit = freezed,Object? buyerOrganization = freezed,Object? lots = freezed,Object? officialLanguages = freezed,Object? status = freezed,}) {
return _then(_TenderData( return _then(_TenderData(
success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable
as bool?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable as bool?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String?,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as String?,id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String?,tenderId: freezed == tenderId ? _self.tenderId : tenderId // ignore: cast_nullable_to_non_nullable as String?,tenderId: freezed == tenderId ? _self.tenderId : tenderId // ignore: cast_nullable_to_non_nullable
as String?,noticePublicationId: freezed == noticePublicationId ? _self.noticePublicationId : noticePublicationId // ignore: cast_nullable_to_non_nullable as String?,noticePublicationId: freezed == noticePublicationId ? _self.noticePublicationId : noticePublicationId // ignore: cast_nullable_to_non_nullable
as String?,publicationDate: freezed == publicationDate ? _self.publicationDate : publicationDate // ignore: cast_nullable_to_non_nullable as String?,noticeTypeCode: freezed == noticeTypeCode ? _self.noticeTypeCode : noticeTypeCode // ignore: cast_nullable_to_non_nullable
as String?,formType: freezed == formType ? _self.formType : formType // ignore: cast_nullable_to_non_nullable
as String?,noticeLanguageCode: freezed == noticeLanguageCode ? _self.noticeLanguageCode : noticeLanguageCode // ignore: cast_nullable_to_non_nullable
as String?,issueDate: freezed == issueDate ? _self.issueDate : issueDate // ignore: cast_nullable_to_non_nullable
as int?,issueTime: freezed == issueTime ? _self.issueTime : issueTime // ignore: cast_nullable_to_non_nullable
as int?,publicationDate: freezed == publicationDate ? _self.publicationDate : publicationDate // ignore: cast_nullable_to_non_nullable
as int?,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable as int?,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,procurementTypeCode: freezed == procurementTypeCode ? _self.procurementTypeCode : procurementTypeCode // ignore: cast_nullable_to_non_nullable as String?,procurementTypeCode: freezed == procurementTypeCode ? _self.procurementTypeCode : procurementTypeCode // ignore: cast_nullable_to_non_nullable
@@ -338,7 +373,9 @@ as int?,submissionUrl: freezed == submissionUrl ? _self.submissionUrl : submissi
as String?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable as String?,countryCode: freezed == countryCode ? _self.countryCode : countryCode // ignore: cast_nullable_to_non_nullable
as String?,durationUnit: freezed == durationUnit ? _self.durationUnit : durationUnit // ignore: cast_nullable_to_non_nullable as String?,durationUnit: freezed == durationUnit ? _self.durationUnit : durationUnit // ignore: cast_nullable_to_non_nullable
as String?,buyerOrganization: freezed == buyerOrganization ? _self.buyerOrganization : buyerOrganization // ignore: cast_nullable_to_non_nullable as String?,buyerOrganization: freezed == buyerOrganization ? _self.buyerOrganization : buyerOrganization // ignore: cast_nullable_to_non_nullable
as Organization?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable as Organization?,lots: freezed == lots ? _self._lots : lots // ignore: cast_nullable_to_non_nullable
as List<Lot>?,officialLanguages: freezed == officialLanguages ? _self._officialLanguages : officialLanguages // ignore: cast_nullable_to_non_nullable
as List<String>?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as String?, as String?,
)); ));
} }
@@ -12,6 +12,11 @@ _TenderData _$TenderDataFromJson(Map<String, dynamic> json) => _TenderData(
id: json['id'] as String?, id: json['id'] as String?,
tenderId: json['tender_id'] as String?, tenderId: json['tender_id'] as String?,
noticePublicationId: json['notice_publication_id'] as String?, noticePublicationId: json['notice_publication_id'] as String?,
noticeTypeCode: json['notice_type_code'] as String?,
formType: json['form_type'] as String?,
noticeLanguageCode: json['notice_language_code'] as String?,
issueDate: unixTimestampFromJson(json['issue_date']),
issueTime: unixTimestampFromJson(json['issue_time']),
publicationDate: unixTimestampFromJson(json['publication_date']), publicationDate: unixTimestampFromJson(json['publication_date']),
title: json['title'] as String?, title: json['title'] as String?,
description: json['description'] as String?, description: json['description'] as String?,
@@ -32,6 +37,14 @@ _TenderData _$TenderDataFromJson(Map<String, dynamic> json) => _TenderData(
: Organization.fromJson( : Organization.fromJson(
json['buyer_organization'] as Map<String, dynamic>, json['buyer_organization'] as Map<String, dynamic>,
), ),
lots:
(json['lots'] as List<dynamic>?)
?.map((e) => Lot.fromJson(e as Map<String, dynamic>))
.toList(),
officialLanguages:
(json['official_languages'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
status: json['status'] as String?, status: json['status'] as String?,
); );
@@ -42,6 +55,11 @@ Map<String, dynamic> _$TenderDataToJson(_TenderData instance) =>
'id': instance.id, 'id': instance.id,
'tender_id': instance.tenderId, 'tender_id': instance.tenderId,
'notice_publication_id': instance.noticePublicationId, 'notice_publication_id': instance.noticePublicationId,
'notice_type_code': instance.noticeTypeCode,
'form_type': instance.formType,
'notice_language_code': instance.noticeLanguageCode,
'issue_date': instance.issueDate,
'issue_time': instance.issueTime,
'publication_date': instance.publicationDate, 'publication_date': instance.publicationDate,
'title': instance.title, 'title': instance.title,
'description': instance.description, 'description': instance.description,
@@ -57,5 +75,7 @@ Map<String, dynamic> _$TenderDataToJson(_TenderData instance) =>
'country_code': instance.countryCode, 'country_code': instance.countryCode,
'duration_unit': instance.durationUnit, 'duration_unit': instance.durationUnit,
'buyer_organization': instance.buyerOrganization, 'buyer_organization': instance.buyerOrganization,
'lots': instance.lots,
'official_languages': instance.officialLanguages,
'status': instance.status, 'status': instance.status,
}; };
@@ -0,0 +1,235 @@
import 'package:tm_app/core/utils/date_utils.dart';
import 'package:tm_app/data/services/model/tender_data/tender_data.dart';
class TenderSubmissionResponse {
const TenderSubmissionResponse({
required this.success,
required this.message,
required this.data,
});
final bool success;
final String? message;
final TenderSubmission? data;
factory TenderSubmissionResponse.fromJson(Map<String, dynamic> json) {
final data = json['data'];
return TenderSubmissionResponse(
success: json['success'] == true,
message: json['message'] as String?,
data:
data is Map<String, dynamic> ? TenderSubmission.fromJson(data) : null,
);
}
}
class TenderSubmissionsResponse {
const TenderSubmissionsResponse({
required this.success,
required this.message,
required this.data,
required this.meta,
});
final bool success;
final String? message;
final List<TenderSubmission> data;
final TenderSubmissionsMeta? meta;
factory TenderSubmissionsResponse.fromJson(Map<String, dynamic> json) {
final items = json['data'];
return TenderSubmissionsResponse(
success: json['success'] == true,
message: json['message'] as String?,
data:
items is List
? items
.whereType<Map>()
.map(
(item) => TenderSubmission.fromJson(
Map<String, dynamic>.from(item),
),
)
.toList()
: const [],
meta:
json['meta'] is Map
? TenderSubmissionsMeta.fromJson(
Map<String, dynamic>.from(json['meta'] as Map),
)
: null,
);
}
}
class TenderSubmission {
const TenderSubmission({
required this.id,
required this.tenderId,
required this.companyId,
required this.customerId,
required this.status,
required this.stage,
required this.submissionMode,
required this.statusHistory,
required this.createdAt,
required this.updatedAt,
required this.tender,
});
final String id;
final String tenderId;
final String? companyId;
final String? customerId;
final String status;
final String stage;
final String? submissionMode;
final List<TenderSubmissionHistory> statusHistory;
final int? createdAt;
final int? updatedAt;
final TenderData? tender;
bool get isTerminal =>
status == 'rejected' || status == 'contract' || status == 'not_applied';
factory TenderSubmission.fromJson(Map<String, dynamic> json) {
final history = json['status_history'];
final tenderJson = json['tender'];
return TenderSubmission(
id: json['id']?.toString() ?? '',
tenderId: json['tender_id']?.toString() ?? '',
companyId: json['company_id']?.toString(),
customerId: json['customer_id']?.toString(),
status: json['status']?.toString() ?? 'opportunity',
stage: json['stage']?.toString() ?? 'opportunity',
submissionMode: json['submission_mode']?.toString(),
statusHistory:
history is List
? history
.whereType<Map>()
.map(
(item) => TenderSubmissionHistory.fromJson(
Map<String, dynamic>.from(item),
),
)
.toList()
: const [],
createdAt: unixTimestampFromJson(json['created_at']),
updatedAt: unixTimestampFromJson(json['updated_at']),
tender:
tenderJson is Map
? TenderData.fromJson(Map<String, dynamic>.from(tenderJson))
: null,
);
}
}
class TenderSubmissionHistory {
const TenderSubmissionHistory({
required this.status,
required this.reason,
required this.changedBy,
required this.changedAt,
required this.description,
});
final String status;
final String? reason;
final String? changedBy;
final int? changedAt;
final String? description;
factory TenderSubmissionHistory.fromJson(Map<String, dynamic> json) {
return TenderSubmissionHistory(
status: json['status']?.toString() ?? '',
reason: json['reason']?.toString(),
changedBy: json['changed_by']?.toString(),
changedAt: unixTimestampFromJson(json['changed_at']),
description: json['description']?.toString(),
);
}
}
class TenderSubmissionsMeta {
const TenderSubmissionsMeta({
required this.total,
required this.limit,
required this.offset,
required this.page,
required this.pages,
required this.hasMore,
});
final int total;
final int limit;
final int offset;
final int page;
final int pages;
final bool hasMore;
factory TenderSubmissionsMeta.fromJson(Map<String, dynamic> json) {
int number(String key) => (json[key] as num?)?.toInt() ?? 0;
return TenderSubmissionsMeta(
total: number('total'),
limit: number('limit'),
offset: number('offset'),
page: number('page'),
pages: number('pages'),
hasMore: json['has_more'] == true,
);
}
}
class TenderSubmissionStatsResponse {
const TenderSubmissionStatsResponse({
required this.success,
required this.data,
});
final bool success;
final TenderSubmissionStats? data;
factory TenderSubmissionStatsResponse.fromJson(Map<String, dynamic> json) {
return TenderSubmissionStatsResponse(
success: json['success'] == true,
data:
json['data'] is Map
? TenderSubmissionStats.fromJson(
Map<String, dynamic>.from(json['data'] as Map),
)
: null,
);
}
}
class TenderSubmissionStats {
const TenderSubmissionStats({
required this.companyId,
required this.total,
required this.byStage,
required this.byStatus,
required this.lastUpdated,
});
final String? companyId;
final int total;
final Map<String, int> byStage;
final Map<String, int> byStatus;
final int? lastUpdated;
factory TenderSubmissionStats.fromJson(Map<String, dynamic> json) {
Map<String, int> counts(value) =>
value is Map
? value.map(
(key, value) => MapEntry(key.toString(), (value as num).toInt()),
)
: const {};
return TenderSubmissionStats(
companyId: json['company_id']?.toString(),
total: (json['total'] as num?)?.toInt() ?? 0,
byStage: counts(json['by_stage']),
byStatus: counts(json['by_status']),
lastUpdated: unixTimestampFromJson(json['last_updated']),
);
}
}
@@ -0,0 +1,84 @@
import 'package:tm_app/core/network/network_manager.dart';
import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/services/api/tender_submissions_api.dart';
import 'package:tm_app/data/services/model/tender_submission/tender_submission.dart';
class TenderSubmissionsService {
TenderSubmissionsService({required NetworkManager networkManager})
: _networkManager = networkManager;
final NetworkManager _networkManager;
Future<Result<TenderSubmissionsResponse>> getSubmissions({
List<String>? statuses,
List<String>? stages,
int? createdFrom,
int? createdTo,
int limit = 20,
int offset = 0,
String sortBy = 'updated_at',
String sortOrder = 'desc',
}) {
return _networkManager.makeRequest(
TenderSubmissionsApi.list,
TenderSubmissionsResponse.fromJson,
queryParameters: {
if (statuses?.isNotEmpty == true) 'status': statuses,
if (stages?.isNotEmpty == true) 'stage': stages,
if (createdFrom != null) 'created_from': createdFrom,
if (createdTo != null) 'created_to': createdTo,
'limit': limit,
'offset': offset,
'sort_by': sortBy,
'sort_order': sortOrder,
},
);
}
Future<Result<TenderSubmissionResponse>> getById(String id) =>
_networkManager.makeRequest(
TenderSubmissionsApi.byId(id),
TenderSubmissionResponse.fromJson,
);
Future<Result<TenderSubmissionResponse>> getByTender(String tenderId) =>
_networkManager.makeRequest(
TenderSubmissionsApi.byTender(tenderId),
TenderSubmissionResponse.fromJson,
);
Future<Result<TenderSubmissionResponse>> ensure(
String tenderId, {
String? companyId,
}) => _networkManager.makeRequest(
TenderSubmissionsApi.ensure(tenderId),
TenderSubmissionResponse.fromJson,
method: 'POST',
queryParameters: {
if (companyId?.isNotEmpty == true) 'company_id': companyId,
},
);
Future<Result<TenderSubmissionResponse>> updateStatus({
required String id,
required String status,
String? reason,
String? description,
}) => _networkManager.makeRequest(
TenderSubmissionsApi.status(id),
TenderSubmissionResponse.fromJson,
method: 'PATCH',
data: {
'status': status,
if (reason?.trim().isNotEmpty == true) 'reason': reason!.trim(),
if (description?.trim().isNotEmpty == true)
'description': description!.trim(),
},
);
Future<Result<TenderSubmissionStatsResponse>> getStats() =>
_networkManager.makeRequest(
TenderSubmissionsApi.stats,
TenderSubmissionStatsResponse.fromJson,
);
}
+33
View File
@@ -11,6 +11,38 @@
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class $AssetsFontsGen {
const $AssetsFontsGen();
/// File path: assets/fonts/Roboto-Black.ttf
String get robotoBlack => 'assets/fonts/Roboto-Black.ttf';
/// File path: assets/fonts/Roboto-Bold.ttf
String get robotoBold => 'assets/fonts/Roboto-Bold.ttf';
/// File path: assets/fonts/Roboto-Light.ttf
String get robotoLight => 'assets/fonts/Roboto-Light.ttf';
/// File path: assets/fonts/Roboto-Medium.ttf
String get robotoMedium => 'assets/fonts/Roboto-Medium.ttf';
/// File path: assets/fonts/Roboto-Regular.ttf
String get robotoRegular => 'assets/fonts/Roboto-Regular.ttf';
/// File path: assets/fonts/Roboto-Thin.ttf
String get robotoThin => 'assets/fonts/Roboto-Thin.ttf';
/// List of all assets
List<String> get values => [
robotoBlack,
robotoBold,
robotoLight,
robotoMedium,
robotoRegular,
robotoThin,
];
}
class $AssetsIconsGen { class $AssetsIconsGen {
const $AssetsIconsGen(); const $AssetsIconsGen();
@@ -301,6 +333,7 @@ class $AssetsSvgsGen {
class Assets { class Assets {
const Assets._(); const Assets._();
static const $AssetsFontsGen fonts = $AssetsFontsGen();
static const $AssetsIconsGen icons = $AssetsIconsGen(); static const $AssetsIconsGen icons = $AssetsIconsGen();
static const $AssetsPngsGen pngs = $AssetsPngsGen(); static const $AssetsPngsGen pngs = $AssetsPngsGen();
static const $AssetsSvgsGen svgs = $AssetsSvgsGen(); static const $AssetsSvgsGen svgs = $AssetsSvgsGen();
+16
View File
@@ -0,0 +1,16 @@
// dart format width=80
/// GENERATED CODE - DO NOT MODIFY BY HAND
/// *****************************************************
/// FlutterGen
/// *****************************************************
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: deprecated_member_use,directives_ordering,implicit_dynamic_list_literal,unnecessary_import
class FontFamily {
FontFamily._();
/// Font family: Roboto
static const String roboto = 'Roboto';
}
@@ -0,0 +1,100 @@
import 'package:flutter/foundation.dart';
import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/repositories/company_ai_repository.dart';
import 'package:tm_app/data/repositories/home_repository.dart';
import 'package:tm_app/data/services/model/recommended_tenders_response/recommended_tenders_response.dart';
import 'package:tm_app/data/services/model/tender_data/tender_data.dart';
/// Drives the "Recommended" tab in the tenders section.
///
/// Calls `GET /api/v1/tenders/recommend`. When the result is empty shortly
/// after onboarding it surfaces a "still learning" hint instead of an empty
/// error.
class AiRecommendationsViewModel with ChangeNotifier {
AiRecommendationsViewModel({
required CompanyAiRepository companyAiRepository,
required HomeRepository homeRepository,
}) : _companyAiRepository = companyAiRepository,
_homeRepository = homeRepository;
final CompanyAiRepository _companyAiRepository;
final HomeRepository _homeRepository;
/// How long after onboarding an empty result is treated as "still learning".
static const Duration _stillLearningWindow = Duration(minutes: 30);
static const int _pageSize = 20;
bool _isLoading = false;
String? _errorMessage;
List<TenderData> _recommendedTenders = const [];
bool _stillLearning = false;
bool _hasLoaded = false;
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
List<TenderData> get recommendedTenders => _recommendedTenders;
bool get stillLearning => _stillLearning;
/// True once a load has completed (used to distinguish "no results" from
/// "not loaded yet").
bool get hasLoaded => _hasLoaded;
/// True when there is nothing to show and we're not loading or erroring.
bool get isEmpty =>
!_isLoading &&
_errorMessage == null &&
_recommendedTenders.isEmpty;
/// Loads recommendations if they haven't been loaded yet this session.
Future<void> loadIfNeeded() async {
if (_hasLoaded || _isLoading) {
return;
}
await loadRecommendations();
}
/// Loads recommended tenders for the Recommended tab.
Future<void> loadRecommendations() async {
_isLoading = true;
_errorMessage = null;
_stillLearning = false;
notifyListeners();
final result = await _homeRepository.getHomeRecommendedTenders(
limit: _pageSize,
offset: 0,
);
var loadedSuccessfully = false;
switch (result) {
case Ok<RecommendedTendersResponse>():
_recommendedTenders =
result.value.data?.tenders.whereType<TenderData>().toList() ??
const [];
if (_recommendedTenders.isEmpty) {
_stillLearning = _onboardingWasRecent();
}
loadedSuccessfully = true;
case Error<RecommendedTendersResponse>():
_recommendedTenders = const [];
_errorMessage = result.error.toString();
}
_isLoading = false;
_hasLoaded = loadedSuccessfully;
notifyListeners();
}
/// Pull-to-refresh: always re-fetches.
Future<void> refresh() => loadRecommendations();
bool _onboardingWasRecent() {
final startedAt = _companyAiRepository.lastOnboardingStartedAt();
if (startedAt == null) {
return false;
}
final elapsed = DateTime.now().millisecondsSinceEpoch - startedAt;
return elapsed >= 0 && elapsed <= _stillLearningWindow.inMilliseconds;
}
}
+18 -4
View File
@@ -104,10 +104,24 @@ class BoardViewModel with ChangeNotifier {
newOrder: newOrder, newOrder: newOrder,
); );
if (result is Error<MoveCardResponse>) { switch (result) {
_board = _board!.copyWith(columns: previousColumns); case Ok<MoveCardResponse>():
notifyListeners(); // HTTP 200 alone isn't success: the API can return
} // {"success": false, "message": ...} (e.g. column over-limit,
// permission denied). Only keep the optimistic move when the body
// confirms success; otherwise roll back and surface the message.
if (result.value.success == true) {
return result; return result;
} }
_board = _board!.copyWith(columns: previousColumns);
notifyListeners();
return Result.error(
Exception(result.value.message ?? 'Failed to move card'),
);
case Error<MoveCardResponse>():
_board = _board!.copyWith(columns: previousColumns);
notifyListeners();
return result;
}
}
} }
+33 -7
View File
@@ -4,6 +4,8 @@ import 'package:tm_app/data/services/model/recommended_tenders_response/recommen
import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart'; import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart';
import 'package:tm_app/data/services/model/tender_approvals_stats_response/tender_approvals_stats_response.dart'; import 'package:tm_app/data/services/model/tender_approvals_stats_response/tender_approvals_stats_response.dart';
import 'package:tm_app/data/services/model/tender_data/tender_data.dart'; import 'package:tm_app/data/services/model/tender_data/tender_data.dart';
import 'package:tm_app/data/services/model/feedback_stats_response/feedback_stat_response.dart';
import 'package:tm_app/data/services/model/tender_submission/tender_submission.dart';
import '../data/repositories/home_repository.dart'; import '../data/repositories/home_repository.dart';
import '../data/services/model/home/home_response/home_response_model.dart'; import '../data/services/model/home/home_response/home_response_model.dart';
@@ -44,6 +46,7 @@ class HomeViewModel with ChangeNotifier {
RecommendedTendersResponse? _recommendedTendersResponse; RecommendedTendersResponse? _recommendedTendersResponse;
TenderApprovalsStatsResponse? tenderApprovalsStateResponse; TenderApprovalsStatsResponse? tenderApprovalsStateResponse;
TenderSubmissionStats? tenderSubmissionStats;
int userLikedTendersCount = 0; int userLikedTendersCount = 0;
final List<TenderData> _tenders = []; final List<TenderData> _tenders = [];
final int _pageSize = 10; final int _pageSize = 10;
@@ -71,11 +74,15 @@ class HomeViewModel with ChangeNotifier {
_isRecommendedMode = false; _isRecommendedMode = false;
data = null; data = null;
tenderApprovalsStateResponse = null; tenderApprovalsStateResponse = null;
tenderSubmissionStats = null;
notifyListeners(); notifyListeners();
await getApprovedStates(); await Future.wait([
await getYourTenders(reset: true); getApprovedStates(),
await loadUserLikedTendersCount(); getYourTenders(reset: true),
loadUserLikedTendersCount(),
loadSubmissionStats(),
]);
if (_errorMessage != null) { if (_errorMessage != null) {
_isLoading = false; _isLoading = false;
@@ -199,19 +206,38 @@ class HomeViewModel with ChangeNotifier {
} }
Future<void> loadUserLikedTendersCount() async { Future<void> loadUserLikedTendersCount() async {
final result = await _homeRepository.getUserLikedTendersCount(); final result = await _homeRepository.getCustomerFeedbackStats();
switch (result) { switch (result) {
case Ok<int>(): case Ok<FeedbackStatResponse>():
userLikedTendersCount = result.value; userLikedTendersCount = result.value.data?.totalLikes ?? 0;
break; break;
case Error<int>(): case Error<FeedbackStatResponse>():
userLikedTendersCount = 0; userLikedTendersCount = 0;
break; break;
} }
notifyListeners(); notifyListeners();
} }
Future<void> loadSubmissionStats() async {
final result = await _homeRepository.getSubmissionStats();
switch (result) {
case Ok<TenderSubmissionStatsResponse>():
tenderSubmissionStats = result.value.data;
break;
case Error<TenderSubmissionStatsResponse>():
tenderSubmissionStats = null;
break;
}
notifyListeners();
}
int get approvedTendersCount =>
tenderApprovalsStateResponse?.data?.submittedTenders ?? 0;
int get submittedTendersCount =>
tenderSubmissionStats?.byStage['submitted'] ?? 0;
double get partnershipCount { double get partnershipCount {
return tenderApprovalsStateResponse?.data?.partnershipCount?.toDouble() ?? return tenderApprovalsStateResponse?.data?.partnershipCount?.toDouble() ??
0.0; 0.0;
+3 -3
View File
@@ -87,7 +87,7 @@ class NotificationViewModel with ChangeNotifier {
} }
notifyListeners(); notifyListeners();
final int offset = page * _pageSize; final int offset = (page - 1) * _pageSize;
final result = await _repository.getNotifications( final result = await _repository.getNotifications(
limit: _pageSize, limit: _pageSize,
@@ -138,7 +138,7 @@ class NotificationViewModel with ChangeNotifier {
} }
notifyListeners(); notifyListeners();
final int offset = page * _pageSize; final int offset = (page - 1) * _pageSize;
final result = await _repository.getUnreadNotifications( final result = await _repository.getUnreadNotifications(
limit: _pageSize, limit: _pageSize,
offset: offset, offset: offset,
@@ -181,7 +181,7 @@ class NotificationViewModel with ChangeNotifier {
} }
notifyListeners(); notifyListeners();
final int offset = page * _pageSize; final int offset = (page - 1) * _pageSize;
final result = await _repository.getImportantNotifications( final result = await _repository.getImportantNotifications(
limit: _pageSize, limit: _pageSize,
+24 -1
View File
@@ -1,5 +1,8 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tm_app/data/repositories/auth_repository.dart'; import 'package:tm_app/data/repositories/auth_repository.dart';
import 'package:tm_app/data/repositories/company_ai_repository.dart';
import 'package:tm_app/data/services/model/company_profile_data/company_profile_data.dart'; import 'package:tm_app/data/services/model/company_profile_data/company_profile_data.dart';
import 'package:tm_app/data/services/model/company_profile_response/company_profile_response.dart'; import 'package:tm_app/data/services/model/company_profile_response/company_profile_response.dart';
import 'package:tm_app/data/services/model/profile_data/profile_data.dart'; import 'package:tm_app/data/services/model/profile_data/profile_data.dart';
@@ -12,12 +15,15 @@ import '../data/services/model/profile_response/profile_response.dart';
class ProfileViewModel with ChangeNotifier { class ProfileViewModel with ChangeNotifier {
final ProfileRepository _profileRepository; final ProfileRepository _profileRepository;
final AuthRepository _authRepository; final AuthRepository _authRepository;
final CompanyAiRepository _companyAiRepository;
ProfileViewModel({ ProfileViewModel({
required ProfileRepository profileRepository, required ProfileRepository profileRepository,
required AuthRepository authRepository, required AuthRepository authRepository,
required CompanyAiRepository companyAiRepository,
}) : _profileRepository = profileRepository, }) : _profileRepository = profileRepository,
_authRepository = authRepository; _authRepository = authRepository,
_companyAiRepository = companyAiRepository;
bool _isLoading = false; bool _isLoading = false;
String? _errorMessage; String? _errorMessage;
@@ -62,6 +68,7 @@ class ProfileViewModel with ChangeNotifier {
switch (result) { switch (result) {
case Ok<CompanyProfileResponse>(): case Ok<CompanyProfileResponse>():
_companyProfileData = result.value.data; _companyProfileData = result.value.data;
_maybeStartAiOnboarding(_companyProfileData);
break; break;
case Error<CompanyProfileResponse>(): case Error<CompanyProfileResponse>():
_errorMessage = result.error.toString(); _errorMessage = result.error.toString();
@@ -73,6 +80,22 @@ class ProfileViewModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
/// Best-effort AI onboarding once the company profile is ready. Fire-and-
/// forget: onboarding is asynchronous on the AI side and the fingerprint
/// guard in [CompanyAiRepository] keeps it from re-running unnecessarily, so
/// the profile screen never blocks or surfaces errors for it.
void _maybeStartAiOnboarding(CompanyProfileData? company) {
if (company == null) {
return;
}
unawaited(
_companyAiRepository.maybeStartOnboarding(
documentFileIds: company.documentFileIds ?? const [],
website: company.website,
),
);
}
Future<void> logout() async { Future<void> logout() async {
_errorMessage = null; _errorMessage = null;
notifyListeners(); notifyListeners();
+58 -105
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:tm_app/core/utils/device_utils.dart'; import 'package:tm_app/core/utils/device_utils.dart';
import 'package:tm_app/core/utils/logger.dart'; import 'package:tm_app/core/utils/logger.dart';
import 'package:tm_app/core/network/app_exceptions.dart';
import 'package:tm_app/data/repositories/tenders_repository.dart'; import 'package:tm_app/data/repositories/tenders_repository.dart';
import 'package:tm_app/data/services/model/feedback_data/feedback_data.dart'; import 'package:tm_app/data/services/model/feedback_data/feedback_data.dart';
import 'package:tm_app/data/services/model/feedback_response/feedback_response.dart'; import 'package:tm_app/data/services/model/feedback_response/feedback_response.dart';
@@ -36,45 +37,21 @@ class TendersViewModel with ChangeNotifier {
String? get searchQuery => _searchQuery; String? get searchQuery => _searchQuery;
List<FeedbackData> feedbacks = []; List<FeedbackData> feedbacks = [];
final Set<String> _hydratedFeedbackTenderIds = <String>{};
final Set<String> _feedbackHydrationsInFlight = <String>{};
final Set<String> _feedbackRequestsInFlight = <String>{};
bool isFeedbackLoading(String tenderId) =>
_feedbackRequestsInFlight.contains(tenderId);
static const int _pageSize = 10; static const int _pageSize = 10;
// --- Offset pagination state --- // --- Infinite-scroll offset state ---
int _currentOffset = 0; int _currentOffset = 0;
int _lastValidPages = 1;
int _lastValidLimit = _pageSize;
bool _hasMoreData = true; bool _hasMoreData = true;
bool get hasMoreData => _hasMoreData; bool get hasMoreData => _hasMoreData;
/// 0-based current page index.
int get currentPageIndex {
final meta = _tendersResponse?.meta;
final limit = meta?.limit ?? _lastValidLimit;
if (limit > 0) {
return _currentOffset ~/ limit;
}
if (meta?.offset != null && limit > 0) {
return meta!.offset! ~/ limit;
}
if (meta?.page != null) {
return meta!.page! - 1;
}
return 0;
}
/// 1-based current page (for display).
int get currentPage => currentPageIndex + 1;
int get totalPages {
final pages = _tendersResponse?.meta?.pages;
if (pages != null && pages > 0) {
return pages;
}
return _lastValidPages;
}
// Filter States // Filter States
DateTimeRange? selectedDateRange; DateTimeRange? selectedDateRange;
String selectedStatus = 'All'; String selectedStatus = 'All';
@@ -233,18 +210,11 @@ class TendersViewModel with ChangeNotifier {
case Ok<TendersResponse>(): case Ok<TendersResponse>():
_tendersResponse = result.value; _tendersResponse = result.value;
final meta = _tendersResponse?.meta; final meta = _tendersResponse?.meta;
if (meta?.pages != null && meta!.pages! > 0) {
_lastValidPages = meta.pages!;
}
if (meta?.limit != null && meta!.limit! > 0) {
_lastValidLimit = meta.limit!;
}
if (meta?.offset != null) { if (meta?.offset != null) {
_currentOffset = meta!.offset!; _currentOffset = meta!.offset!;
} }
final tenders = _tendersResponse?.data?.tenders ?? []; final tenders = _tendersResponse?.data?.tenders ?? [];
_hasMoreData = _hasMoreData = meta?.hasMore ?? (tenders.length >= _pageSize);
meta?.hasMore ?? (tenders.length >= _pageSize);
if (tenders.isNotEmpty) { if (tenders.isNotEmpty) {
if (fetchAllFeedbacks) { if (fetchAllFeedbacks) {
for (final tender in tenders) { for (final tender in tenders) {
@@ -299,14 +269,13 @@ class TendersViewModel with ChangeNotifier {
data: result.value.data?.copyWith(tenders: combined), data: result.value.data?.copyWith(tenders: combined),
); );
final newMeta = result.value.meta; final newMeta = result.value.meta;
if (newMeta?.pages != null && newMeta!.pages! > 0) { _hasMoreData = newMeta?.hasMore ?? (newTenders.length >= _pageSize);
_lastValidPages = newMeta.pages!; for (final tender in newTenders) {
final id = tender.id;
if (id != null && !_hydratedFeedbackTenderIds.contains(id)) {
getTenderFeedback(id);
} }
if (newMeta?.limit != null && newMeta!.limit! > 0) {
_lastValidLimit = newMeta.limit!;
} }
_hasMoreData =
newMeta?.hasMore ?? (newTenders.length >= _pageSize);
} else { } else {
_hasMoreData = false; _hasMoreData = false;
} }
@@ -325,14 +294,18 @@ class TendersViewModel with ChangeNotifier {
} }
Future<void> getTenderFeedback(String tenderId) async { Future<void> getTenderFeedback(String tenderId) async {
_errorMessage = null; if (_hydratedFeedbackTenderIds.contains(tenderId) ||
notifyListeners(); _feedbackHydrationsInFlight.contains(tenderId)) {
return;
}
_feedbackHydrationsInFlight.add(tenderId);
final result = await _tendersRepository.getTenderFeedback( final result = await _tendersRepository.getTenderFeedback(
tenderId: tenderId, tenderId: tenderId,
); );
switch (result) { switch (result) {
case Ok<FeedbackResponse>(): case Ok<FeedbackResponse>():
_hydratedFeedbackTenderIds.add(tenderId);
_feedbackData = result.value.data; _feedbackData = result.value.data;
if (_feedbackData != null) { if (_feedbackData != null) {
final index = feedbacks.indexWhere( final index = feedbacks.indexWhere(
@@ -353,19 +326,33 @@ class TendersViewModel with ChangeNotifier {
} }
break; break;
case Error<FeedbackResponse>(): case Error<FeedbackResponse>():
_errorMessage = result.error.toString(); if (result.error is NotFoundException) {
// A 404 is the expected representation of neutral feedback state.
feedbacks.removeWhere((feedback) => feedback.tenderId == tenderId);
_hydratedFeedbackTenderIds.add(tenderId);
} else {
_feedbackErrorMessage = result.error.toString();
}
break; break;
} }
_feedbackHydrationsInFlight.remove(tenderId);
notifyListeners(); notifyListeners();
_errorMessage = null; if (_feedbackErrorMessage != null) {
_feedbackErrorMessage = null;
notifyListeners(); notifyListeners();
} }
}
Future<void> toogleFeedback({ Future<void> toggleFeedback({
required String tenderId, required String tenderId,
required String feedbackType, required String feedbackType,
}) async { }) async {
if (_feedbackRequestsInFlight.contains(tenderId)) {
return;
}
_feedbackErrorMessage = null; _feedbackErrorMessage = null;
_feedbackRequestsInFlight.add(tenderId);
final existingFeedback = getFeedbackForTender(tenderId); final existingFeedback = getFeedbackForTender(tenderId);
final existingFeedbackIndex = feedbacks.indexWhere( final existingFeedbackIndex = feedbacks.indexWhere(
@@ -377,13 +364,13 @@ class TendersViewModel with ChangeNotifier {
if (existingFeedback == null) { if (existingFeedback == null) {
optimisticFeedback = FeedbackData( optimisticFeedback = FeedbackData(
companyId: null, companyId: null,
createdAt: DateTime.now().millisecondsSinceEpoch, createdAt: DateTime.now().millisecondsSinceEpoch ~/ 1000,
customerId: null, customerId: null,
feedbackType: feedbackType, feedbackType: feedbackType,
id: null, id: null,
tenderId: tenderId, tenderId: tenderId,
tender: null, tender: null,
updatedAt: DateTime.now().millisecondsSinceEpoch, updatedAt: DateTime.now().millisecondsSinceEpoch ~/ 1000,
); );
feedbacks.add(optimisticFeedback); feedbacks.add(optimisticFeedback);
} else { } else {
@@ -395,7 +382,7 @@ class TendersViewModel with ChangeNotifier {
} else { } else {
optimisticFeedback = existingFeedback.copyWith( optimisticFeedback = existingFeedback.copyWith(
feedbackType: feedbackType, feedbackType: feedbackType,
updatedAt: DateTime.now().millisecondsSinceEpoch, updatedAt: DateTime.now().millisecondsSinceEpoch ~/ 1000,
); );
if (existingFeedbackIndex != -1) { if (existingFeedbackIndex != -1) {
feedbacks[existingFeedbackIndex] = optimisticFeedback; feedbacks[existingFeedbackIndex] = optimisticFeedback;
@@ -413,9 +400,13 @@ class TendersViewModel with ChangeNotifier {
switch (result) { switch (result) {
case Ok<FeedbackResponse>(): case Ok<FeedbackResponse>():
_feedbackData = result.value.data; _feedbackData = result.value.data;
if (_feedbackData != null) { final responseType = _feedbackData?.feedbackType;
if (responseType == 'unlike' || responseType == 'undislike') {
feedbacks.removeWhere((feedback) => feedback.tenderId == tenderId);
_feedbackData = null;
} else if (_feedbackData != null) {
final index = feedbacks.indexWhere( final index = feedbacks.indexWhere(
(f) => f.tender == _feedbackData!.tender, (feedback) => feedback.tenderId == tenderId,
); );
if (index != -1) { if (index != -1) {
feedbacks[index] = _feedbackData!; feedbacks[index] = _feedbackData!;
@@ -425,6 +416,7 @@ class TendersViewModel with ChangeNotifier {
} else { } else {
feedbacks.removeWhere((f) => f.tenderId == tenderId); feedbacks.removeWhere((f) => f.tenderId == tenderId);
} }
_hydratedFeedbackTenderIds.add(tenderId);
break; break;
case Error<FeedbackResponse>(): case Error<FeedbackResponse>():
if (existingFeedback == null) { if (existingFeedback == null) {
@@ -443,13 +435,24 @@ class TendersViewModel with ChangeNotifier {
break; break;
} }
_feedbackRequestsInFlight.remove(tenderId);
notifyListeners(); notifyListeners();
_feedbackErrorMessage = null; _feedbackErrorMessage = null;
notifyListeners(); notifyListeners();
} }
/// Kept temporarily for callers compiled against the old misspelled API.
@Deprecated('Use toggleFeedback')
Future<void> toogleFeedback({
required String tenderId,
required String feedbackType,
}) => toggleFeedback(tenderId: tenderId, feedbackType: feedbackType);
void clearFeedbacks() { void clearFeedbacks() {
feedbacks.clear(); feedbacks.clear();
_hydratedFeedbackTenderIds.clear();
_feedbackHydrationsInFlight.clear();
_feedbackRequestsInFlight.clear();
_feedbackData = null; _feedbackData = null;
_feedbackErrorMessage = null; _feedbackErrorMessage = null;
notifyListeners(); notifyListeners();
@@ -492,54 +495,4 @@ class TendersViewModel with ChangeNotifier {
sortOrder = ''; sortOrder = '';
notifyListeners(); notifyListeners();
} }
/// [selected] is the absolute 0-based target page index.
Future<void> handlePaginationChange(int selected) async {
if (selected < 0) {
return;
}
final maxPage = totalPages - 1;
if (maxPage >= 0 && selected > maxPage) {
return;
}
if (selected == currentPageIndex) {
return;
}
await _goToOffsetPage(selected);
}
Future<void> _goToOffsetPage(int pageIndex) async {
final limit = _tendersResponse?.meta?.limit ?? _pageSize;
_currentOffset = pageIndex * limit;
await _fetchPage();
}
Future<void> _fetchPage() async {
_isLoading = true;
notifyListeners();
final result = await _tendersRepository.getTenders(
limit: _pageSize,
offset: _currentOffset,
query: _searchQuery,
sortBy: sortBy,
sortOrder: sortOrder,
publicationDateFrom: startDateUnix,
publicationDateTo: endDateUnix,
);
_handleListResult(result, fetchAllFeedbacks: true);
_isLoading = false;
notifyListeners();
_errorMessage = null;
notifyListeners();
}
/// Kept for backwards compatibility with existing callers.
/// Accepts a 1-based page number.
void jumpToPage(int page) {
handlePaginationChange(page - 1);
}
} }
+119 -8
View File
@@ -3,9 +3,11 @@ import 'package:intl/intl.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/error_utils.dart'; import 'package:tm_app/core/utils/error_utils.dart';
import 'package:tm_app/core/utils/result.dart'; import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/core/network/app_exceptions.dart';
import 'package:tm_app/data/repositories/your_tenders_repository.dart'; import 'package:tm_app/data/repositories/your_tenders_repository.dart';
import 'package:tm_app/data/services/model/liked_tenders_response/liked_tenders_response.dart'; import 'package:tm_app/data/services/model/liked_tenders_response/liked_tenders_response.dart';
import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart'; import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart';
import 'package:tm_app/data/services/model/tender_submission/tender_submission.dart';
import 'package:tm_app/views/your_tenders/strings/your_tenders_strings.dart'; import 'package:tm_app/views/your_tenders/strings/your_tenders_strings.dart';
import '../core/constants/assets.dart'; import '../core/constants/assets.dart';
@@ -22,6 +24,21 @@ class YourTendersViewModel with ChangeNotifier {
String? errorMessage; String? errorMessage;
TenderApprovalsResponse? data; TenderApprovalsResponse? data;
LikedTendersResponse? likedTendersData; LikedTendersResponse? likedTendersData;
TenderSubmissionsResponse? submissionsData;
TenderSubmissionStats? submissionStats;
String selectedSubmissionStage = 'opportunity';
String? updatingSubmissionId;
String? submissionActionError;
bool get isSubmissionWorkflow =>
selectedStatus == TenderApprovalStatus.submitted.value;
static const submissionStages = <String>[
'opportunity',
'in_progress',
'submitted',
'not_applied',
];
// Pagination state for feedback lists // Pagination state for feedback lists
bool isLoadingMore = false; bool isLoadingMore = false;
@@ -247,6 +264,8 @@ class YourTendersViewModel with ChangeNotifier {
// Clear main data // Clear main data
data = null; data = null;
likedTendersData = null; likedTendersData = null;
submissionsData = null;
submissionStats = null;
// Clear loading states // Clear loading states
isLoading = false; isLoading = false;
@@ -269,6 +288,7 @@ class YourTendersViewModel with ChangeNotifier {
Future<void> callWithFilter() async { Future<void> callWithFilter() async {
likedTendersData = null; likedTendersData = null;
data = null; data = null;
submissionsData = null;
isLoading = true; isLoading = true;
notifyListeners(); notifyListeners();
@@ -282,14 +302,8 @@ class YourTendersViewModel with ChangeNotifier {
); );
await getTenders(requestModel: requestModel, reset: true); await getTenders(requestModel: requestModel, reset: true);
} else if (selectedStatus == TenderApprovalStatus.submitted.value) { } else if (selectedStatus == TenderApprovalStatus.submitted.value) {
final requestModel = GetTendersRequestModel( selectedSubmissionStage = 'submitted';
status: TenderApprovalStatus.submitted.value, await loadSubmissions();
createdFrom: startDateUnix,
createdTo: endDateUnix,
limit: _tendersLimit,
offset: _tendersOffset,
);
await getTenders(requestModel: requestModel, reset: true);
} else if (selectedStatus == TenderFeedback.liked.value) { } else if (selectedStatus == TenderFeedback.liked.value) {
await getTendersFeedback( await getTendersFeedback(
feedbackType: TenderFeedback.liked.value, feedbackType: TenderFeedback.liked.value,
@@ -318,6 +332,11 @@ class YourTendersViewModel with ChangeNotifier {
int currentPage = 1; int currentPage = 1;
int get totalPages { int get totalPages {
if (isSubmissionWorkflow && submissionsData?.meta != null) {
return submissionsData!.meta!.pages == 0
? 1
: submissionsData!.meta!.pages;
}
if (likedTendersData?.data?.meta?.pages != null) { if (likedTendersData?.data?.meta?.pages != null) {
return likedTendersData!.data!.meta!.pages!; return likedTendersData!.data!.meta!.pages!;
} }
@@ -329,6 +348,10 @@ class YourTendersViewModel with ChangeNotifier {
Future<void> jumpToPage(int page) async { Future<void> jumpToPage(int page) async {
currentPage = page; currentPage = page;
if (isSubmissionWorkflow) {
await loadSubmissions(offset: (page - 1) * 20);
return;
}
_offset = (page - 1) * _limit; _offset = (page - 1) * _limit;
_tendersOffset = (page - 1) * _tendersLimit; _tendersOffset = (page - 1) * _tendersLimit;
@@ -340,6 +363,94 @@ class YourTendersViewModel with ChangeNotifier {
await callWithFilter(); await callWithFilter();
} }
Future<void> selectSubmissionStage(String stage) async {
if (!submissionStages.contains(stage) || stage == selectedSubmissionStage) {
return;
}
selectedSubmissionStage = stage;
submissionActionError = null;
currentPage = 1;
notifyListeners();
await loadSubmissions();
}
Future<void> loadSubmissions({int offset = 0}) async {
isLoading = true;
errorMessage = null;
notifyListeners();
final result = await yourTendersRepository.getSubmissions(
stage: selectedSubmissionStage,
limit: 20,
offset: offset,
);
switch (result) {
case Ok<TenderSubmissionsResponse>():
submissionsData = result.value;
currentPage = result.value.meta?.page ?? 1;
case Error<TenderSubmissionsResponse>():
errorMessage = ErrorUtils.getErrorMessage(result.error);
}
final statsResult = await yourTendersRepository.getSubmissionStats();
switch (statsResult) {
case Ok<TenderSubmissionStatsResponse>():
submissionStats = statsResult.value.data;
case Error<TenderSubmissionStatsResponse>():
// The list remains useful when stats are temporarily unavailable.
break;
}
isLoading = false;
notifyListeners();
}
Future<bool> updateSubmissionStatus({
required TenderSubmission submission,
required String status,
String? reason,
String? description,
}) async {
if (submission.id.isEmpty || submission.isTerminal) {
return false;
}
updatingSubmissionId = submission.id;
submissionActionError = null;
notifyListeners();
final result = await yourTendersRepository.updateSubmissionStatus(
id: submission.id,
status: status,
reason: reason,
description: description,
);
switch (result) {
case Ok<TenderSubmissionResponse>():
updatingSubmissionId = null;
await loadSubmissions();
return true;
case Error<TenderSubmissionResponse>():
updatingSubmissionId = null;
if (result.error is AppException &&
(result.error as AppException).statusCode == 409) {
await loadSubmissions();
submissionActionError =
'This submission changed elsewhere. The latest state has been loaded; please try again.';
notifyListeners();
return false;
}
submissionActionError = ErrorUtils.getErrorMessage(result.error);
notifyListeners();
return false;
}
}
void dismissSubmissionActionError() {
submissionActionError = null;
notifyListeners();
}
Color getBackGroundColorByStatus(String status) { Color getBackGroundColorByStatus(String status) {
if (status == TenderFeedback.liked.value || if (status == TenderFeedback.liked.value ||
status == TenderApprovalStatus.submitted.value) { status == TenderApprovalStatus.submitted.value) {
+1 -2
View File
@@ -301,7 +301,7 @@ class _TaskCard extends StatelessWidget {
), ),
), ),
if (due != null) const SizedBox(height: 12), if (due != null) const SizedBox(height: 12),
Text( SelectableText(
card.title ?? '', card.title ?? '',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
@@ -309,7 +309,6 @@ class _TaskCard extends StatelessWidget {
color: AppColors.grey70, color: AppColors.grey70,
), ),
maxLines: 3, maxLines: 3,
overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
if (card.priority != null) _PriorityChip(priority: card.priority!), if (card.priority != null) _PriorityChip(priority: card.priority!),
@@ -4,7 +4,6 @@ import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/tender_detail_view_model.dart'; import 'package:tm_app/view_models/tender_detail_view_model.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_action.dart'; import 'package:tm_app/views/detail/widgets/tender_detail_action.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart'; import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
import '../../../core/constants/tender_approval_status.dart'; import '../../../core/constants/tender_approval_status.dart';
@@ -121,8 +120,6 @@ class _TenderDetailDesktopPageState extends State<TenderDetailDesktopPage> {
detail: detail, detail: detail,
), ),
SizedBox(height: 32.0.h()), SizedBox(height: 32.0.h()),
TenderDetailCard(detail: detail),
SizedBox(height: 24.0.h()),
TenderDetailActions( TenderDetailActions(
isScreenBig: true, isScreenBig: true,
detail: detail, detail: detail,
@@ -5,7 +5,6 @@ import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/tender_detail_view_model.dart'; import 'package:tm_app/view_models/tender_detail_view_model.dart';
import 'package:tm_app/views/detail/strings/tender_details_strings.dart'; import 'package:tm_app/views/detail/strings/tender_details_strings.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_action.dart'; import 'package:tm_app/views/detail/widgets/tender_detail_action.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart'; import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
import 'package:tm_app/views/shared/tender_app_bar.dart'; import 'package:tm_app/views/shared/tender_app_bar.dart';
@@ -112,8 +111,6 @@ class _TenderDetailMobilePageState extends State<TenderDetailMobilePage> {
children: [ children: [
TenderDetailHeader(isScreenBig: false, detail: detail), TenderDetailHeader(isScreenBig: false, detail: detail),
SizedBox(height: 24.0.h()), SizedBox(height: 24.0.h()),
TenderDetailCard(detail: detail),
SizedBox(height: 24.0.h()),
TenderDetailActions( TenderDetailActions(
isScreenBig: false, isScreenBig: false,
detail: detail, detail: detail,
@@ -4,7 +4,6 @@ import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/tender_detail_view_model.dart'; import 'package:tm_app/view_models/tender_detail_view_model.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_action.dart'; import 'package:tm_app/views/detail/widgets/tender_detail_action.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart'; import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
import 'package:tm_app/views/shared/tablet_navigation_widget.dart'; import 'package:tm_app/views/shared/tablet_navigation_widget.dart';
@@ -123,8 +122,6 @@ class _TenderDetailTabletPageState extends State<TenderDetailTabletPage> {
SizedBox(height: 28.0.h()), SizedBox(height: 28.0.h()),
TenderDetailHeader(isScreenBig: true, detail: detail), TenderDetailHeader(isScreenBig: true, detail: detail),
SizedBox(height: 28.0.h()), SizedBox(height: 28.0.h()),
TenderDetailCard(detail: detail),
SizedBox(height: 24.0.h()),
TenderDetailActions( TenderDetailActions(
isScreenBig: true, isScreenBig: true,
detail: detail, detail: detail,
@@ -42,4 +42,35 @@ class TenderDetailsStrings {
static const String requestSuccessfullyRegistered = static const String requestSuccessfullyRegistered =
'Your request has been successfully registered.'; 'Your request has been successfully registered.';
static const String confirmAndClose = 'Confirm and close'; static const String confirmAndClose = 'Confirm and close';
// Toast copy shown after the user confirms the Accept/submission modal
// (see tender_detail_action.dart).
static const String submissionReceived =
'Thank you. We have received your submission and will be in touch with you shortly.';
// Field labels for the tender-details info section (one per backend field
// rendered there).
// Tender detail field labels
static const String noticeTypeCode = 'Notice Type';
static const String formType = 'Form Type';
static const String noticeLanguageCode = 'Notice Language';
static const String issueDate = 'Issue Date';
static const String issueTime = 'Issue Time';
static const String procedureCode = 'Procedure';
static const String durationLabel = 'Duration';
static const String publicationDate = 'Publication Date';
static const String tenderDeadlineDate = 'Tender Deadline';
static const String submissionDeadline = 'Submission Deadline';
static const String applicationDeadline = 'Application Deadline';
static const String countryCode = 'Country';
static const String buyerName = 'Buyer';
static const String companyId = 'Company ID';
static const String buyerCity = 'City';
static const String buyerCountry = 'Buyer Country';
static const String officialLanguages = 'Official Languages';
static const String status = 'Status';
static const String tenderId = 'Tender ID';
static const String lots = 'Lots';
static const String lotId = 'Lot ID';
static const String lotTitle = 'Title';
static const String lotDescription = 'Description';
} }
@@ -1,84 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:tm_app/core/utils/size_config.dart';
import '../../../core/theme/colors.dart';
class DetailDropDown extends StatefulWidget {
final String title;
final List<String> items;
const DetailDropDown({required this.title, required this.items, super.key});
@override
State<DetailDropDown> createState() => _DetailDropDownState();
}
class _DetailDropDownState extends State<DetailDropDown>
with SingleTickerProviderStateMixin {
bool isOpen = true;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
InkWell(
onTap: () {
setState(() {
isOpen = !isOpen;
});
},
child: Container(
width: 24.0.w(),
height: 24.0.h(),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: AppColors.grey60),
),
alignment: Alignment.center,
child: Icon(
isOpen
? CupertinoIcons.chevron_up
: CupertinoIcons.chevron_down,
color: AppColors.grey60,
size: 12,
),
),
),
SizedBox(width: 8.0.w()),
Text(
widget.title,
style: TextStyle(
color: AppColors.grey80,
fontSize: 16.0.sp(),
fontWeight: FontWeight.w600,
),
),
],
),
SizedBox(height: 12.0.h()),
isOpen
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children:
widget.items
.map(
(e) => Padding(
padding: EdgeInsets.only(left: 12.0.w()),
child: Text(e),
),
)
.toList(),
)
: const SizedBox.shrink(),
],
);
}
}
@@ -4,14 +4,11 @@ import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/auth_view_model.dart'; import 'package:tm_app/view_models/auth_view_model.dart';
import 'package:tm_app/view_models/tender_detail_view_model.dart'; import 'package:tm_app/view_models/tender_detail_view_model.dart';
import 'package:tm_app/views/detail/widgets/meeting_time_bottom_sheet.dart';
import 'package:tm_app/views/detail/widgets/meeting_time_dialog.dart';
import 'package:tm_app/views/shared/base_button.dart'; import 'package:tm_app/views/shared/base_button.dart';
import 'package:tm_app/views/shared/select_submission_bottom_sheet.dart'; import 'package:tm_app/views/shared/select_submission_bottom_sheet.dart';
import '../../../core/constants/tender_approval_status.dart'; import '../../../core/constants/tender_approval_status.dart';
import '../../../core/constants/tender_submision_mode.dart'; import '../../../core/constants/tender_submision_mode.dart';
import '../../../core/routes/app_routes.dart';
import '../../../core/utils/app_toast.dart'; import '../../../core/utils/app_toast.dart';
import '../../../data/services/model/tender_data/tender_data.dart'; import '../../../data/services/model/tender_data/tender_data.dart';
import '../../shared/select_submission_dialog.dart'; import '../../shared/select_submission_dialog.dart';
@@ -68,38 +65,10 @@ class _TenderDetailActionsState extends State<TenderDetailActions> {
builder: (context) { builder: (context) {
return SelectSubmissionBottomSheet( return SelectSubmissionBottomSheet(
onConfirm: (String value) { onConfirm: (String value) {
if (value == // Confirming the submission modal shows a
TenderSubmissionMode.selfApply.value) { // confirmation toast (no API call, no
// viewModel.submitTenderApproval( // navigation), per product request.
// tenderId: widget.detail.id!, _showSubmissionReceivedToast();
// submissionMode: value,
// );
CompletionOfDocumentsRouteData(
tenderId: widget.detail.id!,
).push(context);
} else if (value ==
TenderSubmissionMode.partnership.value) {
Future.delayed(Duration.zero, () {
if (context.mounted) {
showModalBottomSheet(
context: context,
builder: (_) {
return MeetingTimeBottomSheet(
onConfirm: (value) {
// AppToast.success(
// context,
// TenderDetailsStrings
// .meetingTimeSuccessfullySet,
// );
},
);
},
);
}
});
}
}, },
); );
}, },
@@ -143,6 +112,16 @@ class _TenderDetailActionsState extends State<TenderDetailActions> {
); );
} }
// Shows a success toast confirming the submission was received. Guards on
// `mounted` because it runs from a modal callback after the sheet/dialog is
// popped, and uses the State's own (stable page) context, not the modal's.
void _showSubmissionReceivedToast() {
if (!mounted) {
return;
}
AppToast.success(context, TenderDetailsStrings.submissionReceived);
}
Row _webActions(Role userRole) { Row _webActions(Role userRole) {
return Row( return Row(
children: [ children: [
@@ -206,33 +185,7 @@ class _TenderDetailActionsState extends State<TenderDetailActions> {
builder: (context) { builder: (context) {
return SelectSubmissionDialog( return SelectSubmissionDialog(
onConfirm: (String value) { onConfirm: (String value) {
if (value == _showSubmissionReceivedToast();
TenderSubmissionMode.selfApply.value) {
if (context.mounted) {
if (userRole == Role.analyst) {
const FinalCompletionOfDocumentsRouteData()
.push(context);
} else {
CompletionOfDocumentsRouteData(
tenderId: widget.detail.id!,
).push(context);
}
}
} else if (value ==
TenderSubmissionMode.partnership.value) {
Future.delayed(Duration.zero, () {
if (context.mounted) {
showDialog(
context: context,
builder: (context) {
return MeetingTimeDialog(
onConfirm: (value) {},
);
},
);
}
});
}
}, },
); );
}, },
@@ -242,41 +195,7 @@ class _TenderDetailActionsState extends State<TenderDetailActions> {
builder: (context) { builder: (context) {
return SelectSubmissionBottomSheet( return SelectSubmissionBottomSheet(
onConfirm: (String value) { onConfirm: (String value) {
if (value == _showSubmissionReceivedToast();
TenderSubmissionMode.selfApply.value) {
// viewModel.submitTenderApproval(
// tenderId: widget.detail.id!,
// submissionMode: value,
// );
if (context.mounted) {
const FinalCompletionOfDocumentsRouteData()
.push(context);
}
} else if (value ==
TenderSubmissionMode.partnership.value) {
showDialog(
context: context,
builder: (context) {
return MeetingTimeBottomSheet(
onConfirm: (value) {
AppToast.success(
context,
TenderDetailsStrings
.meetingTimeSuccessfullySet,
);
},
);
// return MeetingTimeDialog(
// onConfirm: (String value) {
// if (context.mounted) {
// const CompletionOfDocumentsRouteData()
// .push(context);
// }
// },
// );
},
);
}
}, },
); );
}, },
@@ -1,95 +0,0 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/data/services/model/tender_data/tender_data.dart';
import 'package:tm_app/views/detail/strings/tender_details_strings.dart';
class TenderDetailCard extends StatelessWidget {
final TenderData detail;
const TenderDetailCard({required this.detail, super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.paleOrange,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColors.veryPaleOrange),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
TenderDetailsStrings.tenderMatchProfile,
style: TextStyle(
fontSize: 12.0.sp(),
color: AppColors.grey80,
fontWeight: FontWeight.w400,
),
),
Text(
'75%',
style: TextStyle(
fontSize: 12.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.jellyBean,
),
),
],
),
SizedBox(height: 6.0.h()),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: 0.75,
backgroundColor: AppColors.grey20,
valueColor: const AlwaysStoppedAnimation<Color>(
AppColors.jellyBean,
),
minHeight: 6.0.h(),
),
),
SizedBox(height: 16.0.h()),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.warning_rounded, color: AppColors.red),
SizedBox(width: 8.0.w()),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
TenderDetailsStrings.tenderIncompleteResume,
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16.0.sp(),
color: AppColors.grey80,
),
),
SizedBox(height: 4.0.h()),
Text(
TenderDetailsStrings.tenderNoExperience,
style: TextStyle(
fontSize: 14.0.sp(),
color: AppColors.grey70,
fontWeight: FontWeight.w400,
),
),
],
),
),
],
),
],
),
);
}
}
@@ -9,7 +9,6 @@ import 'package:tm_app/views/detail/widgets/tender_detail_info_section.dart';
import '../../../core/constants/assets.dart'; import '../../../core/constants/assets.dart';
import '../../shared/flag.dart'; import '../../shared/flag.dart';
import '../strings/tender_details_strings.dart'; import '../strings/tender_details_strings.dart';
import 'detail_drop_down.dart';
class TenderDetailHeader extends StatelessWidget { class TenderDetailHeader extends StatelessWidget {
final bool isScreenBig; final bool isScreenBig;
@@ -41,44 +40,17 @@ class TenderDetailHeader extends StatelessWidget {
SizedBox(height: 16.0.h()), SizedBox(height: 16.0.h()),
_locationBudgetRow(), _locationBudgetRow(),
SizedBox(height: 24.0.h()), SizedBox(height: 24.0.h()),
_projectId(),
SizedBox(height: 12.0.h()),
_winningChanceAndDificulty(),
SizedBox(height: 24.0.h()),
_keyRisksDropDown(),
SizedBox(height: 24.0.h()),
Divider(color: AppColors.grey20), Divider(color: AppColors.grey20),
TenderDetailInfoSection(isScreenBig: isScreenBig, detail: detail), TenderDetailInfoSection(isScreenBig: isScreenBig, detail: detail),
SizedBox(height: 24.0.h()), SizedBox(height: 24.0.h()),
_overViewTitle(), _overViewTitle(),
SizedBox(height: 12.0.h()), SizedBox(height: 12.0.h()),
_descriptionText(), _descriptionText(),
SizedBox(height: 24.0.h()),
_serviceDropDown(),
SizedBox(height: 24.0.h()),
_requirementsDropDown(),
SizedBox(height: 24.0.h()),
_backgroundDropDown(),
], ],
), ),
); );
} }
Widget _serviceDropDown() =>
const DetailDropDown(title: 'Servive/Product', items: []);
Widget _backgroundDropDown() =>
const DetailDropDown(title: 'Background', items: []);
Widget _requirementsDropDown() {
return const DetailDropDown(
title: 'Requirements (Summary)',
items: [
'Market consultation for selecting and procuring a Customer Data Platform (CDP) to unify student data from multiple sources and improve personalization and marketing.',
],
);
}
Widget _descriptionText() { Widget _descriptionText() {
return Text( return Text(
detail.description ?? '', detail.description ?? '',
@@ -101,82 +73,6 @@ class TenderDetailHeader extends StatelessWidget {
); );
} }
Widget _keyRisksDropDown() {
return const DetailDropDown(
title: 'Key risks & dealbreakers',
items: [
'ISO 27001:2022 likely mandatory for future tender.',
'Lack of past performance — public sector values references.',
'Implementation & ongoing support required (15 person team expectation).',
],
);
}
Widget _winningChanceAndDificulty() {
return Row(
children: [
Container(
height: 24.0.h(),
padding: EdgeInsets.symmetric(
horizontal: 16.0.w(),
vertical: 4.0.h(),
),
decoration: BoxDecoration(
color: AppColors.blue0,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Winning chance: <5%',
style: TextStyle(
color: AppColors.blue10,
fontSize: 12.0.sp(),
fontWeight: FontWeight.w500,
),
),
),
SizedBox(width: 12.0.w()),
Container(
height: 24.0.h(),
padding: EdgeInsets.symmetric(
horizontal: 16.0.w(),
vertical: 4.0.h(),
),
decoration: BoxDecoration(
color: AppColors.blue0,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Difficulty 2/5',
style: TextStyle(
color: AppColors.blue10,
fontSize: 12.0.sp(),
fontWeight: FontWeight.w500,
),
),
),
],
);
}
Widget _projectId() {
return Container(
height: 24.0.h(),
padding: EdgeInsets.symmetric(horizontal: 16.0.w(), vertical: 4.0.h()),
decoration: BoxDecoration(
color: AppColors.blue0,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Project ID 6243372025NLD',
style: TextStyle(
color: AppColors.blue10,
fontSize: 12.0.sp(),
fontWeight: FontWeight.w500,
),
),
);
}
Widget _locationBudgetRow() { Widget _locationBudgetRow() {
return Row( return Row(
children: [ children: [
@@ -245,7 +141,7 @@ class TenderDetailHeader extends StatelessWidget {
); );
} }
Widget _titleText() => Text( Widget _titleText() => SelectableText(
detail.title ?? '', detail.title ?? '',
style: TextStyle( style: TextStyle(
color: AppColors.grey80, color: AppColors.grey80,
@@ -1,10 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/date_utils.dart';
import 'package:tm_app/core/utils/price_extension.dart'; import 'package:tm_app/core/utils/price_extension.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/data/services/model/tender_data/tender_data.dart'; import 'package:tm_app/data/services/model/tender_data/tender_data.dart';
import 'package:tm_app/views/detail/strings/tender_details_strings.dart'; import 'package:tm_app/views/detail/strings/tender_details_strings.dart';
import 'package:tm_app/views/detail/widgets/deadline_item.dart'; import 'package:tm_app/views/detail/widgets/deadline_item.dart';
import 'package:tm_app/views/detail/widgets/info_item.dart'; import 'package:tm_app/views/detail/widgets/info_item.dart';
// Renders only the backend-driven fields for the tender-details page (notice
// type, form type, languages, issue/publication dates, procedure, value,
// duration, deadlines, buyer org/address, lots, status, ids), using
// InfoItem/DeadlineItem. The _infoItem() helper returns an empty list when a
// value is null/blank so empty fields don't render as blank rows (this is why
// each entry is spread with `...`).
class TenderDetailInfoSection extends StatelessWidget { class TenderDetailInfoSection extends StatelessWidget {
final bool isScreenBig; final bool isScreenBig;
final TenderData detail; final TenderData detail;
@@ -17,24 +26,114 @@ class TenderDetailInfoSection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final buyer = detail.buyerOrganization;
final address = buyer?.address;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
DeadlineItem(detail: detail, isScreenBig: isScreenBig), DeadlineItem(detail: detail, isScreenBig: isScreenBig),
InfoItem( ..._infoItem(TenderDetailsStrings.noticeTypeCode, detail.noticeTypeCode),
title: TenderDetailsStrings.tenderClientLabel, ..._infoItem(TenderDetailsStrings.formType, detail.formType),
value: detail.buyerOrganization!.name ?? '', ..._infoItem(
TenderDetailsStrings.noticeLanguageCode,
detail.noticeLanguageCode,
), ),
InfoItem( ..._infoItem(
title: TenderDetailsStrings.estimatedValue, TenderDetailsStrings.issueDate,
value: _date(detail.issueDate),
'${detail.estimatedValue?.formattedPrice} ${detail.currency ?? ''}',
), ),
InfoItem( ..._infoItem(
title: TenderDetailsStrings.tenderReferenceNumberLabel, TenderDetailsStrings.issueTime,
value: detail.noticePublicationId ?? '', _dateTime(detail.issueTime),
), ),
..._infoItem(TenderDetailsStrings.procedureCode, detail.procedureCode),
..._infoItem(
TenderDetailsStrings.estimatedValue,
detail.estimatedValue == null
? null
: '${detail.estimatedValue?.formattedPrice} ${detail.currency ?? ''}',
),
..._infoItem(TenderDetailsStrings.durationLabel, _duration()),
..._infoItem(
TenderDetailsStrings.publicationDate,
_date(detail.publicationDate),
),
..._infoItem(
TenderDetailsStrings.tenderDeadlineDate,
_date(detail.tenderDeadline),
),
..._infoItem(TenderDetailsStrings.countryCode, detail.countryCode),
..._infoItem(TenderDetailsStrings.buyerName, buyer?.name),
..._infoItem(TenderDetailsStrings.companyId, buyer?.companyId),
..._infoItem(TenderDetailsStrings.buyerCity, address?.cityName),
..._infoItem(TenderDetailsStrings.buyerCountry, address?.countryCode),
..._infoItem(
TenderDetailsStrings.officialLanguages,
(detail.officialLanguages == null ||
detail.officialLanguages!.isEmpty)
? null
: detail.officialLanguages!.join(', '),
),
..._infoItem(TenderDetailsStrings.status, detail.status),
..._infoItem(TenderDetailsStrings.tenderId, detail.tenderId),
..._lotsSection(),
], ],
); );
} }
/// Returns an [InfoItem] only when [value] has content, so empty fields are
/// not rendered as blank rows.
List<Widget> _infoItem(String title, String? value) {
if (value == null || value.trim().isEmpty) {
return const [];
}
return [InfoItem(title: title, value: value)];
}
List<Widget> _lotsSection() {
final lots = detail.lots;
if (lots == null || lots.isEmpty) {
return const [];
}
return [
SizedBox(height: 8.0.h()),
Text(
TenderDetailsStrings.lots,
style: TextStyle(
color: AppColors.grey80,
fontWeight: FontWeight.w600,
fontSize: 16.0.sp(),
),
),
SizedBox(height: 8.0.h()),
for (final lot in lots) ...[
..._infoItem(TenderDetailsStrings.lotId, lot.lotId),
..._infoItem(TenderDetailsStrings.lotTitle, lot.title),
..._infoItem(TenderDetailsStrings.lotDescription, lot.description),
],
];
}
String? _duration() {
final duration = detail.duration?.trim() ?? '';
final unit = detail.durationUnit?.trim() ?? '';
final combined = '$duration $unit'.trim();
return combined.isEmpty ? null : combined;
}
String? _date(int? unix) {
if (unix == null) {
return null;
}
return timeConvertor(unix);
}
String? _dateTime(int? unix) {
if (unix == null) {
return null;
}
return timeConvertor(unix, hasTime: true);
}
} }
+15 -23
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/assets.dart'; import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/routes/app_routes.dart'; import 'package:tm_app/core/routes/app_routes.dart';
@@ -67,7 +68,7 @@ class DesktopHomePage extends StatelessWidget {
), ),
), ),
SizedBox(height: 32.0.h()), SizedBox(height: 32.0.h()),
_yourTenderText(homeViewModel), _yourTenderText(context, homeViewModel),
SizedBox(height: 8.0.h()), SizedBox(height: 8.0.h()),
_bottomListView(homeViewModel), _bottomListView(homeViewModel),
], ],
@@ -126,13 +127,7 @@ class DesktopHomePage extends StatelessWidget {
backgroundColor: AppColors.primary20, backgroundColor: AppColors.primary20,
iconPath: AssetsManager.arrows, iconPath: AssetsManager.arrows,
title: HomeStrings.tenderSubmitted, title: HomeStrings.tenderSubmitted,
amount: amount: homeViewModel.submittedTendersCount.toString(),
(homeViewModel
.tenderApprovalsStateResponse
?.data
?.submittedTenders ??
0)
.toString(),
textColor: AppColors.mainBlue, textColor: AppColors.mainBlue,
enableTap: true, enableTap: true,
@@ -151,7 +146,7 @@ class DesktopHomePage extends StatelessWidget {
backgroundColor: AppColors.primary10, backgroundColor: AppColors.primary10,
iconPath: AssetsManager.approve, iconPath: AssetsManager.approve,
title: HomeStrings.approvedTenders, title: HomeStrings.approvedTenders,
amount: '0', amount: homeViewModel.approvedTendersCount.toString(),
textColor: AppColors.jellyBean, textColor: AppColors.jellyBean,
enableTap: true, enableTap: true,
width: 178, width: 178,
@@ -164,19 +159,8 @@ class DesktopHomePage extends StatelessWidget {
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
TenderCard( Expanded(
backgroundColor: AppColors.orange10, child: TenderCard(
iconPath: AssetsManager.shield,
title: HomeStrings.tenderValue,
amount: '0',
textColor: AppColors.orange,
enableTap: true,
width: 178,
height: 148,
onTap: () {},
),
const SizedBox(width: 10),
TenderCard(
backgroundColor: AppColors.grey10, backgroundColor: AppColors.grey10,
iconPath: AssetsManager.thumbLike, iconPath: AssetsManager.thumbLike,
title: HomeStrings.likedTenders, title: HomeStrings.likedTenders,
@@ -191,17 +175,24 @@ class DesktopHomePage extends StatelessWidget {
}); });
}, },
), ),
),
], ],
); );
} }
Widget _yourTenderText(HomeViewModel homeViewModel) { Widget _yourTenderText(BuildContext context, HomeViewModel homeViewModel) {
final isYourTenders = final isYourTenders =
(homeViewModel.data?.data?.tenders?.isNotEmpty ?? false); (homeViewModel.data?.data?.tenders?.isNotEmpty ?? false);
return ConstrainedBox( return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 740), constraints: const BoxConstraints(maxWidth: 740),
child: Align( child: Align(
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap:
isYourTenders
? null
: () => context.go('/tenders?tab=recommended'),
child: Text( child: Text(
isYourTenders isYourTenders
? HomeStrings.yourTenders ? HomeStrings.yourTenders
@@ -213,6 +204,7 @@ class DesktopHomePage extends StatelessWidget {
), ),
), ),
), ),
),
); );
} }
+16 -22
View File
@@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/assets.dart'; import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/routes/app_routes.dart'; import 'package:tm_app/core/routes/app_routes.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/data/services/model/tender_approvals_stats_response/tender_approvals_stats_response.dart';
import 'package:tm_app/view_models/home_view_model.dart'; import 'package:tm_app/view_models/home_view_model.dart';
import 'package:tm_app/views/shared/tender_app_bar.dart'; import 'package:tm_app/views/shared/tender_app_bar.dart';
@@ -43,14 +43,11 @@ class MobileHomePage extends StatelessWidget {
SizedBox(height: 32.0.h()), SizedBox(height: 32.0.h()),
_progressBarsRow(homeViewModel), _progressBarsRow(homeViewModel),
SizedBox(height: 32.0.h()), SizedBox(height: 32.0.h()),
_firstTenderCardsRow( _firstTenderCardsRow(context, homeViewModel),
context,
homeViewModel.tenderApprovalsStateResponse!,
),
SizedBox(height: 8.0.h()), SizedBox(height: 8.0.h()),
_secondTenderCardsRow(homeViewModel, context), _secondTenderCardsRow(homeViewModel, context),
SizedBox(height: 40.0.h()), SizedBox(height: 40.0.h()),
_yourTenderText(homeViewModel), _yourTenderText(context, homeViewModel),
_bottomListView(homeViewModel), _bottomListView(homeViewModel),
], ],
), ),
@@ -92,7 +89,7 @@ class MobileHomePage extends StatelessWidget {
Widget _firstTenderCardsRow( Widget _firstTenderCardsRow(
BuildContext context, BuildContext context,
TenderApprovalsStatsResponse tenderApprovalsStateResponse, HomeViewModel homeViewModel,
) { ) {
return Padding( return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0.w()), padding: EdgeInsets.symmetric(horizontal: 24.0.w()),
@@ -103,9 +100,7 @@ class MobileHomePage extends StatelessWidget {
backgroundColor: AppColors.primary20, backgroundColor: AppColors.primary20,
iconPath: AssetsManager.arrows, iconPath: AssetsManager.arrows,
title: HomeStrings.tenderSubmitted, title: HomeStrings.tenderSubmitted,
amount: amount: homeViewModel.submittedTendersCount.toString(),
(tenderApprovalsStateResponse.data?.submittedTenders ?? 0)
.toString(),
textColor: AppColors.mainBlue, textColor: AppColors.mainBlue,
enableTap: true, enableTap: true,
onTap: () { onTap: () {
@@ -118,7 +113,7 @@ class MobileHomePage extends StatelessWidget {
backgroundColor: AppColors.primary10, backgroundColor: AppColors.primary10,
iconPath: AssetsManager.approve, iconPath: AssetsManager.approve,
title: HomeStrings.approvedTenders, title: HomeStrings.approvedTenders,
amount: '0', amount: homeViewModel.approvedTendersCount.toString(),
textColor: AppColors.jellyBean, textColor: AppColors.jellyBean,
enableTap: true, enableTap: true,
@@ -142,38 +137,36 @@ class MobileHomePage extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
TenderCard( Expanded(
backgroundColor: AppColors.orange10, child: TenderCard(
iconPath: AssetsManager.shield,
title: HomeStrings.tenderValue,
amount: '0',
textColor: AppColors.orange,
enableTap: true,
onTap: () {},
),
TenderCard(
backgroundColor: AppColors.grey10, backgroundColor: AppColors.grey10,
iconPath: AssetsManager.thumbLike, iconPath: AssetsManager.thumbLike,
title: HomeStrings.likedTenders, title: HomeStrings.likedTenders,
amount: homeViewModel.userLikedTendersCount.toString(), amount: homeViewModel.userLikedTendersCount.toString(),
textColor: AppColors.grey50, textColor: AppColors.grey50,
enableTap: true, enableTap: true,
width: double.infinity,
onTap: () { onTap: () {
const LikedTendersRouteData().push(context).then((value) { const LikedTendersRouteData().push(context).then((value) {
homeViewModel.init(); homeViewModel.init();
}); });
}, },
), ),
),
], ],
), ),
); );
} }
Widget _yourTenderText(HomeViewModel homeViewModel) { Widget _yourTenderText(BuildContext context, HomeViewModel homeViewModel) {
final isYourTenders = final isYourTenders =
(homeViewModel.data?.data?.tenders?.isNotEmpty ?? false); (homeViewModel.data?.data?.tenders?.isNotEmpty ?? false);
return Padding( return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.0.w()), padding: EdgeInsets.symmetric(horizontal: 24.0.w()),
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap:
isYourTenders ? null : () => context.go('/tenders?tab=recommended'),
child: Text( child: Text(
isYourTenders isYourTenders
? HomeStrings.yourTenders ? HomeStrings.yourTenders
@@ -184,6 +177,7 @@ class MobileHomePage extends StatelessWidget {
color: AppColors.grey80, color: AppColors.grey80,
), ),
), ),
),
); );
} }
+16 -28
View File
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tm_app/core/routes/app_routes.dart'; import 'package:tm_app/core/routes/app_routes.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/data/services/model/tender_approvals_stats_response/tender_approvals_stats_response.dart';
import 'package:tm_app/view_models/home_view_model.dart'; import 'package:tm_app/view_models/home_view_model.dart';
import 'package:tm_app/views/shared/tablet_navigation_widget.dart'; import 'package:tm_app/views/shared/tablet_navigation_widget.dart';
@@ -58,14 +58,11 @@ class TabletHomePage extends StatelessWidget {
SizedBox(height: 40.0.h()), SizedBox(height: 40.0.h()),
_progressBarsRow(homeViewModel), _progressBarsRow(homeViewModel),
SizedBox(height: 32.0.h()), SizedBox(height: 32.0.h()),
_firstTenderCardsRow( _firstTenderCardsRow(context, homeViewModel),
context,
homeViewModel.tenderApprovalsStateResponse!,
),
SizedBox(height: 8.0.h()), SizedBox(height: 8.0.h()),
_secondTenderCardsRow(homeViewModel, context), _secondTenderCardsRow(homeViewModel, context),
SizedBox(height: 32.0.h()), SizedBox(height: 32.0.h()),
_yourTenderText(homeViewModel), _yourTenderText(context, homeViewModel),
SizedBox(height: 8.0.h()), SizedBox(height: 8.0.h()),
_bottomListView(homeViewModel), _bottomListView(homeViewModel),
], ],
@@ -112,7 +109,7 @@ class TabletHomePage extends StatelessWidget {
Widget _firstTenderCardsRow( Widget _firstTenderCardsRow(
BuildContext context, BuildContext context,
TenderApprovalsStatsResponse tenderApprovalsStateResponse, HomeViewModel homeViewModel,
) { ) {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -122,9 +119,7 @@ class TabletHomePage extends StatelessWidget {
backgroundColor: AppColors.primary20, backgroundColor: AppColors.primary20,
iconPath: AssetsManager.arrows, iconPath: AssetsManager.arrows,
title: HomeStrings.tenderSubmitted, title: HomeStrings.tenderSubmitted,
amount: amount: homeViewModel.submittedTendersCount.toString(),
(tenderApprovalsStateResponse.data?.submittedTenders ?? 0)
.toString(),
textColor: AppColors.mainBlue, textColor: AppColors.mainBlue,
enableTap: true, enableTap: true,
@@ -143,7 +138,7 @@ class TabletHomePage extends StatelessWidget {
backgroundColor: AppColors.primary10, backgroundColor: AppColors.primary10,
iconPath: AssetsManager.approve, iconPath: AssetsManager.approve,
title: HomeStrings.approvedTenders, title: HomeStrings.approvedTenders,
amount: '0', amount: homeViewModel.approvedTendersCount.toString(),
textColor: AppColors.jellyBean, textColor: AppColors.jellyBean,
enableTap: true, enableTap: true,
width: double.infinity, width: double.infinity,
@@ -166,20 +161,6 @@ class TabletHomePage extends StatelessWidget {
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Expanded(
child: TenderCard(
backgroundColor: AppColors.orange10,
iconPath: AssetsManager.shield,
title: HomeStrings.tenderValue,
amount: '0',
textColor: AppColors.orange,
enableTap: true,
width: double.infinity,
height: 148,
onTap: () {},
),
),
const SizedBox(width: 16.0),
Expanded( Expanded(
child: TenderCard( child: TenderCard(
backgroundColor: AppColors.grey10, backgroundColor: AppColors.grey10,
@@ -201,16 +182,23 @@ class TabletHomePage extends StatelessWidget {
); );
} }
Widget _yourTenderText(HomeViewModel homeViewModel) { Widget _yourTenderText(BuildContext context, HomeViewModel homeViewModel) {
final isYourTenders = final isYourTenders =
(homeViewModel.data?.data?.tenders?.isNotEmpty ?? false); (homeViewModel.data?.data?.tenders?.isNotEmpty ?? false);
return Text( return GestureDetector(
isYourTenders ? HomeStrings.yourTenders : HomeStrings.recommendedTenders, behavior: HitTestBehavior.opaque,
onTap:
isYourTenders ? null : () => context.go('/tenders?tab=recommended'),
child: Text(
isYourTenders
? HomeStrings.yourTenders
: HomeStrings.recommendedTenders,
style: TextStyle( style: TextStyle(
fontSize: 18.0.sp(), fontSize: 18.0.sp(),
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: AppColors.grey80, color: AppColors.grey80,
), ),
),
); );
} }
+2 -2
View File
@@ -5,11 +5,11 @@ class HomeStrings {
static const String partnership = 'Partnership'; static const String partnership = 'Partnership';
static const String selfApply = 'Self-apply'; static const String selfApply = 'Self-apply';
static const String contracting = 'Contracting'; static const String contracting = 'Contracting';
static const String tenderSubmitted = 'Tenders submitted'; static const String tenderSubmitted = 'Submitted tenders';
static const String approvedTenders = 'Approved tenders'; static const String approvedTenders = 'Approved tenders';
static const String tenderValue = 'Tender Value'; static const String tenderValue = 'Tender Value';
static const String thunderStatus = 'Thunder status'; static const String thunderStatus = 'Thunder status';
static const String likedTenders = 'Liked Tenders'; static const String likedTenders = 'Liked tenders';
static const String yourTenders = 'Your tenders'; static const String yourTenders = 'Your tenders';
static const String recommendation = 'Recommendation'; static const String recommendation = 'Recommendation';
static const String welcome = 'Welcome'; static const String welcome = 'Welcome';
+2 -2
View File
@@ -75,10 +75,10 @@ class TendersListItem extends StatelessWidget {
), ),
), ),
SizedBox(height: 12.0.h()), SizedBox(height: 12.0.h()),
Text( SelectableText(
tender.title ?? '', tender.title ?? '',
maxLines: 2,
style: TextStyle( style: TextStyle(
overflow: TextOverflow.ellipsis,
fontSize: 16.0.sp(), fontSize: 16.0.sp(),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.grey80, color: AppColors.grey80,
@@ -128,10 +128,10 @@ class LikedListItem extends StatelessWidget {
), ),
), ),
SizedBox(height: 12.0.h()), SizedBox(height: 12.0.h()),
Text( SelectableText(
tender.title!, tender.title!,
maxLines: 2,
style: TextStyle( style: TextStyle(
overflow: TextOverflow.ellipsis,
fontSize: 16.0.sp(), fontSize: 16.0.sp(),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.grey80, color: AppColors.grey80,
@@ -75,6 +75,35 @@ class NotificationMoreDialog extends StatelessWidget {
), ),
), ),
), ),
Visibility(
visible: notification.link != null && notification.link != '',
child: Padding(
padding: EdgeInsets.only(left: 28.0.w(), top: 8.0.h()),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.link,
size: 16.0.sp(),
color: AppColors.mainBlue,
),
SizedBox(width: 4.0.w()),
Expanded(
child: SelectableText(
notification.link ?? '',
style: TextStyle(
fontSize: 13.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.mainBlue,
decoration: TextDecoration.underline,
decorationColor: AppColors.mainBlue,
),
),
),
],
),
),
),
SizedBox(height: 12.0.h()), SizedBox(height: 12.0.h()),
Padding( Padding(
padding: EdgeInsets.only(left: 28.0.w()), padding: EdgeInsets.only(left: 28.0.w()),
+3 -1
View File
@@ -28,8 +28,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
_tabService.addListener(_onTabChanged); _tabService.addListener(_onTabChanged);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
// _viewModel.getCompanyProfile();
_viewModel.getProfile(); _viewModel.getProfile();
// Loads the company profile and, when its documents/website are ready,
// kicks off AI onboarding (guarded so it only runs on profile changes).
_viewModel.getCompanyProfile();
}); });
} }
@@ -87,25 +87,9 @@ class DesktopNavigationWidget extends StatelessWidget {
iconPath: AssetsManager.tenders, iconPath: AssetsManager.tenders,
activeIconPath: AssetsManager.tendersActive, activeIconPath: AssetsManager.tendersActive,
), ),
const SizedBox(width: 24), // Board tab is intentionally hidden from desktop navigation. The
_navigationItem( // Board route/page itself remains intact; the nav item (at tab index
context: context, // 2) can be restored from git history when the feature is re-enabled.
text: TendersStrings.board,
isActive: currentIndex == 2,
onTap: () {
if (currentIndex == 2) {
return;
} else {
Router.neglect(
context,
() => const BoardRouteData().go(context),
);
// context.read<TendersViewModel>().getTenders();
}
},
iconPath: AssetsManager.board,
activeIconPath: AssetsManager.boardActive,
),
const SizedBox(width: 24), const SizedBox(width: 24),
_navigationItem( _navigationItem(
context: context, context: context,
+20 -213
View File
@@ -1,26 +1,17 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/tenders_view_model.dart'; import 'package:tm_app/view_models/tenders_view_model.dart';
import 'package:tm_app/views/shared/base_button.dart';
import 'package:tm_app/views/shared/desktop_navigation_widget.dart'; import 'package:tm_app/views/shared/desktop_navigation_widget.dart';
import 'package:tm_app/views/tenders/strings/tenders_strings.dart'; import 'package:tm_app/views/tenders/widgets/recommended_tenders_view.dart';
import 'package:tm_app/views/tenders/widgets/main_tenders_slider.dart';
import 'package:tm_app/views/tenders/widgets/tenders_filter_dialog.dart';
import 'package:tm_app/views/tenders/widgets/tenders_sort_dialog.dart';
import 'package:tm_app/views/tenders/widgets/windowed_pagination.dart';
import '../../../core/constants/common_strings.dart';
import '../../../core/utils/app_toast.dart'; import '../../../core/utils/app_toast.dart';
import '../../../view_models/auth_view_model.dart';
class DesktopTendersPage extends StatefulWidget { class DesktopTendersPage extends StatefulWidget {
const DesktopTendersPage({super.key}); const DesktopTendersPage({required this.initialSelectedTab, super.key});
final int initialSelectedTab;
@override @override
State<DesktopTendersPage> createState() => _DesktopTendersPageState(); State<DesktopTendersPage> createState() => _DesktopTendersPageState();
@@ -50,220 +41,36 @@ class _DesktopTendersPageState extends State<DesktopTendersPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final userRole = context.watch<AuthViewModel>().userRole;
return Scaffold( return Scaffold(
backgroundColor: AppColors.backgroundColor, backgroundColor: AppColors.backgroundColor,
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
const DesktopNavigationWidget(currentIndex: 1), const DesktopNavigationWidget(currentIndex: 1),
SizedBox(height: 48.0.h()), SizedBox(height: 24.0.h()),
if (userRole == Role.analyst) const _SearchBox(), // Tab bar hidden while tenders only shows recommendations.
if (userRole == Role.analyst) const _ActionButtons(), // SizedBox(
Expanded( // width: 740,
child: Consumer<TendersViewModel>( // child: TendersTabBar(
builder: (context, vm, child) { // selectedIndex: 1,
if (vm.isLoading) { // onChanged: (index) {},
return const Center( // ),
child: CircularProgressIndicator( // ),
color: AppColors.jellyBean, // SizedBox(height: 24.0.h()),
), // All tenders controls are hidden while the All tab is disabled.
); // if (userRole == Role.analyst) const _SearchBox(),
} // if (userRole == Role.analyst) const _ActionButtons(),
if (vm.errorMessage != null) { const Expanded(
return Center(child: Text(vm.errorMessage!)); child: Center(
}
final tenders = vm.tendersResponse?.data?.tenders ?? [];
if (tenders.isEmpty) {
return const Center(child: Text(CommonStrings.noData));
}
return Column(
children: [
Expanded(
child: SingleChildScrollView(
child: SizedBox( child: SizedBox(
width: 740, width: 740,
height: 800, child: RecommendedTendersView(isDesktop: true),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 4.0.h()),
child: MainTendersSlider(
tenders: tenders,
isDesktop: true,
), ),
), ),
), ),
),
),
SizedBox(
width: 740,
child: WindowedPagination(
currentPage: vm.currentPageIndex,
totalPages: vm.totalPages,
onPageChanged: vm.handlePaginationChange,
),
),
],
);
},
),
),
], ],
), ),
), ),
); );
} }
} }
class _SearchBox extends StatefulWidget {
const _SearchBox();
@override
State<_SearchBox> createState() => _SearchBoxState();
}
class _SearchBoxState extends State<_SearchBox> {
final TextEditingController _searchController = TextEditingController();
Timer? _debounceTimer;
@override
void dispose() {
_searchController.dispose();
_debounceTimer?.cancel();
super.dispose();
}
void _onSearchChanged(String query) {
_debounceTimer?.cancel();
_debounceTimer = Timer(const Duration(milliseconds: 500), () {
final viewModel = context.read<TendersViewModel>();
viewModel.searchTenders(query);
});
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 48.0.h(),
width: 680,
child: TextField(
controller: _searchController,
onChanged: _onSearchChanged,
style: TextStyle(fontSize: 16.0.sp(), color: AppColors.grey60),
decoration: InputDecoration(
hintText: TendersStrings.search,
hintStyle: TextStyle(fontSize: 16.0.sp(), color: AppColors.grey60),
prefixIcon: Padding(
padding: EdgeInsets.all(12.0.w()),
child: SvgPicture.asset(
AssetsManager.normalSearch,
width: 24.0.w(),
height: 24.0.h(),
),
),
filled: true,
fillColor: AppColors.grey0,
contentPadding: EdgeInsets.symmetric(
horizontal: 12.0.w(),
vertical: 12.0.h(),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0.w()),
borderSide: const BorderSide(color: AppColors.mainBlue, width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0.w()),
borderSide: BorderSide(color: AppColors.grey40, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0.w()),
borderSide: const BorderSide(color: AppColors.grey, width: 1.5),
),
),
),
);
}
}
class _ActionButtons extends StatelessWidget {
const _ActionButtons();
@override
Widget build(BuildContext context) {
final viewModel = context.read<TendersViewModel>();
return Container(
margin: EdgeInsets.only(top: 25.0.h()),
height: 40.0.h(),
width: 680,
child: Row(
children: [
BaseButton(
width: 178.0.w(),
isEnabled: true,
onPressed: () {
showDialog(
context: context,
builder:
(_) => Dialog(
backgroundColor: AppColors.backgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: SizedBox(
width: 560,
child: TendersFilterDialog(
viewModel: viewModel,
onFilterChanged: (dateRange, status) {
viewModel.setDateRange(dateRange);
viewModel.setStatus(status!);
viewModel.callWithSortAndStatus();
},
),
),
),
);
},
text: TendersStrings.filter,
icon: AssetsManager.filter,
iconColor: AppColors.textBlue,
backgroundColor: AppColors.primary20,
textColor: AppColors.textBlue,
),
SizedBox(width: 10.0.w()),
BaseButton(
width: 178.0.w(),
isEnabled: true,
onPressed: () {
showDialog(
context: context,
builder:
(_) => Dialog(
backgroundColor: AppColors.backgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: SizedBox(
width: 560,
child: TendersSortDialog(
viewModel: viewModel,
onConfirm: (sort) {
viewModel.setSort(sort);
viewModel.callWithSortAndStatus();
},
),
),
),
);
},
text: TendersStrings.sort,
icon: AssetsManager.sort,
iconColor: AppColors.textBlue,
backgroundColor: AppColors.primary20,
textColor: AppColors.textBlue,
),
],
),
);
}
}
+19 -46
View File
@@ -1,18 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/tenders_view_model.dart'; import 'package:tm_app/view_models/tenders_view_model.dart';
import '../../../core/constants/common_strings.dart';
import '../../../core/theme/colors.dart'; import '../../../core/theme/colors.dart';
import '../../../core/utils/app_toast.dart'; import '../../../core/utils/app_toast.dart';
import '../../shared/tender_app_bar.dart'; import '../../shared/tender_app_bar.dart';
import '../strings/tenders_strings.dart'; import '../strings/tenders_strings.dart';
import '../widgets/main_tenders_slider.dart'; import '../widgets/recommended_tenders_view.dart';
import '../widgets/windowed_pagination.dart';
class MobileTendersPage extends StatefulWidget { class MobileTendersPage extends StatefulWidget {
const MobileTendersPage({super.key}); const MobileTendersPage({required this.initialSelectedTab, super.key});
final int initialSelectedTab;
@override @override
State<MobileTendersPage> createState() => _MobileTendersPageState(); State<MobileTendersPage> createState() => _MobileTendersPageState();
@@ -20,6 +19,7 @@ class MobileTendersPage extends StatefulWidget {
class _MobileTendersPageState extends State<MobileTendersPage> { class _MobileTendersPageState extends State<MobileTendersPage> {
late final TendersViewModel viewModel; late final TendersViewModel viewModel;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -44,49 +44,22 @@ class _MobileTendersPageState extends State<MobileTendersPage> {
return Scaffold( return Scaffold(
backgroundColor: AppColors.backgroundColor, backgroundColor: AppColors.backgroundColor,
appBar: tenderMobileAppBar(title: TendersStrings.tendersTitle), appBar: tenderMobileAppBar(title: TendersStrings.tendersTitle),
body: SafeArea( body: const SafeArea(
child: Consumer<TendersViewModel>( child: Column(
builder: (context, viewModel, child) {
if (viewModel.isLoading) {
return const Center(
child: CircularProgressIndicator(color: AppColors.jellyBean),
);
}
if (viewModel.errorMessage != null) {
return Center(child: Text(viewModel.errorMessage!));
}
if ((viewModel.tendersResponse?.data?.tenders == null ||
viewModel.tendersResponse!.data!.tenders!.isEmpty) &&
viewModel.isLoading == false) {
return const Center(child: Text(CommonStrings.noData));
}
return Column(
children: [ children: [
Expanded( // Tab bar hidden while tenders only shows recommendations.
child: SingleChildScrollView( // Padding(
child: Padding( // padding: EdgeInsets.symmetric(
padding: EdgeInsets.symmetric(vertical: 18.0.h()), // horizontal: 24.0.w(),
child: MainTendersSlider( // vertical: 12.0.h(),
key: ValueKey( // ),
'tenders-slider-${viewModel.currentPageIndex}', // child: TendersTabBar(
), // selectedIndex: 1,
tenders: // onChanged: (index) {},
viewModel.tendersResponse?.data?.tenders ?? [], // ),
), // ),
), Expanded(child: RecommendedTendersView()),
),
),
WindowedPagination(
currentPage: viewModel.currentPageIndex,
totalPages: viewModel.totalPages,
onPageChanged: viewModel.handlePaginationChange,
compact: true,
),
], ],
);
},
), ),
), ),
); );
+15 -43
View File
@@ -4,15 +4,15 @@ import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/tenders_view_model.dart'; import 'package:tm_app/view_models/tenders_view_model.dart';
import 'package:tm_app/views/shared/tablet_navigation_widget.dart'; import 'package:tm_app/views/shared/tablet_navigation_widget.dart';
import 'package:tm_app/views/tenders/widgets/main_tenders_slider.dart';
import 'package:tm_app/views/tenders/widgets/windowed_pagination.dart';
import '../../../core/constants/common_strings.dart';
import '../../shared/tender_app_bar.dart'; import '../../shared/tender_app_bar.dart';
import '../strings/tenders_strings.dart'; import '../strings/tenders_strings.dart';
import '../widgets/recommended_tenders_view.dart';
class TabletTendersPage extends StatefulWidget { class TabletTendersPage extends StatefulWidget {
const TabletTendersPage({super.key}); const TabletTendersPage({required this.initialSelectedTab, super.key});
final int initialSelectedTab;
@override @override
State<TabletTendersPage> createState() => _TabletTendersPageState(); State<TabletTendersPage> createState() => _TabletTendersPageState();
@@ -39,47 +39,19 @@ class _TabletTendersPageState extends State<TabletTendersPage> {
context: context, context: context,
), ),
drawer: const TabletNavigationWidget(currentIndex: 1), drawer: const TabletNavigationWidget(currentIndex: 1),
body: Consumer<TendersViewModel>( body: Padding(
builder: (context, viewModel, child) { padding: EdgeInsetsDirectional.only(end: 24.0.w(), top: 24.0.h()),
if (viewModel.isLoading) { child: const Column(
return const Center(
child: CircularProgressIndicator(color: AppColors.jellyBean),
);
}
if (viewModel.errorMessage != null) {
return Center(child: Text(viewModel.errorMessage!));
}
if ((viewModel.tendersResponse?.data?.tenders == null ||
viewModel.tendersResponse!.data!.tenders!.isEmpty) &&
viewModel.isLoading == false) {
return const Center(child: Text(CommonStrings.noData));
}
return Column(
children: [ children: [
Expanded( // Tab bar hidden while tenders only shows recommendations.
child: SingleChildScrollView( // TendersTabBar(
child: Padding( // selectedIndex: 1,
padding: EdgeInsetsDirectional.only( // onChanged: (index) {},
end: 24.0.w(), // ),
top: 128.0.h(), // SizedBox(height: 24.0.h()),
), Expanded(child: RecommendedTendersView()),
child: MainTendersSlider(
key: ValueKey(
'tenders-slider-${viewModel.currentPageIndex}',
),
tenders: viewModel.tendersResponse?.data?.tenders ?? [],
),
),
),
),
WindowedPagination(
currentPage: viewModel.currentPageIndex,
totalPages: viewModel.totalPages,
onPageChanged: viewModel.handlePaginationChange,
),
], ],
); ),
},
), ),
); );
} }
+8 -5
View File
@@ -10,7 +10,9 @@ import 'm_tenders_page.dart';
import 't_tenders_page.dart'; import 't_tenders_page.dart';
class TendersScreen extends StatefulWidget { class TendersScreen extends StatefulWidget {
const TendersScreen({super.key}); const TendersScreen({this.initialTab, super.key});
final String? initialTab;
@override @override
State<TendersScreen> createState() => _TendersScreenState(); State<TendersScreen> createState() => _TendersScreenState();
@@ -48,10 +50,11 @@ class _TendersScreenState extends State<TendersScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
SizeConfig.init(context); SizeConfig.init(context);
return const ResponsiveBuilder( final initialSelectedTab = widget.initialTab == 'recommended' ? 1 : 0;
mobile: MobileTendersPage(), return ResponsiveBuilder(
tablet: TabletTendersPage(), mobile: MobileTendersPage(initialSelectedTab: initialSelectedTab),
desktop: DesktopTendersPage(), tablet: TabletTendersPage(initialSelectedTab: initialSelectedTab),
desktop: DesktopTendersPage(initialSelectedTab: initialSelectedTab),
); );
} }
} }
@@ -27,4 +27,22 @@ class TendersStrings {
static const String minimumTime = 'Minimum time'; static const String minimumTime = 'Minimum time';
static const String tenderBudgetLabel = 'Budget :'; static const String tenderBudgetLabel = 'Budget :';
static const String board = 'Board'; static const String board = 'Board';
// Recommended (AI) tab
static const String allTab = 'All';
static const String recommendedTab = 'Recommended';
static const String recommendedMatchLabel = 'Match';
static const String recommendedRankPrefix = '#';
static const String recommendedStillLearningTitle =
'Were still learning about your company';
static const String recommendedStillLearningBody =
'Recommendations will appear here once we finish analyzing your company. Check back soon.';
static const String recommendedEmptyTitle = 'No recommendations yet';
static const String recommendedEmptyBody =
'Add your company documents or website to get personalized tender recommendations.';
static const String recommendedError =
'Could not load recommendations. Please try again.';
static const String recommendedFallbackNotice =
'Showing keyword-based suggestions while AI recommendations are unavailable.';
static const String retry = 'Retry';
} }
@@ -8,6 +8,7 @@ class AnimatedFeedbackButton extends StatefulWidget {
final Color activeColor; final Color activeColor;
final Color inactiveColor; final Color inactiveColor;
final bool isActive; final bool isActive;
final bool isLoading;
final VoidCallback onTap; final VoidCallback onTap;
final String tooltip; final String tooltip;
@@ -18,6 +19,7 @@ class AnimatedFeedbackButton extends StatefulWidget {
required this.isActive, required this.isActive,
required this.onTap, required this.onTap,
required this.tooltip, required this.tooltip,
this.isLoading = false,
super.key, super.key,
}); });
@@ -63,6 +65,9 @@ class _AnimatedFeedbackButtonState extends State<AnimatedFeedbackButton>
} }
void _handleTap() { void _handleTap() {
if (widget.isLoading) {
return;
}
_clickController.forward().then((_) { _clickController.forward().then((_) {
_clickController.reverse(); _clickController.reverse();
}); });
@@ -75,7 +80,7 @@ class _AnimatedFeedbackButtonState extends State<AnimatedFeedbackButton>
message: widget.tooltip, message: widget.tooltip,
child: InkWell( child: InkWell(
splashColor: Colors.transparent, splashColor: Colors.transparent,
onTap: _handleTap, onTap: widget.isLoading ? null : _handleTap,
child: AnimatedBuilder( child: AnimatedBuilder(
animation: _clickController, animation: _clickController,
builder: (context, child) { builder: (context, child) {
@@ -94,15 +99,23 @@ class _AnimatedFeedbackButtonState extends State<AnimatedFeedbackButton>
duration: const Duration(milliseconds: 100), duration: const Duration(milliseconds: 100),
curve: Curves.easeInOut, curve: Curves.easeInOut,
builder: (context, color, child) { builder: (context, color, child) {
return SvgPicture.asset( return SizedBox(
width: 24.0.w(), width: 24.0.w(),
height: 24.0.w(), height: 24.0.w(),
child:
widget.isLoading
? CircularProgressIndicator(
strokeWidth: 2,
color: color ?? widget.inactiveColor,
)
: SvgPicture.asset(
fit: BoxFit.cover, fit: BoxFit.cover,
widget.iconPath, widget.iconPath,
colorFilter: ColorFilter.mode( colorFilter: ColorFilter.mode(
color ?? widget.inactiveColor, color ?? widget.inactiveColor,
BlendMode.srcATop, BlendMode.srcATop,
), ),
),
); );
}, },
), ),
@@ -25,8 +25,92 @@ class MainTendersSlider extends StatefulWidget {
class _MainTendersSliderState extends State<MainTendersSlider> { class _MainTendersSliderState extends State<MainTendersSlider> {
final PageController pageController = PageController(initialPage: 0); final PageController pageController = PageController(initialPage: 0);
final ScrollController _scrollController = ScrollController();
int currentPage = 1; int currentPage = 1;
/// Start prefetching the next page once this fraction of the currently
/// loaded items has been scrolled/swiped past.
static const double _prefetchFraction = 0.5;
@override
void initState() {
super.initState();
// The scroll controller is only attached to the desktop ListView, so only
// register the prefetch listener there. Mobile prefetching is driven by
// PageView's onPageChanged instead.
if (widget.isDesktop) {
_scrollController.addListener(_onScroll);
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_hydrateRenderedFeedback();
}
});
}
@override
void didUpdateWidget(covariant MainTendersSlider oldWidget) {
super.didUpdateWidget(oldWidget);
final oldIds = oldWidget.tenders.map((tender) => tender.id).toList();
final newIds = widget.tenders.map((tender) => tender.id).toList();
if (oldIds.length != newIds.length ||
!Iterable<int>.generate(
newIds.length,
).every((index) => oldIds[index] == newIds[index])) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_hydrateRenderedFeedback();
}
});
}
}
void _hydrateRenderedFeedback() {
if (widget.tenders.isEmpty) {
return;
}
final viewModel = context.read<TendersViewModel>();
final tendersToHydrate =
widget.isDesktop
? widget.tenders
: <TenderData>[
widget.tenders[(currentPage - 1).clamp(
0,
widget.tenders.length - 1,
)],
];
for (final tender in tendersToHydrate) {
final id = tender.id;
if (id != null && id.isNotEmpty) {
viewModel.getTenderFeedback(id);
}
}
}
void _onScroll() {
if (!_scrollController.hasClients) {
return;
}
final position = _scrollController.position;
if (position.maxScrollExtent <= 0) {
return;
}
if (position.pixels >= position.maxScrollExtent * _prefetchFraction) {
_maybeLoadMore();
}
}
void _maybeLoadMore() {
final viewModel = context.read<TendersViewModel>();
if (viewModel.hasMoreData &&
!viewModel.isLoadingMore &&
!viewModel.isLoading) {
viewModel.loadMoreTenders();
}
}
void _goToPreviousPage() { void _goToPreviousPage() {
if (currentPage == 1) { if (currentPage == 1) {
return; return;
@@ -82,6 +166,7 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
@override @override
void dispose() { void dispose() {
pageController.dispose(); pageController.dispose();
_scrollController.dispose();
super.dispose(); super.dispose();
} }
@@ -106,6 +191,10 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
controller: pageController, controller: pageController,
itemCount: widget.tenders.length, itemCount: widget.tenders.length,
onPageChanged: (index) { onPageChanged: (index) {
// currentPage is still the previous page here; a forward swipe means
// the new (1-indexed) page is greater than the old one.
final bool isForwardSwipe = index + 1 > currentPage;
setState(() { setState(() {
currentPage = index + 1; currentPage = index + 1;
}); });
@@ -115,6 +204,15 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
if (!viewModel.hasFeedbackForTender(id)) { if (!viewModel.hasFeedbackForTender(id)) {
viewModel.getTenderFeedback(id); viewModel.getTenderFeedback(id);
} }
// Infinite scroll: once we swipe forward past the halfway point of the
// loaded tenders, prefetch the next page in the background. Skip
// back-swipes so we don't refetch while the user reviews earlier
// tenders.
if (isForwardSwipe &&
index >= (widget.tenders.length * _prefetchFraction).floor()) {
_maybeLoadMore();
}
}, },
itemBuilder: (context, index) { itemBuilder: (context, index) {
final tender = widget.tenders[index]; final tender = widget.tenders[index];
@@ -230,6 +328,7 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
children: [ children: [
Expanded( Expanded(
child: ListView.separated( child: ListView.separated(
controller: _scrollController,
padding: EdgeInsets.all(16.0.h()), padding: EdgeInsets.all(16.0.h()),
itemCount: itemCount:
widget.tenders.length + widget.tenders.length +
@@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/routes/app_routes.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/data/services/model/recommendation_item/recommendation_item.dart';
import '../strings/tenders_strings.dart';
/// A single AI recommendation: rank badge + the AI's analysis, tappable to open
/// the tender detail via [RecommendationItem.tenderId].
class RecommendationCard extends StatelessWidget {
const RecommendationCard({required this.item, super.key});
final RecommendationItem item;
void _openDetail(BuildContext context) {
final tenderId = item.tenderId;
if (tenderId == null || tenderId.isEmpty) {
return;
}
TenderDetailRouteData(tenderId: tenderId).push(context);
}
@override
Widget build(BuildContext context) {
final analysis = item.analysis?.trim() ?? '';
return InkWell(
borderRadius: BorderRadius.circular(12.0.w()),
onTap: () => _openDetail(context),
child: Container(
width: double.infinity,
padding: EdgeInsets.all(16.0.h()),
decoration: BoxDecoration(
color: AppColors.grey0,
borderRadius: BorderRadius.circular(12.0.w()),
border: Border.all(color: AppColors.grey30),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
_rankBadge(),
SizedBox(width: 12.0.w()),
Text(
TendersStrings.recommendedMatchLabel,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.grey70,
),
),
],
),
if (analysis.isNotEmpty) ...[
SizedBox(height: 12.0.h()),
Text(
analysis,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey80,
height: 1.5,
),
),
],
SizedBox(height: 16.0.h()),
Align(
alignment: AlignmentDirectional.centerEnd,
child: Text(
TendersStrings.tenderSeeMore,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.mainBlue,
),
),
),
],
),
),
);
}
Widget _rankBadge() {
final rank = item.rank?.trim();
final label =
(rank == null || rank.isEmpty)
? TendersStrings.recommendedRankPrefix
: '${TendersStrings.recommendedRankPrefix}$rank';
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.0.w(), vertical: 4.0.h()),
decoration: BoxDecoration(
color: AppColors.primary10,
borderRadius: BorderRadius.circular(8.0.w()),
),
child: Text(
label,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w700,
color: AppColors.mainBlue,
),
),
);
}
}
@@ -0,0 +1,170 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/ai_recommendations_view_model.dart';
import '../strings/tenders_strings.dart';
import 'main_tenders_slider.dart';
import 'tender_card_shimmer.dart';
/// The "Recommended" tab body. Handles loading, empty/"still learning",
/// error+retry and the recommended tender list for [AiRecommendationsViewModel].
class RecommendedTendersView extends StatefulWidget {
const RecommendedTendersView({super.key, this.isDesktop = false});
final bool isDesktop;
@override
State<RecommendedTendersView> createState() => _RecommendedTendersViewState();
}
class _RecommendedTendersViewState extends State<RecommendedTendersView> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
context.read<AiRecommendationsViewModel>().loadIfNeeded();
}
});
}
@override
Widget build(BuildContext context) {
return Consumer<AiRecommendationsViewModel>(
builder: (context, vm, child) {
if (vm.isLoading) {
return _LoadingSkeleton(isDesktop: widget.isDesktop);
}
if (vm.errorMessage != null) {
return _MessageState(
title: TendersStrings.recommendedError,
onRetry: vm.refresh,
);
}
if (vm.stillLearning) {
return _MessageState(
title: TendersStrings.recommendedStillLearningTitle,
body: TendersStrings.recommendedStillLearningBody,
onRetry: vm.refresh,
);
}
if (vm.recommendedTenders.isNotEmpty) {
if (widget.isDesktop) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4.0.h()),
child: MainTendersSlider(
tenders: vm.recommendedTenders,
isDesktop: true,
),
);
}
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 18.0.h()),
child: MainTendersSlider(tenders: vm.recommendedTenders),
),
);
}
return _MessageState(
title: TendersStrings.recommendedEmptyTitle,
body: TendersStrings.recommendedEmptyBody,
onRetry: vm.refresh,
);
},
);
}
}
/// Centered title/body message with an optional retry button. Wrapped in a
/// scroll view so pull-to-refresh works even when empty.
class _MessageState extends StatelessWidget {
const _MessageState({required this.title, this.body, this.onRetry});
final String title;
final String? body;
final Future<void> Function()? onRetry;
@override
Widget build(BuildContext context) {
final content = Padding(
padding: EdgeInsets.symmetric(horizontal: 32.0.w(), vertical: 48.0.h()),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.grey70,
),
),
if (body != null) ...[
SizedBox(height: 12.0.h()),
Text(
body!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w400,
color: AppColors.grey60,
height: 1.5,
),
),
],
if (onRetry != null) ...[
SizedBox(height: 24.0.h()),
TextButton(
onPressed: onRetry,
child: Text(
TendersStrings.retry,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w600,
color: AppColors.mainBlue,
),
),
),
],
],
),
);
if (onRetry == null) {
return Center(child: content);
}
return RefreshIndicator(
color: AppColors.jellyBean,
onRefresh: onRetry!,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: SizeConfig.screenHeight * 0.6),
child: Center(child: content),
),
),
);
}
}
/// Simple placeholder list shown while recommendations load.
class _LoadingSkeleton extends StatelessWidget {
const _LoadingSkeleton({required this.isDesktop});
final bool isDesktop;
@override
Widget build(BuildContext context) {
return ListView.separated(
padding: EdgeInsets.all(16.0.h()),
itemCount: isDesktop ? 3 : 2,
separatorBuilder:
(_, __) => SizedBox(height: isDesktop ? 20.0.h() : 52.0.h()),
itemBuilder: (context, index) => TenderCardShimmer(isDesktop: isDesktop),
);
}
}
@@ -48,6 +48,7 @@ class TenderActionButtonsRow extends StatelessWidget {
final feedback = viewModel.getFeedbackForTender(tender.id!); final feedback = viewModel.getFeedbackForTender(tender.id!);
final isDisliked = feedback?.feedbackType == TenderFeedback.disliked.value; final isDisliked = feedback?.feedbackType == TenderFeedback.disliked.value;
final isLoading = viewModel.isFeedbackLoading(tender.id!);
return Column( return Column(
children: [ children: [
@@ -56,9 +57,10 @@ class TenderActionButtonsRow extends StatelessWidget {
activeColor: AppColors.errorColor, activeColor: AppColors.errorColor,
inactiveColor: AppColors.grey50, inactiveColor: AppColors.grey50,
isActive: isDisliked, isActive: isDisliked,
isLoading: isLoading,
tooltip: TendersStrings.dislike, tooltip: TendersStrings.dislike,
onTap: () { onTap: () {
viewModel.toogleFeedback( viewModel.toggleFeedback(
tenderId: tender.id!, tenderId: tender.id!,
feedbackType: TenderFeedback.disliked.value, feedbackType: TenderFeedback.disliked.value,
); );
@@ -85,6 +87,7 @@ class TenderActionButtonsRow extends StatelessWidget {
final feedback = viewModel.getFeedbackForTender(tender.id!); final feedback = viewModel.getFeedbackForTender(tender.id!);
final isLiked = feedback?.feedbackType == TenderFeedback.liked.value; final isLiked = feedback?.feedbackType == TenderFeedback.liked.value;
final isLoading = viewModel.isFeedbackLoading(tender.id!);
return Column( return Column(
children: [ children: [
@@ -93,9 +96,10 @@ class TenderActionButtonsRow extends StatelessWidget {
activeColor: AppColors.successColor, activeColor: AppColors.successColor,
inactiveColor: AppColors.grey50, inactiveColor: AppColors.grey50,
isActive: isLiked, isActive: isLiked,
isLoading: isLoading,
tooltip: TendersStrings.like, tooltip: TendersStrings.like,
onTap: () { onTap: () {
viewModel.toogleFeedback( viewModel.toggleFeedback(
tenderId: tender.id!, tenderId: tender.id!,
feedbackType: TenderFeedback.liked.value, feedbackType: TenderFeedback.liked.value,
); );
+1 -55
View File
@@ -8,7 +8,6 @@ import '../../../core/constants/assets.dart';
import '../../../core/theme/colors.dart'; import '../../../core/theme/colors.dart';
import '../../../core/utils/date_utils.dart'; import '../../../core/utils/date_utils.dart';
import '../../../core/utils/size_config.dart'; import '../../../core/utils/size_config.dart';
import '../../detail/strings/tender_details_strings.dart';
import '../../shared/flag.dart'; import '../../shared/flag.dart';
import '../strings/tenders_strings.dart'; import '../strings/tenders_strings.dart';
import 'tender_action_buttons_row.dart'; import 'tender_action_buttons_row.dart';
@@ -83,14 +82,6 @@ class TenderCard extends StatelessWidget {
), ),
child: Column( child: Column(
children: [ children: [
// Match percentage
_matchPercentage(),
SizedBox(height: isDesktop ? 4.0.h() : 8.0.h()),
// Progress bar
_progressBar(),
SizedBox(height: isDesktop ? 8.0.h() : 16.0.h()),
// Location and apply button // Location and apply button
Row( Row(
children: [ children: [
@@ -207,7 +198,7 @@ class TenderCard extends StatelessWidget {
} }
Widget _tenderTitle() { Widget _tenderTitle() {
return Text( return SelectableText(
tender.title ?? '', tender.title ?? '',
maxLines: isDesktop ? 2 : 4, maxLines: isDesktop ? 2 : 4,
style: TextStyle( style: TextStyle(
@@ -243,51 +234,6 @@ class TenderCard extends StatelessWidget {
); );
} }
Widget _matchPercentage() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
TenderDetailsStrings.tenderMatchProfile,
style: TextStyle(
color: AppColors.grey80,
fontSize: isDesktop ? 12.0.sp() : 14.0.sp(),
fontWeight: isDesktop ? FontWeight.w400 : FontWeight.w500,
),
),
Text(
'${45}%',
style: TextStyle(
color: AppColors.secondaryTextColor,
fontSize: isDesktop ? 12.0.sp() : 16.0.sp(),
fontWeight: FontWeight.w600,
),
),
],
);
}
Widget _progressBar() {
return Container(
width: double.infinity,
height: 4.0.h(),
decoration: BoxDecoration(
color: AppColors.grey20,
borderRadius: BorderRadius.circular(2),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: (45) / 100,
child: Container(
decoration: BoxDecoration(
color: AppColors.secondary50,
borderRadius: BorderRadius.circular(2),
),
),
),
);
}
Widget _locationInfo() { Widget _locationInfo() {
return Expanded( return Expanded(
child: Row( child: Row(
@@ -0,0 +1,173 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
class TenderCardShimmer extends StatelessWidget {
const TenderCardShimmer({super.key, this.isDesktop = false});
final bool isDesktop;
@override
Widget build(BuildContext context) {
return _Shimmer(
child: Container(
width: double.infinity,
margin: EdgeInsets.symmetric(horizontal: 16.0.w()),
height: isDesktop ? 350.0.h() : 587.0.h(),
padding: EdgeInsets.all(16.0.w()),
decoration: BoxDecoration(
color: AppColors.grey0,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.grey30),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_bar(width: isDesktop ? 120.0.w() : 140.0.w(), height: 14.0.h()),
SizedBox(height: 16.0.h()),
_block(height: isDesktop ? 58.0.h() : 70.0.h()),
SizedBox(height: isDesktop ? 12.0.h() : 20.0.h()),
_bar(width: double.infinity, height: 18.0.h()),
SizedBox(height: 8.0.h()),
_fractionBar(widthFactor: 0.72, height: 18.0.h()),
SizedBox(height: 16.0.h()),
_bar(width: double.infinity, height: 14.0.h()),
SizedBox(height: 8.0.h()),
_bar(width: double.infinity, height: 14.0.h()),
SizedBox(height: 8.0.h()),
_fractionBar(widthFactor: 0.55, height: 14.0.h()),
const Spacer(),
Row(
children: [
_circle(size: 24.0.h()),
SizedBox(width: 8.0.w()),
_bar(width: 60.0.w(), height: 14.0.h()),
const Spacer(),
_pill(width: 108.0.w(), height: 40.0.h()),
],
),
],
),
),
);
}
Widget _bar({required double width, required double height}) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
color: AppColors.grey20,
borderRadius: BorderRadius.circular(6),
),
);
}
Widget _fractionBar({required double widthFactor, required double height}) {
return FractionallySizedBox(
widthFactor: widthFactor,
alignment: AlignmentDirectional.centerStart,
child: _bar(width: double.infinity, height: height),
);
}
Widget _block({required double height}) {
return Container(
width: double.infinity,
height: height,
decoration: BoxDecoration(
color: AppColors.grey20,
borderRadius: BorderRadius.circular(4),
),
);
}
Widget _pill({required double width, required double height}) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
color: AppColors.grey20,
borderRadius: BorderRadius.circular(100),
),
);
}
Widget _circle({required double size}) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
color: AppColors.grey20,
shape: BoxShape.circle,
),
);
}
}
class _Shimmer extends StatefulWidget {
const _Shimmer({required this.child});
final Widget child;
@override
State<_Shimmer> createState() => _ShimmerState();
}
class _ShimmerState extends State<_Shimmer>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1400),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: widget.child,
builder: (context, child) {
return ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (bounds) {
final shimmerWidth = bounds.width * 0.8;
final offset = (bounds.width + shimmerWidth) * _controller.value;
return LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [AppColors.grey20, AppColors.grey10, AppColors.grey20],
stops: const [0.25, 0.5, 0.75],
transform: _SlidingGradientTransform(
slidePercent: (offset - shimmerWidth) / bounds.width,
),
).createShader(bounds);
},
child: child,
);
},
);
}
}
class _SlidingGradientTransform extends GradientTransform {
const _SlidingGradientTransform({required this.slidePercent});
final double slidePercent;
@override
Matrix4 transform(Rect bounds, {TextDirection? textDirection}) {
return Matrix4.translationValues(bounds.width * slidePercent, 0, 0);
}
}
@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import '../strings/tenders_strings.dart';
/// Segmented control for the tenders list.
class TendersTabBar extends StatelessWidget {
const TendersTabBar({
required this.selectedIndex,
required this.onChanged,
super.key,
});
/// 1 = Recommended. The former All tab is intentionally hidden.
final int selectedIndex;
final ValueChanged<int> onChanged;
@override
Widget build(BuildContext context) {
return Container(
height: 44.0.h(),
padding: EdgeInsets.all(4.0.h()),
decoration: BoxDecoration(
color: AppColors.grey0,
borderRadius: BorderRadius.circular(12.0.w()),
border: Border.all(color: AppColors.grey30),
),
child: Row(
children: [
// All tenders is hidden for now.
// _segment(TendersStrings.allTab, 0),
_segment(TendersStrings.recommendedTab, 1),
],
),
);
}
Widget _segment(String label, int index) {
final bool isActive = selectedIndex == index;
return Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => onChanged(index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isActive ? AppColors.mainBlue : Colors.transparent,
borderRadius: BorderRadius.circular(8.0.w()),
),
child: Text(
label,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w600,
color: isActive ? AppColors.grey0 : AppColors.grey60,
),
),
),
),
);
}
}
@@ -1,250 +0,0 @@
import 'package:flutter/material.dart';
import 'package:tm_app/core/theme/colors.dart';
/// A windowed numbered pagination widget that mirrors the Next.js
/// implementation: ±10 page-button window around the current page,
/// 1-based labels, prev/next chevrons.
class WindowedPagination extends StatelessWidget {
const WindowedPagination({
super.key,
required this.currentPage,
required this.totalPages,
required this.onPageChanged,
this.maxPageJump = 10,
this.compact = false,
});
/// 0-based absolute current page index.
final int currentPage;
/// Total number of pages (1-based count).
final int totalPages;
/// Called with the 0-based target page index.
final ValueChanged<int> onPageChanged;
/// Half-width of the page-button window (web uses 10).
final int maxPageJump;
/// If true, renders smaller buttons (for mobile).
final bool compact;
@override
Widget build(BuildContext context) {
if (totalPages <= 1) {
return const SizedBox.shrink();
}
final clampedCurrent = currentPage.clamp(0, totalPages - 1);
final windowStart = (clampedCurrent - maxPageJump).clamp(0, totalPages - 1);
final windowEnd = (clampedCurrent + maxPageJump).clamp(0, totalPages - 1);
final buttons = <Widget>[];
buttons.add(
_NavButton(
icon: Icons.chevron_left,
enabled: clampedCurrent > 0,
compact: compact,
onTap: () => onPageChanged(clampedCurrent - 1),
),
);
if (windowStart > 0) {
buttons.add(_buildPageButton(0, clampedCurrent));
if (windowStart > 1) {
buttons.add(const _Ellipsis());
}
}
for (var i = windowStart; i <= windowEnd; i++) {
buttons.add(_buildPageButton(i, clampedCurrent));
}
if (windowEnd < totalPages - 1) {
if (windowEnd < totalPages - 2) {
buttons.add(const _Ellipsis());
}
buttons.add(_buildPageButton(totalPages - 1, clampedCurrent));
}
buttons.add(
_NavButton(
icon: Icons.chevron_right,
enabled: clampedCurrent < totalPages - 1,
compact: compact,
onTap: () => onPageChanged(clampedCurrent + 1),
),
);
final gap = compact ? 4.0 : 6.0;
return Padding(
padding: EdgeInsets.symmetric(vertical: compact ? 8 : 12),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: constraints.maxWidth),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _withSpacing(buttons, gap),
),
),
);
},
),
);
}
Widget _buildPageButton(int pageIndex, int currentPageIndex) {
final isActive = pageIndex == currentPageIndex;
return _PageButton(
label: '${pageIndex + 1}',
isActive: isActive,
compact: compact,
onTap: isActive ? null : () => onPageChanged(pageIndex),
);
}
static List<Widget> _withSpacing(List<Widget> items, double gap) {
final result = <Widget>[];
for (var i = 0; i < items.length; i++) {
result.add(items[i]);
if (i != items.length - 1) {
result.add(SizedBox(width: gap));
}
}
return result;
}
}
class _PageButton extends StatelessWidget {
const _PageButton({
required this.label,
required this.isActive,
required this.compact,
this.onTap,
});
final String label;
final bool isActive;
final bool compact;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final size = compact ? 32.0 : 38.0;
return SizedBox(
width: size,
height: size,
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onTap,
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
gradient: isActive
? const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.mainBlue, AppColors.primaryColor],
)
: null,
color: isActive ? null : AppColors.grey0,
border: Border.all(
color: isActive ? Colors.transparent : AppColors.grey30,
width: 1,
),
boxShadow: isActive
? [
BoxShadow(
color: AppColors.mainBlue.withValues(alpha: 0.25),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
),
child: Text(
label,
style: TextStyle(
fontSize: compact ? 12 : 13,
fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
color: isActive ? Colors.white : AppColors.grey70,
),
),
),
),
),
);
}
}
class _NavButton extends StatelessWidget {
const _NavButton({
required this.icon,
required this.enabled,
required this.compact,
required this.onTap,
});
final IconData icon;
final bool enabled;
final bool compact;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final size = compact ? 32.0 : 38.0;
return SizedBox(
width: size,
height: size,
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: enabled ? onTap : null,
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: AppColors.grey0,
border: Border.all(
color: enabled ? AppColors.grey30 : AppColors.grey20,
width: 1,
),
),
child: Icon(
icon,
size: compact ? 18 : 20,
color: enabled ? AppColors.grey70 : AppColors.grey40,
),
),
),
),
);
}
}
class _Ellipsis extends StatelessWidget {
const _Ellipsis();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: Text(
'',
style: TextStyle(
fontSize: 14,
color: AppColors.grey60,
fontWeight: FontWeight.w500,
),
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More