Compare commits

...

10 Commits

Author SHA1 Message Date
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
43 changed files with 1728 additions and 1022 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
+22 -2
View File
@@ -1,3 +1,23 @@
FROM docker-mirror.ravanertebat.ir/hub/nginx:1.25.2-alpine3.18-slim
FROM docker-mirror.ravanertebat.ir/ghcr/cirruslabs/flutter:3.38.10 AS base
COPY ./artifacts/flutter/tm_pwa /usr/share/nginx/html
WORKDIR /app
RUN flutter config --enable-web
FROM base AS deps
COPY pubspec.yaml pubspec.lock ./
RUN flutter pub get
FROM base AS builder
# Pull in the resolved package config from deps so `flutter pub get` is not
# re-run when only source files change.
COPY --from=deps /app/.dart_tool ./.dart_tool
COPY . .
RUN flutter build web --release --output=build/tm_pwa
FROM docker-mirror.ravanertebat.ir/hub/nginx:1.25.2-alpine3.18-slim AS runner
COPY --from=builder /app/build/tm_pwa /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+12
View File
@@ -0,0 +1,12 @@
/// 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';
}
+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;
}
}
}
+14 -51
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 '../config/app_config.dart';
import '../constants/pref_keys.dart';
import '../utils/logger.dart';
import '../utils/result.dart';
import 'app_exceptions.dart';
import 'auth_interceptor.dart';
import 'network_connectivity.dart';
class NetworkManager {
@@ -45,54 +47,15 @@ class NetworkManager {
return;
}
final requestInterceptor = InterceptorsWrapper(
onRequest: (options, handler) {
appLogger.info('🌐 Request: ${options.method} ${options.path}');
String? token = _prefs.getString('bearer');
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
return handler.next(options);
},
onResponse: (response, handler) {
appLogger.info(
'✅ Response: ${response.statusCode} ${response.requestOptions.path}',
mainDio.interceptors.add(
AuthInterceptor(
dio: mainDio,
prefs: _prefs,
refreshToken: refreshToken,
// Resolved lazily so the callback can be wired up after construction.
onAuthFailed: () => _onAuthFailed,
),
);
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;
}
@@ -167,7 +130,7 @@ class NetworkManager {
}
}
String? bearerToken = _prefs.getString('bearer');
String? bearerToken = _prefs.getString(PrefKeys.bearer);
// String? url = prefs.getString('url');
// appLogger.info('🔗 Base URL: ${url ?? 'NOT SET'}');
@@ -344,7 +307,7 @@ class NetworkManager {
/// Refresh the access token using the refresh token
Future<String?> refreshToken() async {
try {
final refreshToken = _prefs.getString('refresh_token');
final refreshToken = _prefs.getString(PrefKeys.refreshToken);
if (refreshToken == null) {
appLogger.error('❌ No refresh token available');
return null;
@@ -363,9 +326,9 @@ class NetworkManager {
final newRefreshToken = loginResponse.data.refreshToken;
if (newAccessToken != null) {
await _prefs.setString('bearer', newAccessToken);
await _prefs.setString(PrefKeys.bearer, newAccessToken);
if (newRefreshToken != null) {
await _prefs.setString('refresh_token', newRefreshToken);
await _prefs.setString(PrefKeys.refreshToken, newRefreshToken);
}
appLogger.info('✅ Token refreshed successfully');
return newAccessToken;
+2 -1
View File
@@ -18,6 +18,7 @@ import '../../views/profile/pages/profile_screen.dart';
import '../../views/splash/pages/splash_screen.dart';
import '../../views/tenders/pages/tenders_screen.dart';
import '../../views/your_tenders/pages/your_tenders_screen.dart';
import '../constants/pref_keys.dart';
import '../providers/board_provider.dart';
import '../providers/final_completion_provider.dart';
import '../providers/forgot_password_provider.dart';
@@ -51,7 +52,7 @@ final GoRouter appRouter = GoRouter(
navigatorKey: rootNavigatorKey,
redirect: (context, state) {
final prefs = context.read<SharedPreferences>();
final hasToken = prefs.getString('bearer') != null;
final hasToken = prefs.getString(PrefKeys.bearer) != null;
final path = state.uri.path;
final location = state.matchedLocation;
final isSplash = path == '/splash' || location == '/splash';
+1 -19
View File
@@ -47,7 +47,6 @@ mixin _$LoginScreenRoute on GoRouteData {
RouteBase get $splashScreenRoute => GoRouteData.$route(
path: '/splash',
factory: _$SplashScreenRoute._fromState,
);
@@ -111,40 +110,33 @@ RouteBase get $appShellRouteData => StatefulShellRouteData.$route(
branches: [
StatefulShellBranchData.$branch(
navigatorKey: HomeBranch.$navigatorKey,
routes: [
GoRouteData.$route(path: '/home', factory: _$HomeRouteData._fromState),
],
),
StatefulShellBranchData.$branch(
navigatorKey: TendersBranch.$navigatorKey,
routes: [
GoRouteData.$route(
path: '/tenders',
factory: _$TendersRouteData._fromState,
),
],
),
StatefulShellBranchData.$branch(
navigatorKey: NotificationBranch.$navigatorKey,
routes: [
GoRouteData.$route(
path: '/notification',
factory: _$NotificationRouteData._fromState,
),
],
),
StatefulShellBranchData.$branch(
navigatorKey: ProfileBranch.$navigatorKey,
routes: [
GoRouteData.$route(
path: '/profile',
factory: _$ProfileRouteData._fromState,
),
],
@@ -242,7 +234,6 @@ mixin _$ProfileRouteData on GoRouteData {
RouteBase get $yourTendersRouteData => GoRouteData.$route(
path: '/your-tenders',
factory: _$YourTendersRouteData._fromState,
);
@@ -274,7 +265,6 @@ mixin _$YourTendersRouteData on GoRouteData {
RouteBase get $finalCompletionOfDocumentsRouteData => GoRouteData.$route(
path: '/final_completion_of_documents',
factory: _$FinalCompletionOfDocumentsRouteData._fromState,
);
@@ -302,7 +292,6 @@ mixin _$FinalCompletionOfDocumentsRouteData on GoRouteData {
RouteBase get $completionOfDocumentsRouteData => GoRouteData.$route(
path: '/completion_of_documents',
factory: _$CompletionOfDocumentsRouteData._fromState,
);
@@ -318,9 +307,7 @@ mixin _$CompletionOfDocumentsRouteData on GoRouteData {
@override
String get location => GoRouteData.$location(
'/completion_of_documents',
queryParams: {
if (_self.tenderId != null) 'tender-id': _self.tenderId!,
},
queryParams: {if (_self.tenderId != null) 'tender-id': _self.tenderId},
);
@override
@@ -339,7 +326,6 @@ mixin _$CompletionOfDocumentsRouteData on GoRouteData {
RouteBase get $likedTendersRouteData => GoRouteData.$route(
path: '/like-tenders',
factory: _$LikedTendersRouteData._fromState,
);
@@ -390,7 +376,6 @@ mixin _$BoardRouteData on GoRouteData {
RouteBase get $tenderDetailRouteData => GoRouteData.$route(
path: '/tender-detail',
factory: _$TenderDetailRouteData._fromState,
);
@@ -422,7 +407,6 @@ mixin _$TenderDetailRouteData on GoRouteData {
RouteBase get $forgotPasswordRouteData => GoRouteData.$route(
path: '/forgot-password',
factory: _$ForgotPasswordRouteData._fromState,
);
@@ -449,7 +433,6 @@ mixin _$ForgotPasswordRouteData on GoRouteData {
RouteBase get $forgotPasswordOtpRouteData => GoRouteData.$route(
path: '/forgot-password-otp',
factory: _$ForgotPasswordOtpRouteData._fromState,
);
@@ -481,7 +464,6 @@ mixin _$ForgotPasswordOtpRouteData on GoRouteData {
RouteBase get $forgotPasswordCreateRouteData => GoRouteData.$route(
path: '/forgot-password-create',
factory: _$ForgotPasswordCreateRouteData._fromState,
);
+9 -8
View File
@@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:firebase_messaging/firebase_messaging.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/data/services/api/auth_api.dart';
import 'package:tm_app/data/services/model/customer/customer.dart';
@@ -58,13 +59,13 @@ class AuthService {
final refreshToken = result.value.data.refreshToken;
final customer = result.value.data.customer;
await prefs.setString('bearer', token!);
await prefs.setString('refresh_token', refreshToken!);
await prefs.setString(PrefKeys.bearer, token!);
await prefs.setString(PrefKeys.refreshToken, refreshToken!);
// Save customer data as JSON string
if (customer != null) {
final customerJson = jsonEncode(customer.toJson());
await prefs.setString('customer_data', customerJson);
await prefs.setString(PrefKeys.customerData, customerJson);
AppLogger().info('👤 Customer data saved to preferences');
}
}
@@ -102,9 +103,9 @@ class AuthService {
Future<void> localLogout() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('bearer');
await prefs.remove('refresh_token');
await prefs.remove('customer_data');
await prefs.remove(PrefKeys.bearer);
await prefs.remove(PrefKeys.refreshToken);
await prefs.remove(PrefKeys.customerData);
appLogger.info('tokens and customer data cleared!');
}
@@ -141,7 +142,7 @@ class AuthService {
Future<Result<bool>> checkIsLoggedIn() async {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('bearer');
final token = prefs.getString(PrefKeys.bearer);
if (token == null) {
return const Result.ok(false);
}
@@ -150,7 +151,7 @@ class AuthService {
Future<Customer?> getStoredCustomer() async {
final prefs = await SharedPreferences.getInstance();
final customerJson = prefs.getString('customer_data');
final customerJson = prefs.getString(PrefKeys.customerData);
if (customerJson == null) {
return null;
}
+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,
};
@@ -1,15 +1,22 @@
// ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:tm_app/data/services/model/organization/organization_address.dart';
part 'organization.freezed.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
abstract class Organization with _$Organization {
const factory Organization({
required String? name,
@JsonKey(name: 'company_id') required String? companyId,
required OrganizationAddress? address,
}) = _Organization;
factory Organization.fromJson(Map<String, Object?> json) =>
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$Organization {
String? get name;
String? get name;@JsonKey(name: 'company_id') String? get companyId; OrganizationAddress? get address;
/// Create a copy of Organization
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $OrganizationCopyWith<Organization> get copyWith => _$OrganizationCopyWithImpl<O
@override
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)
@override
int get hashCode => Object.hash(runtimeType,name);
int get hashCode => Object.hash(runtimeType,name,companyId,address);
@override
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;
@useResult
$Res call({
String? name
String? name,@JsonKey(name: 'company_id') String? companyId, OrganizationAddress? address
});
$OrganizationAddressCopyWith<$Res>? get address;
}
/// @nodoc
@@ -65,13 +65,27 @@ class _$OrganizationCopyWithImpl<$Res>
/// Create a copy of Organization
/// 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(
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) {
case _Organization() when $default != null:
return $default(_that.name);case _:
return $default(_that.name,_that.companyId,_that.address);case _:
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) {
case _Organization():
return $default(_that.name);case _:
return $default(_that.name,_that.companyId,_that.address);case _:
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) {
case _Organization() when $default != null:
return $default(_that.name);case _:
return $default(_that.name,_that.companyId,_that.address);case _:
return null;
}
@@ -209,10 +223,12 @@ return $default(_that.name);case _:
@JsonSerializable()
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);
@override final String? name;
@override@JsonKey(name: 'company_id') final String? companyId;
@override final OrganizationAddress? address;
/// Create a copy of Organization
/// with the given fields replaced by the non-null parameter values.
@@ -227,16 +243,16 @@ Map<String, dynamic> toJson() {
@override
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)
@override
int get hashCode => Object.hash(runtimeType,name);
int get hashCode => Object.hash(runtimeType,name,companyId,address);
@override
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;
@override @useResult
$Res call({
String? name
String? name,@JsonKey(name: 'company_id') String? companyId, OrganizationAddress? address
});
@override $OrganizationAddressCopyWith<$Res>? get address;
}
/// @nodoc
@@ -264,14 +280,28 @@ class __$OrganizationCopyWithImpl<$Res>
/// Create a copy of Organization
/// 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(
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
@@ -7,7 +7,20 @@ part of 'organization.dart';
// **************************************************************************
_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) =>
<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,
};
@@ -2,11 +2,18 @@
import 'package:freezed_annotation/freezed_annotation.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';
part 'tender_data.freezed.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
abstract class TenderData with _$TenderData {
const factory TenderData({
@@ -16,6 +23,13 @@ abstract class TenderData with _$TenderData {
@JsonKey(name: 'tender_id') required String? tenderId,
@JsonKey(name: 'notice_publication_id')
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)
required int? publicationDate,
required String? title,
@@ -37,6 +51,9 @@ abstract class TenderData with _$TenderData {
@JsonKey(name: 'duration_unit') required String? durationUnit,
@JsonKey(name: 'buyer_organization')
required Organization? buyerOrganization,
required List<Lot>? lots,
@JsonKey(name: 'official_languages')
required List<String>? officialLanguages,
required String? status,
}) = _TenderData;
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
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
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $TenderDataCopyWith<TenderData> get copyWith => _$TenderDataCopyWithImpl<TenderD
@override
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)
@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
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;
@useResult
$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
/// 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(
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 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?,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 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
@@ -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?,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 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?,
));
}
@@ -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) {
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();
}
@@ -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) {
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');
}
@@ -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) {
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;
}
@@ -241,7 +248,7 @@ return $default(_that.success,_that.message,_that.id,_that.tenderId,_that.notice
@JsonSerializable()
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);
@override final bool? success;
@@ -249,7 +256,12 @@ class _TenderData implements TenderData {
@override final String? id;
@override@JsonKey(name: 'tender_id') final String? tenderId;
@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? description;
@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 final String? currency;
@override final String? duration;
@override@JsonKey(name: 'tender_deadline') final int? tenderDeadline;
@override@JsonKey(name: 'submission_deadline') final int? submissionDeadline;
@override@JsonKey(name: 'application_deadline') final int? applicationDeadline;
@override@JsonKey(name: 'tender_deadline', fromJson: unixTimestampFromJson) final int? tenderDeadline;
@override@JsonKey(name: 'submission_deadline', fromJson: unixTimestampFromJson) final int? submissionDeadline;
@override@JsonKey(name: 'application_deadline', fromJson: unixTimestampFromJson) final int? applicationDeadline;
@override@JsonKey(name: 'submission_url') final String? submissionUrl;
@override@JsonKey(name: 'country_code') final String? countryCode;
@override@JsonKey(name: 'duration_unit') final String? durationUnit;
@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;
/// Create a copy of TenderData
@@ -279,16 +309,16 @@ Map<String, dynamic> toJson() {
@override
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)
@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
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;
@override @useResult
$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
/// 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(
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 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?,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 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
@@ -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?,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 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?,
));
}
@@ -12,6 +12,11 @@ _TenderData _$TenderDataFromJson(Map<String, dynamic> json) => _TenderData(
id: json['id'] as String?,
tenderId: json['tender_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']),
title: json['title'] as String?,
description: json['description'] as String?,
@@ -32,6 +37,14 @@ _TenderData _$TenderDataFromJson(Map<String, dynamic> json) => _TenderData(
: Organization.fromJson(
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?,
);
@@ -42,6 +55,11 @@ Map<String, dynamic> _$TenderDataToJson(_TenderData instance) =>
'id': instance.id,
'tender_id': instance.tenderId,
'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,
'title': instance.title,
'description': instance.description,
@@ -57,5 +75,7 @@ Map<String, dynamic> _$TenderDataToJson(_TenderData instance) =>
'country_code': instance.countryCode,
'duration_unit': instance.durationUnit,
'buyer_organization': instance.buyerOrganization,
'lots': instance.lots,
'official_languages': instance.officialLanguages,
'status': instance.status,
};
+33
View File
@@ -11,6 +11,38 @@
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 {
const $AssetsIconsGen();
@@ -301,6 +333,7 @@ class $AssetsSvgsGen {
class Assets {
const Assets._();
static const $AssetsFontsGen fonts = $AssetsFontsGen();
static const $AssetsIconsGen icons = $AssetsIconsGen();
static const $AssetsPngsGen pngs = $AssetsPngsGen();
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';
}
+18 -4
View File
@@ -104,10 +104,24 @@ class BoardViewModel with ChangeNotifier {
newOrder: newOrder,
);
if (result is Error<MoveCardResponse>) {
_board = _board!.copyWith(columns: previousColumns);
notifyListeners();
}
switch (result) {
case Ok<MoveCardResponse>():
// 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;
}
_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;
}
}
}
+1 -93
View File
@@ -39,42 +39,12 @@ class TendersViewModel with ChangeNotifier {
static const int _pageSize = 10;
// --- Offset pagination state ---
// --- Infinite-scroll offset state ---
int _currentOffset = 0;
int _lastValidPages = 1;
int _lastValidLimit = _pageSize;
bool _hasMoreData = true;
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
DateTimeRange? selectedDateRange;
String selectedStatus = 'All';
@@ -233,12 +203,6 @@ class TendersViewModel with ChangeNotifier {
case Ok<TendersResponse>():
_tendersResponse = result.value;
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) {
_currentOffset = meta!.offset!;
}
@@ -299,12 +263,6 @@ class TendersViewModel with ChangeNotifier {
data: result.value.data?.copyWith(tenders: combined),
);
final newMeta = result.value.meta;
if (newMeta?.pages != null && newMeta!.pages! > 0) {
_lastValidPages = newMeta.pages!;
}
if (newMeta?.limit != null && newMeta!.limit! > 0) {
_lastValidLimit = newMeta.limit!;
}
_hasMoreData =
newMeta?.hasMore ?? (newTenders.length >= _pageSize);
} else {
@@ -492,54 +450,4 @@ class TendersViewModel with ChangeNotifier {
sortOrder = '';
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);
}
}
@@ -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/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_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
import '../../../core/constants/tender_approval_status.dart';
@@ -121,8 +120,6 @@ class _TenderDetailDesktopPageState extends State<TenderDetailDesktopPage> {
detail: detail,
),
SizedBox(height: 32.0.h()),
TenderDetailCard(detail: detail),
SizedBox(height: 24.0.h()),
TenderDetailActions(
isScreenBig: true,
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/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_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
import 'package:tm_app/views/shared/tender_app_bar.dart';
@@ -112,8 +111,6 @@ class _TenderDetailMobilePageState extends State<TenderDetailMobilePage> {
children: [
TenderDetailHeader(isScreenBig: false, detail: detail),
SizedBox(height: 24.0.h()),
TenderDetailCard(detail: detail),
SizedBox(height: 24.0.h()),
TenderDetailActions(
isScreenBig: false,
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/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_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.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()),
TenderDetailHeader(isScreenBig: true, detail: detail),
SizedBox(height: 28.0.h()),
TenderDetailCard(detail: detail),
SizedBox(height: 24.0.h()),
TenderDetailActions(
isScreenBig: true,
detail: detail,
@@ -42,4 +42,35 @@ class TenderDetailsStrings {
static const String requestSuccessfullyRegistered =
'Your request has been successfully registered.';
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/view_models/auth_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/select_submission_bottom_sheet.dart';
import '../../../core/constants/tender_approval_status.dart';
import '../../../core/constants/tender_submision_mode.dart';
import '../../../core/routes/app_routes.dart';
import '../../../core/utils/app_toast.dart';
import '../../../data/services/model/tender_data/tender_data.dart';
import '../../shared/select_submission_dialog.dart';
@@ -68,38 +65,10 @@ class _TenderDetailActionsState extends State<TenderDetailActions> {
builder: (context) {
return SelectSubmissionBottomSheet(
onConfirm: (String value) {
if (value ==
TenderSubmissionMode.selfApply.value) {
// viewModel.submitTenderApproval(
// tenderId: widget.detail.id!,
// 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,
// );
},
);
},
);
}
});
}
// Confirming the submission modal shows a
// confirmation toast (no API call, no
// navigation), per product request.
_showSubmissionReceivedToast();
},
);
},
@@ -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) {
return Row(
children: [
@@ -206,33 +185,7 @@ class _TenderDetailActionsState extends State<TenderDetailActions> {
builder: (context) {
return SelectSubmissionDialog(
onConfirm: (String value) {
if (value ==
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) {},
);
},
);
}
});
}
_showSubmissionReceivedToast();
},
);
},
@@ -242,41 +195,7 @@ class _TenderDetailActionsState extends State<TenderDetailActions> {
builder: (context) {
return SelectSubmissionBottomSheet(
onConfirm: (String value) {
if (value ==
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);
// }
// },
// );
},
);
}
_showSubmissionReceivedToast();
},
);
},
@@ -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 '../../shared/flag.dart';
import '../strings/tender_details_strings.dart';
import 'detail_drop_down.dart';
class TenderDetailHeader extends StatelessWidget {
final bool isScreenBig;
@@ -41,44 +40,17 @@ class TenderDetailHeader extends StatelessWidget {
SizedBox(height: 16.0.h()),
_locationBudgetRow(),
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),
TenderDetailInfoSection(isScreenBig: isScreenBig, detail: detail),
SizedBox(height: 24.0.h()),
_overViewTitle(),
SizedBox(height: 12.0.h()),
_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() {
return Text(
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() {
return Row(
children: [
@@ -1,10 +1,19 @@
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/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';
import 'package:tm_app/views/detail/widgets/deadline_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 {
final bool isScreenBig;
final TenderData detail;
@@ -17,24 +26,114 @@ class TenderDetailInfoSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
final buyer = detail.buyerOrganization;
final address = buyer?.address;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DeadlineItem(detail: detail, isScreenBig: isScreenBig),
InfoItem(
title: TenderDetailsStrings.tenderClientLabel,
value: detail.buyerOrganization!.name ?? '',
..._infoItem(TenderDetailsStrings.noticeTypeCode, detail.noticeTypeCode),
..._infoItem(TenderDetailsStrings.formType, detail.formType),
..._infoItem(
TenderDetailsStrings.noticeLanguageCode,
detail.noticeLanguageCode,
),
InfoItem(
title: TenderDetailsStrings.estimatedValue,
value:
'${detail.estimatedValue?.formattedPrice} ${detail.currency ?? ''}',
..._infoItem(
TenderDetailsStrings.issueDate,
_date(detail.issueDate),
),
InfoItem(
title: TenderDetailsStrings.tenderReferenceNumberLabel,
value: detail.noticePublicationId ?? '',
..._infoItem(
TenderDetailsStrings.issueTime,
_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);
}
}
@@ -87,25 +87,9 @@ class DesktopNavigationWidget extends StatelessWidget {
iconPath: AssetsManager.tenders,
activeIconPath: AssetsManager.tendersActive,
),
const SizedBox(width: 24),
_navigationItem(
context: context,
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,
),
// Board tab is intentionally hidden from desktop navigation. The
// Board route/page itself remains intact; the nav item (at tab index
// 2) can be restored from git history when the feature is re-enabled.
const SizedBox(width: 24),
_navigationItem(
context: context,
+1 -17
View File
@@ -13,7 +13,6 @@ import 'package:tm_app/views/tenders/strings/tenders_strings.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';
@@ -78,13 +77,9 @@ class _DesktopTendersPageState extends State<DesktopTendersPage> {
return const Center(child: Text(CommonStrings.noData));
}
return Column(
children: [
Expanded(
child: SingleChildScrollView(
return Center(
child: SizedBox(
width: 740,
height: 800,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 4.0.h()),
child: MainTendersSlider(
@@ -93,17 +88,6 @@ class _DesktopTendersPageState extends State<DesktopTendersPage> {
),
),
),
),
),
SizedBox(
width: 740,
child: WindowedPagination(
currentPage: vm.currentPageIndex,
totalPages: vm.totalPages,
onPageChanged: vm.handlePaginationChange,
),
),
],
);
},
),
+2 -19
View File
@@ -9,7 +9,6 @@ import '../../../core/utils/app_toast.dart';
import '../../shared/tender_app_bar.dart';
import '../strings/tenders_strings.dart';
import '../widgets/main_tenders_slider.dart';
import '../widgets/windowed_pagination.dart';
class MobileTendersPage extends StatefulWidget {
const MobileTendersPage({super.key});
@@ -62,29 +61,13 @@ class _MobileTendersPageState extends State<MobileTendersPage> {
return const Center(child: Text(CommonStrings.noData));
}
return Column(
children: [
Expanded(
child: SingleChildScrollView(
return SingleChildScrollView(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 18.0.h()),
child: MainTendersSlider(
key: ValueKey(
'tenders-slider-${viewModel.currentPageIndex}',
),
tenders:
viewModel.tendersResponse?.data?.tenders ?? [],
tenders: viewModel.tendersResponse?.data?.tenders ?? [],
),
),
),
),
WindowedPagination(
currentPage: viewModel.currentPageIndex,
totalPages: viewModel.totalPages,
onPageChanged: viewModel.handlePaginationChange,
compact: true,
),
],
);
},
),
+1 -16
View File
@@ -5,7 +5,6 @@ import 'package:tm_app/core/utils/size_config.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/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';
@@ -54,30 +53,16 @@ class _TabletTendersPageState extends State<TabletTendersPage> {
viewModel.isLoading == false) {
return const Center(child: Text(CommonStrings.noData));
}
return Column(
children: [
Expanded(
child: SingleChildScrollView(
return SingleChildScrollView(
child: Padding(
padding: EdgeInsetsDirectional.only(
end: 24.0.w(),
top: 128.0.h(),
),
child: MainTendersSlider(
key: ValueKey(
'tenders-slider-${viewModel.currentPageIndex}',
),
tenders: viewModel.tendersResponse?.data?.tenders ?? [],
),
),
),
),
WindowedPagination(
currentPage: viewModel.currentPageIndex,
totalPages: viewModel.totalPages,
onPageChanged: viewModel.handlePaginationChange,
),
],
);
},
),
@@ -25,8 +25,46 @@ class MainTendersSlider extends StatefulWidget {
class _MainTendersSliderState extends State<MainTendersSlider> {
final PageController pageController = PageController(initialPage: 0);
final ScrollController _scrollController = ScrollController();
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);
}
}
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() {
if (currentPage == 1) {
return;
@@ -82,6 +120,7 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
@override
void dispose() {
pageController.dispose();
_scrollController.dispose();
super.dispose();
}
@@ -106,6 +145,10 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
controller: pageController,
itemCount: widget.tenders.length,
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(() {
currentPage = index + 1;
});
@@ -115,6 +158,15 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
if (!viewModel.hasFeedbackForTender(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) {
final tender = widget.tenders[index];
@@ -230,6 +282,7 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
children: [
Expanded(
child: ListView.separated(
controller: _scrollController,
padding: EdgeInsets.all(16.0.h()),
itemCount:
widget.tenders.length +
@@ -8,7 +8,6 @@ import '../../../core/constants/assets.dart';
import '../../../core/theme/colors.dart';
import '../../../core/utils/date_utils.dart';
import '../../../core/utils/size_config.dart';
import '../../detail/strings/tender_details_strings.dart';
import '../../shared/flag.dart';
import '../strings/tenders_strings.dart';
import 'tender_action_buttons_row.dart';
@@ -83,14 +82,6 @@ class TenderCard extends StatelessWidget {
),
child: Column(
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
Row(
children: [
@@ -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() {
return Expanded(
child: Row(
@@ -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,
),
),
);
}
}
@@ -0,0 +1,166 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tm_app/core/constants/pref_keys.dart';
import 'package:tm_app/core/network/auth_interceptor.dart';
import 'package:tm_app/core/utils/logger.dart';
/// A configurable [HttpClientAdapter] that lets each test decide, per request,
/// what the wire should return. Avoids a mocking dependency the repo doesn't
/// pull in while still exercising the real Dio request/replay machinery.
class _FakeAdapter implements HttpClientAdapter {
_FakeAdapter(this.responder);
Future<ResponseBody> Function(RequestOptions options) responder;
int requestCount = 0;
@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future<void>? cancelFuture,
) {
requestCount++;
return responder(options);
}
@override
void close({bool force = false}) {}
}
ResponseBody _json(int status, [Map<String, dynamic> body = const {}]) {
return ResponseBody.fromString(
jsonEncode(body),
status,
headers: <String, List<String>>{
Headers.contentTypeHeader: <String>[Headers.jsonContentType],
},
);
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
// The interceptor logs through the app-wide singleton, which is `late`-init.
appLogger.init();
late Dio dio;
late SharedPreferences prefs;
late int refreshCalls;
late int logoutCalls;
setUp(() async {
SharedPreferences.setMockInitialValues(<String, Object>{
PrefKeys.bearer: 'old-token',
PrefKeys.refreshToken: 'old-refresh',
});
prefs = await SharedPreferences.getInstance();
refreshCalls = 0;
logoutCalls = 0;
dio = Dio(BaseOptions(baseUrl: 'https://example.test'));
});
/// Refresh that succeeds: rotates the stored bearer and returns the new token,
/// mirroring the production refresh which persists before replaying.
Future<String?> refreshSucceeds() async {
refreshCalls++;
// A small delay so concurrent 401s both register on the single-flight
// future before it resolves, exercising the coalescing path for real.
await Future<void>.delayed(const Duration(milliseconds: 10));
await prefs.setString(PrefKeys.bearer, 'new-token');
return 'new-token';
}
void attachInterceptor({Future<String?> Function()? refreshToken}) {
dio.interceptors.add(
AuthInterceptor(
dio: dio,
prefs: prefs,
refreshToken: refreshToken ?? refreshSucceeds,
onAuthFailed: () => () async {
logoutCalls++;
await prefs.clear();
},
),
);
}
test(
'retry guard: a replay that still 401s does not trigger a second refresh',
() async {
// Every request — original and replay — comes back 401.
dio.httpClientAdapter = _FakeAdapter(
(RequestOptions options) async => _json(401, <String, dynamic>{
'message': 'unauthorized',
}),
);
attachInterceptor();
await expectLater(
dio.get<dynamic>('/api/v1/protected'),
throwsA(isA<DioException>()),
);
// The refresh + replay happens exactly once; the second 401 short-circuits
// via the auth_retried flag instead of refreshing again.
expect(refreshCalls, 1);
expect(logoutCalls, 1);
},
);
test('single-flight: concurrent 401s share a single refresh', () async {
// First attempt 401s; the tagged replay succeeds.
dio.httpClientAdapter = _FakeAdapter((RequestOptions options) async {
final bool isReplay = options.extra['auth_retried'] == true;
return _json(isReplay ? 200 : 401, <String, dynamic>{'ok': isReplay});
});
attachInterceptor();
final List<Response<dynamic>> responses = await Future.wait(
<Future<Response<dynamic>>>[
dio.get<dynamic>('/api/v1/a'),
dio.get<dynamic>('/api/v1/b'),
],
);
expect(refreshCalls, 1);
expect(logoutCalls, 0);
expect(
responses.every((Response<dynamic> r) => r.statusCode == 200),
isTrue,
);
});
test(
'non-401 replay failure propagates and does not log the user out',
() async {
// Refresh succeeds, but the replay hits a transient/server error (a 500
// here stands in for the timeout/connection-drop case): authentication is
// fine, so the error must surface to the caller rather than wipe the
// session.
dio.httpClientAdapter = _FakeAdapter((RequestOptions options) async {
final bool isReplay = options.extra['auth_retried'] == true;
return _json(isReplay ? 500 : 401);
});
attachInterceptor();
await expectLater(
dio.get<dynamic>('/api/v1/protected'),
throwsA(
isA<DioException>().having(
(DioException e) => e.response?.statusCode,
'replay status code',
500,
),
),
);
expect(refreshCalls, 1);
expect(logoutCalls, 0);
// Session left intact for the retry — credentials were not cleared.
expect(prefs.getString(PrefKeys.bearer), 'new-token');
},
);
}
+18
View File
@@ -58,6 +58,11 @@ void main() {
id: 'TND123',
tenderId: 'TND123',
noticePublicationId: 'NP123',
noticeTypeCode: 'cn-standard',
formType: 'competition',
noticeLanguageCode: 'ENG',
issueDate: 1700000000,
issueTime: 1700000000,
publicationDate: 1700000000,
title: 'Tender Title',
description: 'Some description',
@@ -74,7 +79,11 @@ void main() {
durationUnit: 'month',
buyerOrganization: Organization(
name: 'Test Organization',
companyId: 'ORG001',
address: null,
),
lots: null,
officialLanguages: null,
status: 'active',
);
@@ -114,6 +123,11 @@ void main() {
id: 'TND001',
tenderId: 'TND001',
noticePublicationId: 'NP1',
noticeTypeCode: 'cn-standard',
formType: 'competition',
noticeLanguageCode: 'ENG',
issueDate: 1700000000,
issueTime: 1700000000,
publicationDate: 1700000000,
title: 'Old Tender',
description: 'Old desc',
@@ -130,7 +144,11 @@ void main() {
durationUnit: 'month',
buyerOrganization: Organization(
name: 'Old Org',
companyId: 'ORG001',
address: null,
),
lots: null,
officialLanguages: null,
status: 'inactive',
),
updatedAt: 1750000000,
+158
View File
@@ -0,0 +1,158 @@
import 'package:flutter_test/flutter_test.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_address.dart';
import 'package:tm_app/data/services/model/tender_data/tender_data.dart';
void main() {
group('Lot', () {
test('fromJson maps lot_id/title/description', () {
final lot = Lot.fromJson(const {
'lot_id': 'LOT-1',
'title': 'Lot title',
'description': 'Lot description',
});
expect(lot.lotId, 'LOT-1');
expect(lot.title, 'Lot title');
expect(lot.description, 'Lot description');
});
test('toJson round-trips back to the same values', () {
const lot = Lot(lotId: 'LOT-2', title: 'T', description: 'D');
expect(Lot.fromJson(lot.toJson()), lot);
});
test('missing fields parse as null (resilient to partial payloads)', () {
final lot = Lot.fromJson(const {});
expect(lot.lotId, isNull);
expect(lot.title, isNull);
expect(lot.description, isNull);
});
});
group('OrganizationAddress', () {
test('fromJson maps city_name/country_code', () {
final address = OrganizationAddress.fromJson(const {
'city_name': 'Amsterdam',
'country_code': 'NL',
});
expect(address.cityName, 'Amsterdam');
expect(address.countryCode, 'NL');
});
test('toJson round-trips back to the same values', () {
const address = OrganizationAddress(
cityName: 'Berlin',
countryCode: 'DE',
);
expect(OrganizationAddress.fromJson(address.toJson()), address);
});
test('nested under Organization.fromJson', () {
final org = Organization.fromJson(const {
'name': 'Buyer Org',
'company_id': 'COMP-1',
'address': {'city_name': 'Paris', 'country_code': 'FR'},
});
expect(org.name, 'Buyer Org');
expect(org.companyId, 'COMP-1');
expect(org.address?.cityName, 'Paris');
expect(org.address?.countryCode, 'FR');
});
});
group('TenderData new fields', () {
Map<String, Object?> baseJson({
Object? issueDate,
Object? issueTime,
}) => {
'success': true,
'message': 'ok',
'id': 'TND-1',
'tender_id': 'TND-1',
'notice_publication_id': 'NP-1',
'notice_type_code': 'NT-1',
'form_type': 'open',
'notice_language_code': 'en',
'issue_date': issueDate,
'issue_time': issueTime,
'publication_date': 1700000000,
'title': 'Title',
'description': 'Desc',
'procurement_type_code': 'PT-1',
'procedure_code': 'PRC-1',
'estimated_value': 1000,
'currency': 'EUR',
'duration': '12',
'tender_deadline': 1710000000,
'submission_deadline': 1710000000,
'application_deadline': 1710000000,
'submission_url': 'https://example.com',
'country_code': 'NL',
'duration_unit': 'month',
'buyer_organization': {
'name': 'Buyer Org',
'company_id': 'COMP-1',
'address': {'city_name': 'Amsterdam', 'country_code': 'NL'},
},
'lots': [
{'lot_id': 'LOT-1', 'title': 'Lot title', 'description': 'Lot desc'},
],
'official_languages': ['en', 'nl'],
'status': 'active',
};
test('parses notice/form/language, lots and official_languages', () {
final tender = TenderData.fromJson(baseJson(
issueDate: 1700000000,
issueTime: 1700003600,
));
expect(tender.noticeTypeCode, 'NT-1');
expect(tender.formType, 'open');
expect(tender.noticeLanguageCode, 'en');
expect(tender.officialLanguages, ['en', 'nl']);
expect(tender.lots, hasLength(1));
expect(tender.lots?.first.lotId, 'LOT-1');
expect(tender.buyerOrganization?.address?.cityName, 'Amsterdam');
});
test('issue_date/issue_time accept epoch seconds', () {
final tender = TenderData.fromJson(baseJson(
issueDate: 1700000000,
issueTime: 1700003600,
));
expect(tender.issueDate, 1700000000);
expect(tender.issueTime, 1700003600);
});
test('issue_date/issue_time accept millisecond and string inputs', () {
final tender = TenderData.fromJson(baseJson(
issueDate: '1700000000',
issueTime: 1700000000000,
));
// unixTimestampFromJson normalizes the raw value; the >1e10 ms value is
// kept as-is here (timeConvertor downscales it at display time).
expect(tender.issueDate, 1700000000);
expect(tender.issueTime, 1700000000000);
});
test('non-positive or missing issue_date/issue_time become null', () {
final tender = TenderData.fromJson(baseJson(
issueDate: 0,
issueTime: null,
));
expect(tender.issueDate, isNull);
expect(tender.issueTime, isNull);
});
});
}