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>
This commit is contained in:
@@ -20,7 +20,5 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '12.0'
|
||||
# platform :ios, '13.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -55,6 +56,7 @@
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -62,6 +64,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -79,6 +82,7 @@
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
@@ -142,6 +146,9 @@
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
@@ -165,6 +172,9 @@
|
||||
|
||||
/* Begin PBXProject section */
|
||||
97C146E61CF9000F007C117D /* Project object */ = {
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
@@ -346,7 +356,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -472,7 +482,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -523,7 +533,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -611,6 +621,18 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "abseil-cpp-binary",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/abseil-cpp-binary.git",
|
||||
"state" : {
|
||||
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
|
||||
"version" : "1.2024072200.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "app-check",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/app-check.git",
|
||||
"state" : {
|
||||
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
|
||||
"version" : "11.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkcamera",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkimagepickercontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||
"state" : {
|
||||
"branch" : "4.3.9",
|
||||
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkphotogallery",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "firebase-ios-sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/firebase-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "8d5b4189f1f482df8d5c58c9985ea70491ef5382",
|
||||
"version" : "12.14.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "google-ads-on-device-conversion-ios-sdk",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/googleads/google-ads-on-device-conversion-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "9bfcc6cf435b2e7c5562c1900b8680c594fa9a64",
|
||||
"version" : "3.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googleappmeasurement",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
||||
"state" : {
|
||||
"revision" : "219e564a8510e983e675c94f77f7f7c50049f22d",
|
||||
"version" : "12.14.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googledatatransport",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleDataTransport.git",
|
||||
"state" : {
|
||||
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
|
||||
"version" : "10.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "googleutilities",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleUtilities.git",
|
||||
"state" : {
|
||||
"revision" : "60da361632d0de02786f709bdc0c4df340f7613e",
|
||||
"version" : "8.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "grpc-binary",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/grpc-binary.git",
|
||||
"state" : {
|
||||
"revision" : "75b31c842f664a0f46a2e590a570e370249fd8f6",
|
||||
"version" : "1.69.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "gtm-session-fetcher",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/gtm-session-fetcher.git",
|
||||
"state" : {
|
||||
"revision" : "c0ac7575d70050c2973ba2318bd5af47f8e8153a",
|
||||
"version" : "5.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "interop-ios-for-google-sdks",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
|
||||
"state" : {
|
||||
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
|
||||
"version" : "101.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "leveldb",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/leveldb.git",
|
||||
"state" : {
|
||||
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
|
||||
"version" : "1.22.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "nanopb",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/nanopb.git",
|
||||
"state" : {
|
||||
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
|
||||
"version" : "2.30910.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "promises",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/promises.git",
|
||||
"state" : {
|
||||
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
|
||||
"version" : "2.4.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sdwebimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||
"state" : {
|
||||
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||
"version" : "5.21.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftygif",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||
"state" : {
|
||||
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||
"version" : "5.4.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tocropviewcontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||
"state" : {
|
||||
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||
"version" : "2.8.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
@@ -5,6 +5,24 @@
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -26,6 +44,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
@@ -54,6 +73,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
||||
@@ -2,12 +2,15 @@ import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
}
|
||||
}
|
||||
|
||||
+25
-4
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -24,6 +26,29 @@
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>FlutterSceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
@@ -41,9 +66,5 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -121,6 +121,7 @@ List<SingleChildWidget> get repositories {
|
||||
(context) => HomeRepository(
|
||||
homeService: context.read(),
|
||||
yourTendersService: context.read(),
|
||||
likedTendersService: context.read(),
|
||||
),
|
||||
lazy: true,
|
||||
),
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
enum TaskStatus { toDo, inProgress, done }
|
||||
|
||||
extension TaskStatusExtension on TaskStatus {
|
||||
// تبدیل enum به string
|
||||
String get value => name;
|
||||
|
||||
// تبدیل string به enum
|
||||
static TaskStatus fromString(String? status) {
|
||||
if (status == null) {
|
||||
return TaskStatus.toDo;
|
||||
}
|
||||
|
||||
return TaskStatus.values.firstWhere(
|
||||
(e) => e.name == status,
|
||||
orElse: () => TaskStatus.toDo,
|
||||
);
|
||||
}
|
||||
|
||||
String get displayNameEn {
|
||||
switch (this) {
|
||||
case TaskStatus.toDo:
|
||||
return 'To Do';
|
||||
case TaskStatus.inProgress:
|
||||
return 'In Progress';
|
||||
case TaskStatus.done:
|
||||
return 'Done';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import '../../data/services/model/board_response/board_response.dart';
|
||||
import '../enums/task_status.dart';
|
||||
|
||||
extension BoardDataExtension on BoardData {
|
||||
// تبدیل status string به TaskStatus enum
|
||||
TaskStatus get statusEnum => TaskStatusExtension.fromString(status);
|
||||
|
||||
// کپی BoardData با status جدید
|
||||
BoardData copyWithStatus(TaskStatus newStatus) {
|
||||
return copyWith(status: newStatus.value);
|
||||
}
|
||||
}
|
||||
@@ -250,11 +250,14 @@ class FinalCompletionOfDocumentsRouteData extends GoRouteData
|
||||
@TypedGoRoute<CompletionOfDocumentsRouteData>(path: '/completion_of_documents')
|
||||
class CompletionOfDocumentsRouteData extends GoRouteData
|
||||
with _$CompletionOfDocumentsRouteData {
|
||||
const CompletionOfDocumentsRouteData();
|
||||
final String? tenderId;
|
||||
const CompletionOfDocumentsRouteData({this.tenderId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const CompletionOfDocumentsScreen();
|
||||
return tenderDetailProvider(
|
||||
child: CompletionOfDocumentsScreen(tenderId: tenderId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -308,10 +308,20 @@ RouteBase get $completionOfDocumentsRouteData => GoRouteData.$route(
|
||||
|
||||
mixin _$CompletionOfDocumentsRouteData on GoRouteData {
|
||||
static CompletionOfDocumentsRouteData _fromState(GoRouterState state) =>
|
||||
const CompletionOfDocumentsRouteData();
|
||||
CompletionOfDocumentsRouteData(
|
||||
tenderId: state.uri.queryParameters['tender-id'],
|
||||
);
|
||||
|
||||
CompletionOfDocumentsRouteData get _self =>
|
||||
this as CompletionOfDocumentsRouteData;
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location('/completion_of_documents');
|
||||
String get location => GoRouteData.$location(
|
||||
'/completion_of_documents',
|
||||
queryParams: {
|
||||
if (_self.tenderId != null) 'tender-id': _self.tenderId!,
|
||||
},
|
||||
);
|
||||
|
||||
@override
|
||||
void go(BuildContext context) => context.go(location);
|
||||
|
||||
@@ -1,9 +1,41 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
String timeConvertor(int timestamp) {
|
||||
// Convert seconds → milliseconds
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
||||
/// Parses an API timestamp field that may arrive as int, double, or string.
|
||||
/// Returns null for missing, empty, unparseable, or non-positive values
|
||||
/// (a non-positive epoch is treated as "no value" by the API).
|
||||
int? unixTimestampFromJson(dynamic value) {
|
||||
if (value == null) return null;
|
||||
int? parsed;
|
||||
if (value is int) {
|
||||
parsed = value;
|
||||
} else if (value is num) {
|
||||
parsed = value.toInt();
|
||||
} else if (value is String) {
|
||||
final trimmed = value.trim();
|
||||
if (trimmed.isEmpty) return null;
|
||||
parsed = int.tryParse(trimmed);
|
||||
}
|
||||
if (parsed == null || parsed <= 0) return null;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// Format to yyyy-MM-dd
|
||||
/// Converts a Unix timestamp from the API to a display string.
|
||||
///
|
||||
/// The API contract is seconds since epoch, but some legacy fields still
|
||||
/// emit milliseconds. Anything past the year 2286 in seconds (>1e10) is
|
||||
/// assumed to be milliseconds and converted down. Remove this branch once
|
||||
/// every endpoint has been migrated to seconds.
|
||||
String timeConvertor(int? unix, {bool hasTime = false}) {
|
||||
if (unix == null || unix <= 0) return '-';
|
||||
|
||||
final seconds = unix > 9999999999 ? unix ~/ 1000 : unix;
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(
|
||||
seconds * 1000,
|
||||
isUtc: true,
|
||||
).toLocal();
|
||||
|
||||
if (hasTime) {
|
||||
return DateFormat('yyyy-MM-dd HH:mm').format(date);
|
||||
}
|
||||
return DateFormat('yyyy-MM-dd').format(date);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,17 @@ class BoardRepository {
|
||||
BoardRepository({required BoardService boardService})
|
||||
: _boardService = boardService;
|
||||
|
||||
Future<Result<BoardResponse>> getBoard() async {
|
||||
return _boardService.getBoard();
|
||||
Future<Result<BoardResponse>> getBoard() => _boardService.getBoard();
|
||||
|
||||
Future<Result<MoveCardResponse>> moveCard({
|
||||
required String cardId,
|
||||
required String columnId,
|
||||
required int newOrder,
|
||||
}) {
|
||||
return _boardService.moveCard(
|
||||
cardId: cardId,
|
||||
columnId: columnId,
|
||||
newOrder: newOrder,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import 'package:tm_app/core/utils/result.dart';
|
||||
import 'package:tm_app/data/services/home_service.dart';
|
||||
import 'package:tm_app/data/services/liked_tenders_service.dart';
|
||||
import 'package:tm_app/data/services/model/recommended_tenders_response/recommended_tenders_response.dart';
|
||||
import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart';
|
||||
import 'package:tm_app/data/services/model/tender_approvals_stats_response/tender_approvals_stats_response.dart';
|
||||
|
||||
import '../services/model/feedback_stats_response/feedback_stat_response.dart';
|
||||
import '../services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart';
|
||||
import '../services/model/request_models/tender_feedback_request_model/tender_feedback_request_model.dart';
|
||||
import '../services/your_tenders_service.dart';
|
||||
|
||||
class HomeRepository {
|
||||
HomeRepository({
|
||||
required HomeService homeService,
|
||||
required YourTendersService yourTendersService,
|
||||
required LikedTendersService likedTendersService,
|
||||
}) : _homeService = homeService,
|
||||
_yourTendersService = yourTendersService;
|
||||
_yourTendersService = yourTendersService,
|
||||
_likedTendersService = likedTendersService;
|
||||
final HomeService _homeService;
|
||||
final YourTendersService _yourTendersService;
|
||||
final LikedTendersService _likedTendersService;
|
||||
|
||||
Future<Result<RecommendedTendersResponse>> getHomeRecommendedTenders({
|
||||
required int limit,
|
||||
@@ -34,11 +38,25 @@ class HomeRepository {
|
||||
return _homeService.getTendersApprovalsStats();
|
||||
}
|
||||
|
||||
Future<Result<FeedbackStatResponse>> getFeedbackStats() {
|
||||
return _homeService.getFeedbackStats();
|
||||
/// Returns how many tenders the current user has liked (not company-wide).
|
||||
Future<Result<int>> getUserLikedTendersCount() async {
|
||||
final result = await _likedTendersService.getLikedTenders(
|
||||
requestModel: TenderFeedbackRequestModel(
|
||||
feedbackType: 'like',
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
),
|
||||
);
|
||||
|
||||
switch (result) {
|
||||
case Ok(value: final response):
|
||||
return Result.ok(response.data?.meta?.total ?? 0);
|
||||
case Error(error: final error):
|
||||
return Result.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Result<Map<String, dynamic>>> checkUnreadNotifications() async {
|
||||
return _homeService.checkUnreadNotifications();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ class TendersRepository {
|
||||
|
||||
Future<Result<TendersResponse>> getTenders({
|
||||
required int limit,
|
||||
required int offset,
|
||||
int? offset,
|
||||
String? cursor,
|
||||
String? query,
|
||||
String? sortBy,
|
||||
String? sortOrder,
|
||||
@@ -23,6 +24,7 @@ class TendersRepository {
|
||||
return _tendersService.getTenders(
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
cursor: cursor,
|
||||
query: query,
|
||||
sortBy: sortBy,
|
||||
sortOrder: sortOrder,
|
||||
|
||||
@@ -4,14 +4,20 @@ class TendersApi {
|
||||
static const String tenders = '/api/v1/tenders';
|
||||
static String getTenders({
|
||||
required int limit,
|
||||
required int offset,
|
||||
int? offset,
|
||||
String? cursor,
|
||||
String? query,
|
||||
String? sortBy,
|
||||
String? sortOrder,
|
||||
String? publicationDateFrom,
|
||||
String? publicationDateTo,
|
||||
}) {
|
||||
String url = '$tenders?limit=$limit&offset=$offset';
|
||||
String url = '$tenders?limit=$limit';
|
||||
if (cursor != null && cursor.isNotEmpty) {
|
||||
url += '&cursor=${Uri.encodeComponent(cursor)}';
|
||||
} else if (offset != null) {
|
||||
url += '&offset=$offset';
|
||||
}
|
||||
if (query != null && query.isNotEmpty) {
|
||||
url += '&q=${Uri.encodeComponent(query)}';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import '../../../core/network/network_manager.dart';
|
||||
import '../../core/config/app_config.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import '../../core/network/network_manager.dart';
|
||||
import '../../core/utils/result.dart';
|
||||
import 'model/board_response/board_response.dart';
|
||||
|
||||
@@ -9,72 +10,45 @@ class BoardService {
|
||||
|
||||
final NetworkManager _networkManager;
|
||||
|
||||
static const _basePath = '/admin/v1/kanban/kanban';
|
||||
|
||||
Future<Result<BoardResponse>> getBoard() async {
|
||||
final uri = '${AppConfig.apiBaseUrl}/api/v1/board';
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
return _networkManager.makeRequest(
|
||||
'$_basePath/boards/default',
|
||||
(json) => BoardResponse.fromJson(json),
|
||||
method: 'GET',
|
||||
);
|
||||
}
|
||||
|
||||
// final result = await _networkManager.makeRequest(
|
||||
// uri,
|
||||
// (json) => BoardResponse.fromJson(json),
|
||||
// method: 'GET',
|
||||
// );
|
||||
// return result;
|
||||
|
||||
return Result.ok(
|
||||
BoardResponse(
|
||||
success: true,
|
||||
message: 'Board data fetched successfully',
|
||||
data: tasks,
|
||||
error: null,
|
||||
),
|
||||
Future<Result<MoveCardResponse>> moveCard({
|
||||
required String cardId,
|
||||
required String columnId,
|
||||
required int newOrder,
|
||||
}) async {
|
||||
final body = jsonEncode({
|
||||
'card_id': cardId,
|
||||
'column_id': columnId,
|
||||
'new_order': newOrder,
|
||||
});
|
||||
return _networkManager.makeRequest(
|
||||
'$_basePath/cards/move',
|
||||
(json) => MoveCardResponse.fromJson(json),
|
||||
method: 'PUT',
|
||||
data: body,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<BoardData> tasks = [
|
||||
BoardData(
|
||||
id: '1',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',
|
||||
deadline: DateTime(2025, 6, 15),
|
||||
assignedTo: 'Pratyush Tewari',
|
||||
isReady: false,
|
||||
status: 'toDo',
|
||||
),
|
||||
BoardData(
|
||||
id: '2',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',
|
||||
deadline: DateTime(2025, 6, 15),
|
||||
assignedTo: 'Pratyush Tewari',
|
||||
isReady: true,
|
||||
status: 'toDo',
|
||||
),
|
||||
BoardData(
|
||||
id: '3',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',
|
||||
deadline: DateTime(2025, 6, 15),
|
||||
assignedTo: 'Pratyush Tewari',
|
||||
isReady: false,
|
||||
status: 'inProgress',
|
||||
),
|
||||
BoardData(
|
||||
id: '4',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',
|
||||
deadline: DateTime(2025, 6, 15),
|
||||
assignedTo: 'Pratyush Tewari',
|
||||
isReady: true,
|
||||
status: 'inProgress',
|
||||
),
|
||||
BoardData(
|
||||
id: '5',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',
|
||||
deadline: DateTime(2025, 6, 15),
|
||||
assignedTo: 'Pratyush Tewari',
|
||||
isReady: false,
|
||||
status: 'done',
|
||||
),
|
||||
];
|
||||
class MoveCardResponse {
|
||||
MoveCardResponse({required this.success, required this.message});
|
||||
|
||||
final bool? success;
|
||||
final String? message;
|
||||
|
||||
factory MoveCardResponse.fromJson(Map<String, dynamic> json) {
|
||||
return MoveCardResponse(
|
||||
success: json['success'] as bool?,
|
||||
message: json['message'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,229 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../../../core/utils/date_utils.dart';
|
||||
import '../error/error_model.dart';
|
||||
|
||||
part 'board_response.freezed.dart';
|
||||
part 'board_response.g.dart';
|
||||
class BoardResponse {
|
||||
BoardResponse({
|
||||
required this.success,
|
||||
required this.message,
|
||||
required this.data,
|
||||
required this.error,
|
||||
});
|
||||
|
||||
@freezed
|
||||
abstract class BoardResponse with _$BoardResponse {
|
||||
const factory BoardResponse({
|
||||
required bool? success,
|
||||
required String? message,
|
||||
required List<BoardData>? data,
|
||||
required ErrorModel? error,
|
||||
}) = _BoardResponse;
|
||||
final bool? success;
|
||||
final String? message;
|
||||
final BoardDetailData? data;
|
||||
final ErrorModel? error;
|
||||
|
||||
factory BoardResponse.fromJson(Map<String, Object?> json) =>
|
||||
_$BoardResponseFromJson(json);
|
||||
factory BoardResponse.fromJson(Map<String, dynamic> json) {
|
||||
final dataJson = json['data'];
|
||||
final errorJson = json['error'];
|
||||
return BoardResponse(
|
||||
success: json['success'] as bool?,
|
||||
message: json['message'] as String?,
|
||||
data:
|
||||
dataJson is Map<String, dynamic>
|
||||
? BoardDetailData.fromJson(dataJson)
|
||||
: null,
|
||||
error:
|
||||
errorJson is Map<String, dynamic>
|
||||
? ErrorModel.fromJson(errorJson)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class BoardData with _$BoardData {
|
||||
const factory BoardData({
|
||||
required String? id,
|
||||
required String? description,
|
||||
required DateTime? deadline,
|
||||
required String? assignedTo,
|
||||
required bool? isReady,
|
||||
required String? status,
|
||||
}) = _BoardData;
|
||||
class BoardDetailData {
|
||||
BoardDetailData({required this.board});
|
||||
|
||||
factory BoardData.fromJson(Map<String, Object?> json) =>
|
||||
_$BoardDataFromJson(json);
|
||||
final Board? board;
|
||||
|
||||
factory BoardDetailData.fromJson(Map<String, dynamic> json) {
|
||||
final boardJson = json['board'];
|
||||
return BoardDetailData(
|
||||
board:
|
||||
boardJson is Map<String, dynamic> ? Board.fromJson(boardJson) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Board {
|
||||
Board({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.columns,
|
||||
required this.isDefault,
|
||||
required this.userId,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
final String? id;
|
||||
final String? name;
|
||||
final String? description;
|
||||
final List<BoardColumn> columns;
|
||||
final bool? isDefault;
|
||||
final String? userId;
|
||||
final int? createdAt;
|
||||
final int? updatedAt;
|
||||
|
||||
factory Board.fromJson(Map<String, dynamic> json) {
|
||||
final columnsJson = json['columns'];
|
||||
return Board(
|
||||
id: json['id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
description: json['description'] as String?,
|
||||
columns:
|
||||
columnsJson is List
|
||||
? columnsJson
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(BoardColumn.fromJson)
|
||||
.toList()
|
||||
: <BoardColumn>[],
|
||||
isDefault: json['is_default'] as bool?,
|
||||
userId: json['user_id'] as String?,
|
||||
createdAt: unixTimestampFromJson(json['created_at']),
|
||||
updatedAt: unixTimestampFromJson(json['updated_at']),
|
||||
);
|
||||
}
|
||||
|
||||
Board copyWith({List<BoardColumn>? columns}) {
|
||||
return Board(
|
||||
id: id,
|
||||
name: name,
|
||||
description: description,
|
||||
columns: columns ?? this.columns,
|
||||
isDefault: isDefault,
|
||||
userId: userId,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BoardColumn {
|
||||
BoardColumn({
|
||||
required this.id,
|
||||
required this.boardId,
|
||||
required this.name,
|
||||
required this.order,
|
||||
required this.color,
|
||||
required this.limit,
|
||||
required this.cards,
|
||||
required this.cardCount,
|
||||
required this.isOverLimit,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
final String? id;
|
||||
final String? boardId;
|
||||
final String? name;
|
||||
final int? order;
|
||||
final String? color;
|
||||
final int? limit;
|
||||
final List<BoardCard> cards;
|
||||
final int? cardCount;
|
||||
final bool? isOverLimit;
|
||||
final int? createdAt;
|
||||
final int? updatedAt;
|
||||
|
||||
factory BoardColumn.fromJson(Map<String, dynamic> json) {
|
||||
final cardsJson = json['cards'];
|
||||
return BoardColumn(
|
||||
id: json['id'] as String?,
|
||||
boardId: json['board_id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
order: (json['order'] as num?)?.toInt(),
|
||||
color: json['color'] as String?,
|
||||
limit: (json['limit'] as num?)?.toInt(),
|
||||
cards:
|
||||
cardsJson is List
|
||||
? cardsJson
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(BoardCard.fromJson)
|
||||
.toList()
|
||||
: <BoardCard>[],
|
||||
cardCount: (json['card_count'] as num?)?.toInt(),
|
||||
isOverLimit: json['is_over_limit'] as bool?,
|
||||
createdAt: unixTimestampFromJson(json['created_at']),
|
||||
updatedAt: unixTimestampFromJson(json['updated_at']),
|
||||
);
|
||||
}
|
||||
|
||||
BoardColumn copyWith({List<BoardCard>? cards, int? cardCount}) {
|
||||
return BoardColumn(
|
||||
id: id,
|
||||
boardId: boardId,
|
||||
name: name,
|
||||
order: order,
|
||||
color: color,
|
||||
limit: limit,
|
||||
cards: cards ?? this.cards,
|
||||
cardCount: cardCount ?? this.cardCount,
|
||||
isOverLimit: isOverLimit,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BoardCard {
|
||||
BoardCard({
|
||||
required this.id,
|
||||
required this.columnId,
|
||||
required this.tenderId,
|
||||
required this.title,
|
||||
required this.order,
|
||||
required this.priority,
|
||||
required this.labels,
|
||||
required this.dueDate,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
final String? id;
|
||||
final String? columnId;
|
||||
final String? tenderId;
|
||||
final String? title;
|
||||
final int? order;
|
||||
final String? priority;
|
||||
final List<String> labels;
|
||||
final int? dueDate;
|
||||
final int? createdAt;
|
||||
final int? updatedAt;
|
||||
|
||||
factory BoardCard.fromJson(Map<String, dynamic> json) {
|
||||
final labelsJson = json['labels'];
|
||||
return BoardCard(
|
||||
id: json['id'] as String?,
|
||||
columnId: json['column_id'] as String?,
|
||||
tenderId: json['tender_id'] as String?,
|
||||
title: json['title'] as String?,
|
||||
order: (json['order'] as num?)?.toInt(),
|
||||
priority: json['priority'] as String?,
|
||||
labels:
|
||||
labelsJson is List
|
||||
? labelsJson.whereType<String>().toList()
|
||||
: <String>[],
|
||||
dueDate: unixTimestampFromJson(json['due_date']),
|
||||
createdAt: unixTimestampFromJson(json['created_at']),
|
||||
updatedAt: unixTimestampFromJson(json['updated_at']),
|
||||
);
|
||||
}
|
||||
|
||||
BoardCard copyWith({String? columnId, int? order}) {
|
||||
return BoardCard(
|
||||
id: id,
|
||||
columnId: columnId ?? this.columnId,
|
||||
tenderId: tenderId,
|
||||
title: title,
|
||||
order: order ?? this.order,
|
||||
priority: priority,
|
||||
labels: labels,
|
||||
dueDate: dueDate,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,596 +0,0 @@
|
||||
// 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 'board_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$BoardResponse {
|
||||
|
||||
bool? get success; String? get message; List<BoardData>? get data; ErrorModel? get error;
|
||||
/// Create a copy of BoardResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$BoardResponseCopyWith<BoardResponse> get copyWith => _$BoardResponseCopyWithImpl<BoardResponse>(this as BoardResponse, _$identity);
|
||||
|
||||
/// Serializes this BoardResponse to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is BoardResponse&&(identical(other.success, success) || other.success == success)&&(identical(other.message, message) || other.message == message)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,success,message,const DeepCollectionEquality().hash(data),error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BoardResponse(success: $success, message: $message, data: $data, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $BoardResponseCopyWith<$Res> {
|
||||
factory $BoardResponseCopyWith(BoardResponse value, $Res Function(BoardResponse) _then) = _$BoardResponseCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool? success, String? message, List<BoardData>? data, ErrorModel? error
|
||||
});
|
||||
|
||||
|
||||
$ErrorModelCopyWith<$Res>? get error;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$BoardResponseCopyWithImpl<$Res>
|
||||
implements $BoardResponseCopyWith<$Res> {
|
||||
_$BoardResponseCopyWithImpl(this._self, this._then);
|
||||
|
||||
final BoardResponse _self;
|
||||
final $Res Function(BoardResponse) _then;
|
||||
|
||||
/// Create a copy of BoardResponse
|
||||
/// 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? data = freezed,Object? error = 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?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as List<BoardData>?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as ErrorModel?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of BoardResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$ErrorModelCopyWith<$Res>? get error {
|
||||
if (_self.error == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $ErrorModelCopyWith<$Res>(_self.error!, (value) {
|
||||
return _then(_self.copyWith(error: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [BoardResponse].
|
||||
extension BoardResponsePatterns on BoardResponse {
|
||||
/// 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( _BoardResponse value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _BoardResponse() 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( _BoardResponse value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _BoardResponse():
|
||||
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( _BoardResponse value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _BoardResponse() 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( bool? success, String? message, List<BoardData>? data, ErrorModel? error)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _BoardResponse() when $default != null:
|
||||
return $default(_that.success,_that.message,_that.data,_that.error);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( bool? success, String? message, List<BoardData>? data, ErrorModel? error) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _BoardResponse():
|
||||
return $default(_that.success,_that.message,_that.data,_that.error);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( bool? success, String? message, List<BoardData>? data, ErrorModel? error)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _BoardResponse() when $default != null:
|
||||
return $default(_that.success,_that.message,_that.data,_that.error);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _BoardResponse implements BoardResponse {
|
||||
const _BoardResponse({required this.success, required this.message, required final List<BoardData>? data, required this.error}): _data = data;
|
||||
factory _BoardResponse.fromJson(Map<String, dynamic> json) => _$BoardResponseFromJson(json);
|
||||
|
||||
@override final bool? success;
|
||||
@override final String? message;
|
||||
final List<BoardData>? _data;
|
||||
@override List<BoardData>? get data {
|
||||
final value = _data;
|
||||
if (value == null) return null;
|
||||
if (_data is EqualUnmodifiableListView) return _data;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
@override final ErrorModel? error;
|
||||
|
||||
/// Create a copy of BoardResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$BoardResponseCopyWith<_BoardResponse> get copyWith => __$BoardResponseCopyWithImpl<_BoardResponse>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$BoardResponseToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BoardResponse&&(identical(other.success, success) || other.success == success)&&(identical(other.message, message) || other.message == message)&&const DeepCollectionEquality().equals(other._data, _data)&&(identical(other.error, error) || other.error == error));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,success,message,const DeepCollectionEquality().hash(_data),error);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BoardResponse(success: $success, message: $message, data: $data, error: $error)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$BoardResponseCopyWith<$Res> implements $BoardResponseCopyWith<$Res> {
|
||||
factory _$BoardResponseCopyWith(_BoardResponse value, $Res Function(_BoardResponse) _then) = __$BoardResponseCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool? success, String? message, List<BoardData>? data, ErrorModel? error
|
||||
});
|
||||
|
||||
|
||||
@override $ErrorModelCopyWith<$Res>? get error;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$BoardResponseCopyWithImpl<$Res>
|
||||
implements _$BoardResponseCopyWith<$Res> {
|
||||
__$BoardResponseCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _BoardResponse _self;
|
||||
final $Res Function(_BoardResponse) _then;
|
||||
|
||||
/// Create a copy of BoardResponse
|
||||
/// 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? data = freezed,Object? error = freezed,}) {
|
||||
return _then(_BoardResponse(
|
||||
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?,data: freezed == data ? _self._data : data // ignore: cast_nullable_to_non_nullable
|
||||
as List<BoardData>?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
|
||||
as ErrorModel?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of BoardResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$ErrorModelCopyWith<$Res>? get error {
|
||||
if (_self.error == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $ErrorModelCopyWith<$Res>(_self.error!, (value) {
|
||||
return _then(_self.copyWith(error: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$BoardData {
|
||||
|
||||
String? get id; String? get description; DateTime? get deadline; String? get assignedTo; bool? get isReady; String? get status;
|
||||
/// Create a copy of BoardData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$BoardDataCopyWith<BoardData> get copyWith => _$BoardDataCopyWithImpl<BoardData>(this as BoardData, _$identity);
|
||||
|
||||
/// Serializes this BoardData to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is BoardData&&(identical(other.id, id) || other.id == id)&&(identical(other.description, description) || other.description == description)&&(identical(other.deadline, deadline) || other.deadline == deadline)&&(identical(other.assignedTo, assignedTo) || other.assignedTo == assignedTo)&&(identical(other.isReady, isReady) || other.isReady == isReady)&&(identical(other.status, status) || other.status == status));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,description,deadline,assignedTo,isReady,status);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BoardData(id: $id, description: $description, deadline: $deadline, assignedTo: $assignedTo, isReady: $isReady, status: $status)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $BoardDataCopyWith<$Res> {
|
||||
factory $BoardDataCopyWith(BoardData value, $Res Function(BoardData) _then) = _$BoardDataCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String? id, String? description, DateTime? deadline, String? assignedTo, bool? isReady, String? status
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$BoardDataCopyWithImpl<$Res>
|
||||
implements $BoardDataCopyWith<$Res> {
|
||||
_$BoardDataCopyWithImpl(this._self, this._then);
|
||||
|
||||
final BoardData _self;
|
||||
final $Res Function(BoardData) _then;
|
||||
|
||||
/// Create a copy of BoardData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = freezed,Object? description = freezed,Object? deadline = freezed,Object? assignedTo = freezed,Object? isReady = freezed,Object? status = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,deadline: freezed == deadline ? _self.deadline : deadline // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,assignedTo: freezed == assignedTo ? _self.assignedTo : assignedTo // ignore: cast_nullable_to_non_nullable
|
||||
as String?,isReady: freezed == isReady ? _self.isReady : isReady // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [BoardData].
|
||||
extension BoardDataPatterns on BoardData {
|
||||
/// 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( _BoardData value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _BoardData() 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( _BoardData value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _BoardData():
|
||||
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( _BoardData value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _BoardData() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? id, String? description, DateTime? deadline, String? assignedTo, bool? isReady, String? status)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _BoardData() when $default != null:
|
||||
return $default(_that.id,_that.description,_that.deadline,_that.assignedTo,_that.isReady,_that.status);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? id, String? description, DateTime? deadline, String? assignedTo, bool? isReady, String? status) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _BoardData():
|
||||
return $default(_that.id,_that.description,_that.deadline,_that.assignedTo,_that.isReady,_that.status);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? id, String? description, DateTime? deadline, String? assignedTo, bool? isReady, String? status)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _BoardData() when $default != null:
|
||||
return $default(_that.id,_that.description,_that.deadline,_that.assignedTo,_that.isReady,_that.status);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _BoardData implements BoardData {
|
||||
const _BoardData({required this.id, required this.description, required this.deadline, required this.assignedTo, required this.isReady, required this.status});
|
||||
factory _BoardData.fromJson(Map<String, dynamic> json) => _$BoardDataFromJson(json);
|
||||
|
||||
@override final String? id;
|
||||
@override final String? description;
|
||||
@override final DateTime? deadline;
|
||||
@override final String? assignedTo;
|
||||
@override final bool? isReady;
|
||||
@override final String? status;
|
||||
|
||||
/// Create a copy of BoardData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$BoardDataCopyWith<_BoardData> get copyWith => __$BoardDataCopyWithImpl<_BoardData>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$BoardDataToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BoardData&&(identical(other.id, id) || other.id == id)&&(identical(other.description, description) || other.description == description)&&(identical(other.deadline, deadline) || other.deadline == deadline)&&(identical(other.assignedTo, assignedTo) || other.assignedTo == assignedTo)&&(identical(other.isReady, isReady) || other.isReady == isReady)&&(identical(other.status, status) || other.status == status));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,description,deadline,assignedTo,isReady,status);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BoardData(id: $id, description: $description, deadline: $deadline, assignedTo: $assignedTo, isReady: $isReady, status: $status)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$BoardDataCopyWith<$Res> implements $BoardDataCopyWith<$Res> {
|
||||
factory _$BoardDataCopyWith(_BoardData value, $Res Function(_BoardData) _then) = __$BoardDataCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String? id, String? description, DateTime? deadline, String? assignedTo, bool? isReady, String? status
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$BoardDataCopyWithImpl<$Res>
|
||||
implements _$BoardDataCopyWith<$Res> {
|
||||
__$BoardDataCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _BoardData _self;
|
||||
final $Res Function(_BoardData) _then;
|
||||
|
||||
/// Create a copy of BoardData
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = freezed,Object? description = freezed,Object? deadline = freezed,Object? assignedTo = freezed,Object? isReady = freezed,Object? status = freezed,}) {
|
||||
return _then(_BoardData(
|
||||
id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,deadline: freezed == deadline ? _self.deadline : deadline // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,assignedTo: freezed == assignedTo ? _self.assignedTo : assignedTo // ignore: cast_nullable_to_non_nullable
|
||||
as String?,isReady: freezed == isReady ? _self.isReady : isReady // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
@@ -1,51 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'board_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_BoardResponse _$BoardResponseFromJson(Map<String, dynamic> json) =>
|
||||
_BoardResponse(
|
||||
success: json['success'] as bool?,
|
||||
message: json['message'] as String?,
|
||||
data:
|
||||
(json['data'] as List<dynamic>?)
|
||||
?.map((e) => BoardData.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
error:
|
||||
json['error'] == null
|
||||
? null
|
||||
: ErrorModel.fromJson(json['error'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BoardResponseToJson(_BoardResponse instance) =>
|
||||
<String, dynamic>{
|
||||
'success': instance.success,
|
||||
'message': instance.message,
|
||||
'data': instance.data,
|
||||
'error': instance.error,
|
||||
};
|
||||
|
||||
_BoardData _$BoardDataFromJson(Map<String, dynamic> json) => _BoardData(
|
||||
id: json['id'] as String?,
|
||||
description: json['description'] as String?,
|
||||
deadline:
|
||||
json['deadline'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deadline'] as String),
|
||||
assignedTo: json['assignedTo'] as String?,
|
||||
isReady: json['isReady'] as bool?,
|
||||
status: json['status'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BoardDataToJson(_BoardData instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'description': instance.description,
|
||||
'deadline': instance.deadline?.toIso8601String(),
|
||||
'assignedTo': instance.assignedTo,
|
||||
'isReady': instance.isReady,
|
||||
'status': instance.status,
|
||||
};
|
||||
@@ -13,6 +13,9 @@ abstract class Meta with _$Meta {
|
||||
required int? page,
|
||||
required int? pages,
|
||||
required int? total,
|
||||
@JsonKey(name: 'has_more') bool? hasMore,
|
||||
@JsonKey(name: 'next_cursor') String? nextCursor,
|
||||
@JsonKey(name: 'prev_cursor') String? prevCursor,
|
||||
}) = _Meta;
|
||||
|
||||
factory Meta.fromJson(Map<String, dynamic> json) => _$MetaFromJson(json);
|
||||
|
||||
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$Meta {
|
||||
|
||||
int? get limit; int? get offset; int? get page; int? get pages; int? get total;
|
||||
int? get limit; int? get offset; int? get page; int? get pages; int? get total;@JsonKey(name: 'has_more') bool? get hasMore;@JsonKey(name: 'next_cursor') String? get nextCursor;@JsonKey(name: 'prev_cursor') String? get prevCursor;
|
||||
/// Create a copy of Meta
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -28,16 +28,16 @@ $MetaCopyWith<Meta> get copyWith => _$MetaCopyWithImpl<Meta>(this as Meta, _$ide
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is Meta&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.offset, offset) || other.offset == offset)&&(identical(other.page, page) || other.page == page)&&(identical(other.pages, pages) || other.pages == pages)&&(identical(other.total, total) || other.total == total));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is Meta&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.offset, offset) || other.offset == offset)&&(identical(other.page, page) || other.page == page)&&(identical(other.pages, pages) || other.pages == pages)&&(identical(other.total, total) || other.total == total)&&(identical(other.hasMore, hasMore) || other.hasMore == hasMore)&&(identical(other.nextCursor, nextCursor) || other.nextCursor == nextCursor)&&(identical(other.prevCursor, prevCursor) || other.prevCursor == prevCursor));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,limit,offset,page,pages,total);
|
||||
int get hashCode => Object.hash(runtimeType,limit,offset,page,pages,total,hasMore,nextCursor,prevCursor);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Meta(limit: $limit, offset: $offset, page: $page, pages: $pages, total: $total)';
|
||||
return 'Meta(limit: $limit, offset: $offset, page: $page, pages: $pages, total: $total, hasMore: $hasMore, nextCursor: $nextCursor, prevCursor: $prevCursor)';
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ abstract mixin class $MetaCopyWith<$Res> {
|
||||
factory $MetaCopyWith(Meta value, $Res Function(Meta) _then) = _$MetaCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
int? limit, int? offset, int? page, int? pages, int? total
|
||||
int? limit, int? offset, int? page, int? pages, int? total,@JsonKey(name: 'has_more') bool? hasMore,@JsonKey(name: 'next_cursor') String? nextCursor,@JsonKey(name: 'prev_cursor') String? prevCursor
|
||||
});
|
||||
|
||||
|
||||
@@ -65,14 +65,17 @@ class _$MetaCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of Meta
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? limit = freezed,Object? offset = freezed,Object? page = freezed,Object? pages = freezed,Object? total = freezed,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? limit = freezed,Object? offset = freezed,Object? page = freezed,Object? pages = freezed,Object? total = freezed,Object? hasMore = freezed,Object? nextCursor = freezed,Object? prevCursor = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
limit: freezed == limit ? _self.limit : limit // ignore: cast_nullable_to_non_nullable
|
||||
as int?,offset: freezed == offset ? _self.offset : offset // ignore: cast_nullable_to_non_nullable
|
||||
as int?,page: freezed == page ? _self.page : page // ignore: cast_nullable_to_non_nullable
|
||||
as int?,pages: freezed == pages ? _self.pages : pages // ignore: cast_nullable_to_non_nullable
|
||||
as int?,total: freezed == total ? _self.total : total // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
as int?,hasMore: freezed == hasMore ? _self.hasMore : hasMore // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,nextCursor: freezed == nextCursor ? _self.nextCursor : nextCursor // ignore: cast_nullable_to_non_nullable
|
||||
as String?,prevCursor: freezed == prevCursor ? _self.prevCursor : prevCursor // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -157,10 +160,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int? limit, int? offset, int? page, int? pages, int? total)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int? limit, int? offset, int? page, int? pages, int? total, @JsonKey(name: 'has_more') bool? hasMore, @JsonKey(name: 'next_cursor') String? nextCursor, @JsonKey(name: 'prev_cursor') String? prevCursor)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Meta() when $default != null:
|
||||
return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total);case _:
|
||||
return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total,_that.hasMore,_that.nextCursor,_that.prevCursor);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -178,10 +181,10 @@ return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total);cas
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int? limit, int? offset, int? page, int? pages, int? total) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int? limit, int? offset, int? page, int? pages, int? total, @JsonKey(name: 'has_more') bool? hasMore, @JsonKey(name: 'next_cursor') String? nextCursor, @JsonKey(name: 'prev_cursor') String? prevCursor) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Meta():
|
||||
return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total);case _:
|
||||
return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total,_that.hasMore,_that.nextCursor,_that.prevCursor);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -198,10 +201,10 @@ return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total);cas
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int? limit, int? offset, int? page, int? pages, int? total)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int? limit, int? offset, int? page, int? pages, int? total, @JsonKey(name: 'has_more') bool? hasMore, @JsonKey(name: 'next_cursor') String? nextCursor, @JsonKey(name: 'prev_cursor') String? prevCursor)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _Meta() when $default != null:
|
||||
return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total);case _:
|
||||
return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total,_that.hasMore,_that.nextCursor,_that.prevCursor);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -213,7 +216,7 @@ return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total);cas
|
||||
@JsonSerializable()
|
||||
|
||||
class _Meta implements Meta {
|
||||
const _Meta({required this.limit, required this.offset, required this.page, required this.pages, required this.total});
|
||||
const _Meta({required this.limit, required this.offset, required this.page, required this.pages, required this.total, @JsonKey(name: 'has_more') this.hasMore, @JsonKey(name: 'next_cursor') this.nextCursor, @JsonKey(name: 'prev_cursor') this.prevCursor});
|
||||
factory _Meta.fromJson(Map<String, dynamic> json) => _$MetaFromJson(json);
|
||||
|
||||
@override final int? limit;
|
||||
@@ -221,6 +224,9 @@ class _Meta implements Meta {
|
||||
@override final int? page;
|
||||
@override final int? pages;
|
||||
@override final int? total;
|
||||
@override@JsonKey(name: 'has_more') final bool? hasMore;
|
||||
@override@JsonKey(name: 'next_cursor') final String? nextCursor;
|
||||
@override@JsonKey(name: 'prev_cursor') final String? prevCursor;
|
||||
|
||||
/// Create a copy of Meta
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@@ -235,16 +241,16 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Meta&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.offset, offset) || other.offset == offset)&&(identical(other.page, page) || other.page == page)&&(identical(other.pages, pages) || other.pages == pages)&&(identical(other.total, total) || other.total == total));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Meta&&(identical(other.limit, limit) || other.limit == limit)&&(identical(other.offset, offset) || other.offset == offset)&&(identical(other.page, page) || other.page == page)&&(identical(other.pages, pages) || other.pages == pages)&&(identical(other.total, total) || other.total == total)&&(identical(other.hasMore, hasMore) || other.hasMore == hasMore)&&(identical(other.nextCursor, nextCursor) || other.nextCursor == nextCursor)&&(identical(other.prevCursor, prevCursor) || other.prevCursor == prevCursor));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,limit,offset,page,pages,total);
|
||||
int get hashCode => Object.hash(runtimeType,limit,offset,page,pages,total,hasMore,nextCursor,prevCursor);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Meta(limit: $limit, offset: $offset, page: $page, pages: $pages, total: $total)';
|
||||
return 'Meta(limit: $limit, offset: $offset, page: $page, pages: $pages, total: $total, hasMore: $hasMore, nextCursor: $nextCursor, prevCursor: $prevCursor)';
|
||||
}
|
||||
|
||||
|
||||
@@ -255,7 +261,7 @@ abstract mixin class _$MetaCopyWith<$Res> implements $MetaCopyWith<$Res> {
|
||||
factory _$MetaCopyWith(_Meta value, $Res Function(_Meta) _then) = __$MetaCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
int? limit, int? offset, int? page, int? pages, int? total
|
||||
int? limit, int? offset, int? page, int? pages, int? total,@JsonKey(name: 'has_more') bool? hasMore,@JsonKey(name: 'next_cursor') String? nextCursor,@JsonKey(name: 'prev_cursor') String? prevCursor
|
||||
});
|
||||
|
||||
|
||||
@@ -272,14 +278,17 @@ class __$MetaCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of Meta
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? limit = freezed,Object? offset = freezed,Object? page = freezed,Object? pages = freezed,Object? total = freezed,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? limit = freezed,Object? offset = freezed,Object? page = freezed,Object? pages = freezed,Object? total = freezed,Object? hasMore = freezed,Object? nextCursor = freezed,Object? prevCursor = freezed,}) {
|
||||
return _then(_Meta(
|
||||
limit: freezed == limit ? _self.limit : limit // ignore: cast_nullable_to_non_nullable
|
||||
as int?,offset: freezed == offset ? _self.offset : offset // ignore: cast_nullable_to_non_nullable
|
||||
as int?,page: freezed == page ? _self.page : page // ignore: cast_nullable_to_non_nullable
|
||||
as int?,pages: freezed == pages ? _self.pages : pages // ignore: cast_nullable_to_non_nullable
|
||||
as int?,total: freezed == total ? _self.total : total // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
as int?,hasMore: freezed == hasMore ? _self.hasMore : hasMore // ignore: cast_nullable_to_non_nullable
|
||||
as bool?,nextCursor: freezed == nextCursor ? _self.nextCursor : nextCursor // ignore: cast_nullable_to_non_nullable
|
||||
as String?,prevCursor: freezed == prevCursor ? _self.prevCursor : prevCursor // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ _Meta _$MetaFromJson(Map<String, dynamic> json) => _Meta(
|
||||
page: (json['page'] as num?)?.toInt(),
|
||||
pages: (json['pages'] as num?)?.toInt(),
|
||||
total: (json['total'] as num?)?.toInt(),
|
||||
hasMore: json['has_more'] as bool?,
|
||||
nextCursor: json['next_cursor'] as String?,
|
||||
prevCursor: json['prev_cursor'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$MetaToJson(_Meta instance) => <String, dynamic>{
|
||||
@@ -20,4 +23,7 @@ Map<String, dynamic> _$MetaToJson(_Meta instance) => <String, dynamic>{
|
||||
'page': instance.page,
|
||||
'pages': instance.pages,
|
||||
'total': instance.total,
|
||||
'has_more': instance.hasMore,
|
||||
'next_cursor': instance.nextCursor,
|
||||
'prev_cursor': instance.prevCursor,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// ignore_for_file: invalid_annotation_target
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:tm_app/core/utils/date_utils.dart';
|
||||
import 'package:tm_app/data/services/model/organization/organization.dart';
|
||||
|
||||
part 'tender_data.freezed.dart';
|
||||
@@ -15,7 +16,8 @@ abstract class TenderData with _$TenderData {
|
||||
@JsonKey(name: 'tender_id') required String? tenderId,
|
||||
@JsonKey(name: 'notice_publication_id')
|
||||
required String? noticePublicationId,
|
||||
@JsonKey(name: 'publication_date') required int? publicationDate,
|
||||
@JsonKey(name: 'publication_date', fromJson: unixTimestampFromJson)
|
||||
required int? publicationDate,
|
||||
required String? title,
|
||||
required String? description,
|
||||
@JsonKey(name: 'procurement_type_code')
|
||||
@@ -24,9 +26,12 @@ abstract class TenderData with _$TenderData {
|
||||
@JsonKey(name: 'estimated_value') required int? estimatedValue,
|
||||
required String? currency,
|
||||
required String? duration,
|
||||
@JsonKey(name: 'tender_deadline') required int? tenderDeadline,
|
||||
@JsonKey(name: 'submission_deadline') required int? submissionDeadline,
|
||||
@JsonKey(name: 'application_deadline') required int? applicationDeadline,
|
||||
@JsonKey(name: 'tender_deadline', fromJson: unixTimestampFromJson)
|
||||
required int? tenderDeadline,
|
||||
@JsonKey(name: 'submission_deadline', fromJson: unixTimestampFromJson)
|
||||
required int? submissionDeadline,
|
||||
@JsonKey(name: 'application_deadline', fromJson: unixTimestampFromJson)
|
||||
required int? applicationDeadline,
|
||||
@JsonKey(name: 'submission_url') required String? submissionUrl,
|
||||
@JsonKey(name: 'country_code') required String? countryCode,
|
||||
@JsonKey(name: 'duration_unit') required String? durationUnit,
|
||||
|
||||
@@ -12,7 +12,7 @@ _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?,
|
||||
publicationDate: (json['publication_date'] as num?)?.toInt(),
|
||||
publicationDate: unixTimestampFromJson(json['publication_date']),
|
||||
title: json['title'] as String?,
|
||||
description: json['description'] as String?,
|
||||
procurementTypeCode: json['procurement_type_code'] as String?,
|
||||
@@ -20,9 +20,9 @@ _TenderData _$TenderDataFromJson(Map<String, dynamic> json) => _TenderData(
|
||||
estimatedValue: (json['estimated_value'] as num?)?.toInt(),
|
||||
currency: json['currency'] as String?,
|
||||
duration: json['duration'] as String?,
|
||||
tenderDeadline: (json['tender_deadline'] as num?)?.toInt(),
|
||||
submissionDeadline: (json['submission_deadline'] as num?)?.toInt(),
|
||||
applicationDeadline: (json['application_deadline'] as num?)?.toInt(),
|
||||
tenderDeadline: unixTimestampFromJson(json['tender_deadline']),
|
||||
submissionDeadline: unixTimestampFromJson(json['submission_deadline']),
|
||||
applicationDeadline: unixTimestampFromJson(json['application_deadline']),
|
||||
submissionUrl: json['submission_url'] as String?,
|
||||
countryCode: json['country_code'] as String?,
|
||||
durationUnit: json['duration_unit'] as String?,
|
||||
|
||||
@@ -16,7 +16,8 @@ class TendersService {
|
||||
|
||||
Future<Result<TendersResponse>> getTenders({
|
||||
required int limit,
|
||||
required int offset,
|
||||
int? offset,
|
||||
String? cursor,
|
||||
String? query,
|
||||
String? sortBy,
|
||||
String? sortOrder,
|
||||
@@ -27,6 +28,7 @@ class TendersService {
|
||||
TendersApi.getTenders(
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
cursor: cursor,
|
||||
query: query,
|
||||
sortBy: sortBy,
|
||||
sortOrder: sortOrder,
|
||||
@@ -37,7 +39,6 @@ class TendersService {
|
||||
method: 'GET',
|
||||
(json) => TendersResponse.fromJson(json),
|
||||
);
|
||||
print(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+5
-4
@@ -2,6 +2,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -130,6 +131,9 @@ class _MyAppState extends State<MyApp> {
|
||||
Widget build(BuildContext context) {
|
||||
// Listen to theme changes
|
||||
final themeProvider = context.watch<ThemeProvider>();
|
||||
final pageTransition = kIsWeb
|
||||
? const FadeUpwardsPageTransitionsBuilder()
|
||||
: const CupertinoPageTransitionsBuilder();
|
||||
|
||||
return ToastificationWrapper(
|
||||
child: MaterialApp.router(
|
||||
@@ -147,10 +151,7 @@ class _MyAppState extends State<MyApp> {
|
||||
pageTransitionsTheme: PageTransitionsTheme(
|
||||
builders: {
|
||||
for (final platform in TargetPlatform.values)
|
||||
platform:
|
||||
kIsWeb
|
||||
? const FadeUpwardsPageTransitionsBuilder()
|
||||
: const CupertinoPageTransitionsBuilder(),
|
||||
platform: pageTransition,
|
||||
},
|
||||
),
|
||||
brightness:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../core/enums/task_status.dart';
|
||||
import '../core/utils/result.dart';
|
||||
import '../data/repositories/board_repositories.dart';
|
||||
import '../data/services/board_service.dart';
|
||||
import '../data/services/model/board_response/board_response.dart';
|
||||
|
||||
class BoardViewModel with ChangeNotifier {
|
||||
@@ -12,23 +12,27 @@ class BoardViewModel with ChangeNotifier {
|
||||
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
List<BoardData> _boardData = [];
|
||||
Board? _board;
|
||||
|
||||
bool get isLoading => _isLoading;
|
||||
String? get errorMessage => _errorMessage;
|
||||
List<BoardData> get boardData => _boardData;
|
||||
bool get hasData => _boardData.isNotEmpty;
|
||||
Board? get board => _board;
|
||||
List<BoardColumn> get columns => _board?.columns ?? const [];
|
||||
bool get hasData => _board != null && columns.isNotEmpty;
|
||||
|
||||
Future<void> getBoard() async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
final result = await _boardRepository.getBoard();
|
||||
|
||||
switch (result) {
|
||||
case Ok<BoardResponse>():
|
||||
// تبدیل به لیست قابل تغییر
|
||||
_boardData = List.from(result.value.data ?? []);
|
||||
_board = result.value.data?.board;
|
||||
if (_board == null) {
|
||||
_errorMessage = result.value.error?.message ?? 'No board data';
|
||||
}
|
||||
break;
|
||||
case Error<BoardResponse>():
|
||||
_errorMessage = result.error.toString();
|
||||
@@ -38,40 +42,86 @@ class BoardViewModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// update task status
|
||||
void updateTaskStatus(String taskId, String newStatus) {
|
||||
final taskIndex = _boardData.indexWhere((task) => task.id == taskId);
|
||||
if (taskIndex != -1) {
|
||||
_boardData[taskIndex] = _boardData[taskIndex].copyWith(status: newStatus);
|
||||
/// Look up the column a given card currently lives in.
|
||||
BoardColumn? columnForCard(String cardId) {
|
||||
for (final column in columns) {
|
||||
if (column.cards.any((c) => c.id == cardId)) return column;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Move a card to [targetColumnId] at the end of that column.
|
||||
/// Optimistically updates local state, then calls the API; rolls back on
|
||||
/// failure.
|
||||
Future<Result<MoveCardResponse>> moveCard({
|
||||
required BoardCard card,
|
||||
required BoardColumn targetColumn,
|
||||
}) async {
|
||||
final cardId = card.id;
|
||||
final targetColumnId = targetColumn.id;
|
||||
if (cardId == null || targetColumnId == null || _board == null) {
|
||||
return Result.error(Exception('Invalid card or column'));
|
||||
}
|
||||
|
||||
final sourceColumn = columnForCard(cardId);
|
||||
if (sourceColumn == null) {
|
||||
return Result.error(Exception('Card not found in any column'));
|
||||
}
|
||||
if (sourceColumn.id == targetColumnId) {
|
||||
return Result.ok(MoveCardResponse(success: true, message: 'No change'));
|
||||
}
|
||||
|
||||
final previousColumns = _board!.columns;
|
||||
final newOrder = targetColumn.cards.length;
|
||||
|
||||
_board = _board!.copyWith(
|
||||
columns:
|
||||
previousColumns.map((column) {
|
||||
if (column.id == sourceColumn.id) {
|
||||
final remaining =
|
||||
column.cards.where((c) => c.id != cardId).toList();
|
||||
return column.copyWith(
|
||||
cards: remaining,
|
||||
cardCount: remaining.length,
|
||||
);
|
||||
}
|
||||
if (column.id == targetColumnId) {
|
||||
final updated = List<BoardCard>.from(column.cards)
|
||||
..add(card.copyWith(columnId: targetColumnId, order: newOrder));
|
||||
return column.copyWith(
|
||||
cards: updated,
|
||||
cardCount: updated.length,
|
||||
);
|
||||
}
|
||||
return column;
|
||||
}).toList(),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
bool isValidMove(TaskStatus currentStatus, TaskStatus targetStatus) {
|
||||
if (currentStatus == TaskStatus.toDo) {
|
||||
return targetStatus == TaskStatus.inProgress;
|
||||
} else if (currentStatus == TaskStatus.inProgress) {
|
||||
return targetStatus == TaskStatus.toDo || targetStatus == TaskStatus.done;
|
||||
} else if (currentStatus == TaskStatus.done) {
|
||||
return targetStatus == TaskStatus.inProgress;
|
||||
final result = await _boardRepository.moveCard(
|
||||
cardId: cardId,
|
||||
columnId: targetColumnId,
|
||||
newOrder: newOrder,
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
return false;
|
||||
_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;
|
||||
}
|
||||
|
||||
List<BoardData> getTasksByStatus(TaskStatus status) {
|
||||
return boardData.where((task) => task.status == status.value).toList();
|
||||
}
|
||||
|
||||
void _updateTaskStatus(BoardData task, TaskStatus newStatus) {
|
||||
if (task.status == newStatus.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
final oldStatus = task.status;
|
||||
|
||||
// update task status in ViewModel
|
||||
updateTaskStatus(task.id!, newStatus.value);
|
||||
|
||||
// _showStatusChangeMessage(context, task, oldStatus, newStatus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tm_app/core/utils/result.dart';
|
||||
import 'package:tm_app/data/services/model/feedback_stats/feedback_stats.dart';
|
||||
import 'package:tm_app/data/services/model/recommended_tenders_response/recommended_tenders_response.dart';
|
||||
import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart';
|
||||
import 'package:tm_app/data/services/model/tender_approvals_stats_response/tender_approvals_stats_response.dart';
|
||||
import 'package:tm_app/data/services/model/tender_data/tender_data.dart';
|
||||
|
||||
import '../data/repositories/home_repository.dart';
|
||||
import '../data/services/model/feedback_stats_response/feedback_stat_response.dart';
|
||||
import '../data/services/model/home/home_response/home_response_model.dart';
|
||||
import '../data/services/model/request_models/get_tenders_request_model/get_tenders_request_model.dart';
|
||||
import '../data/services/notification_state_service.dart';
|
||||
@@ -46,7 +44,7 @@ class HomeViewModel with ChangeNotifier {
|
||||
|
||||
RecommendedTendersResponse? _recommendedTendersResponse;
|
||||
TenderApprovalsStatsResponse? tenderApprovalsStateResponse;
|
||||
FeedbackStats? feedbackStats;
|
||||
int userLikedTendersCount = 0;
|
||||
final List<TenderData> _tenders = [];
|
||||
final int _pageSize = 10;
|
||||
int _currentPage = 0;
|
||||
@@ -65,11 +63,19 @@ class HomeViewModel with ChangeNotifier {
|
||||
|
||||
void init() async {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
userLikedTendersCount = 0;
|
||||
_tenders.clear();
|
||||
_currentPage = 0;
|
||||
_hasMore = true;
|
||||
_isRecommendedMode = false;
|
||||
data = null;
|
||||
tenderApprovalsStateResponse = null;
|
||||
notifyListeners();
|
||||
|
||||
await getApprovedStates();
|
||||
await getYourTenders(reset: true);
|
||||
await getFeedbackStats();
|
||||
await loadUserLikedTendersCount();
|
||||
|
||||
if (_errorMessage != null) {
|
||||
_isLoading = false;
|
||||
@@ -192,15 +198,15 @@ class HomeViewModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> getFeedbackStats() async {
|
||||
final result = await _homeRepository.getFeedbackStats();
|
||||
Future<void> loadUserLikedTendersCount() async {
|
||||
final result = await _homeRepository.getUserLikedTendersCount();
|
||||
|
||||
switch (result) {
|
||||
case Ok<FeedbackStatResponse>():
|
||||
feedbackStats = result.value.data;
|
||||
case Ok<int>():
|
||||
userLikedTendersCount = result.value;
|
||||
break;
|
||||
case Error<FeedbackStatResponse>():
|
||||
_errorMessage = result.error.toString();
|
||||
case Error<int>():
|
||||
userLikedTendersCount = 0;
|
||||
break;
|
||||
}
|
||||
notifyListeners();
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:tm_app/data/services/model/tender_data/tender_data.dart';
|
||||
import 'package:tm_app/data/services/model/tender_detail_response/tender_detail_response_model.dart';
|
||||
|
||||
import '../core/constants/tender_approval_status.dart';
|
||||
import '../core/constants/tender_submision_mode.dart';
|
||||
import '../data/services/model/tender_approvals_by_id_response/tender_approvals_by_id_response.dart';
|
||||
import '../data/services/model/tender_approvals_data/tender_approvals_data.dart';
|
||||
import '../data/services/model/tender_approvals_toggle_response/tender_approvals_toggle_response.dart';
|
||||
@@ -143,4 +144,33 @@ class TenderDetailViewModel with ChangeNotifier {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Submits the tender for completion via the self-apply flow.
|
||||
/// POSTs to /api/v1/tender-approvals with status "submitted" and
|
||||
/// submission_mode "self-apply". Returns true on success.
|
||||
Future<bool> submitForCompletion({required String tenderId}) async {
|
||||
_isSubmitApprovalLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
bool success = false;
|
||||
final result = await _tendersRepository.toggleApprovals(
|
||||
tenderId: tenderId,
|
||||
status: TenderApprovalStatus.submitted.value,
|
||||
submissionMode: TenderSubmissionMode.selfApply.value,
|
||||
);
|
||||
switch (result) {
|
||||
case Ok<TenderApprovalsToggleResponse>():
|
||||
success = true;
|
||||
_status = result.value.data?.status ?? '';
|
||||
break;
|
||||
case Error<TenderApprovalsToggleResponse>():
|
||||
_errorMessage = result.error.toString();
|
||||
break;
|
||||
}
|
||||
|
||||
_isSubmitApprovalLoading = false;
|
||||
notifyListeners();
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:tm_app/core/utils/logger.dart';
|
||||
import 'package:tm_app/data/repositories/tenders_repository.dart';
|
||||
import 'package:tm_app/data/services/model/feedback_data/feedback_data.dart';
|
||||
import 'package:tm_app/data/services/model/feedback_response/feedback_response.dart';
|
||||
import 'package:tm_app/data/services/model/tender_data/tender_data.dart';
|
||||
import 'package:tm_app/data/services/model/tenders/tenders_response/tenders_response.dart';
|
||||
import 'package:tm_app/views/tenders/strings/tenders_strings.dart';
|
||||
|
||||
@@ -36,15 +37,44 @@ class TendersViewModel with ChangeNotifier {
|
||||
|
||||
List<FeedbackData> feedbacks = [];
|
||||
|
||||
int currentPage = 1;
|
||||
int _currentOffset = 0;
|
||||
int _totalPages = 0;
|
||||
int get totalPages => _totalPages;
|
||||
static const int _pageSize = 10;
|
||||
bool _hasMoreData = true;
|
||||
|
||||
// --- Offset pagination 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';
|
||||
@@ -80,15 +110,12 @@ class TendersViewModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
void setStatus(String status) {
|
||||
// Clear data when status changes
|
||||
_clearAllData();
|
||||
selectedStatus = status;
|
||||
currentPage = 1;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setSort(String sort) {
|
||||
// Clear data when status changes
|
||||
_clearAllData();
|
||||
selectedSort = sort;
|
||||
if (sort == TendersStrings.minimumTime) {
|
||||
@@ -98,16 +125,13 @@ class TendersViewModel with ChangeNotifier {
|
||||
sortBy = 'submission_deadline';
|
||||
sortOrder = 'desc';
|
||||
}
|
||||
currentPage = 1;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _clearAllData() {
|
||||
_tendersResponse = null;
|
||||
_errorMessage = null;
|
||||
_currentOffset = 0;
|
||||
_hasMoreData = true;
|
||||
currentPage = 1;
|
||||
_resetPagination();
|
||||
sortBy = '';
|
||||
sortOrder = '';
|
||||
selectedSort = '';
|
||||
@@ -116,6 +140,11 @@ class TendersViewModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _resetPagination() {
|
||||
_currentOffset = 0;
|
||||
_hasMoreData = true;
|
||||
}
|
||||
|
||||
void init(BuildContext context) async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
@@ -151,10 +180,7 @@ class TendersViewModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> getTendersMobile({String? query}) async {
|
||||
// Clear all data before fetching new tenders
|
||||
|
||||
_currentOffset = 0;
|
||||
_hasMoreData = true;
|
||||
_resetPagination();
|
||||
_searchQuery = query;
|
||||
|
||||
final result = await _tendersRepository.getTenders(
|
||||
@@ -164,28 +190,7 @@ class TendersViewModel with ChangeNotifier {
|
||||
sortBy: '',
|
||||
sortOrder: '',
|
||||
);
|
||||
switch (result) {
|
||||
case Ok<TendersResponse>():
|
||||
_tendersResponse = result.value;
|
||||
// Check if we have more data
|
||||
if (_tendersResponse?.data?.tenders != null) {
|
||||
_hasMoreData = _tendersResponse!.data!.tenders!.length >= _pageSize;
|
||||
}
|
||||
if (_tendersResponse?.data?.tenders?.isNotEmpty == true) {
|
||||
// Fetch feedback and approvals for the first tender only
|
||||
getTenderFeedback(_tendersResponse!.data!.tenders!.first.id!);
|
||||
}
|
||||
if (_tendersResponse?.meta?.pages != null) {
|
||||
_totalPages = _tendersResponse!.meta!.pages!;
|
||||
} else {
|
||||
_totalPages = 1;
|
||||
}
|
||||
notifyListeners();
|
||||
break;
|
||||
case Error<TendersResponse>():
|
||||
_errorMessage = result.error.toString();
|
||||
break;
|
||||
}
|
||||
_handleListResult(result, fetchAllFeedbacks: false);
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
_errorMessage = null;
|
||||
@@ -199,11 +204,9 @@ class TendersViewModel with ChangeNotifier {
|
||||
String? publicationDateFrom,
|
||||
String? publicationDateTo,
|
||||
}) async {
|
||||
// Clear all data before fetching new tenders
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
_currentOffset = 0;
|
||||
_hasMoreData = true;
|
||||
_resetPagination();
|
||||
_searchQuery = query;
|
||||
|
||||
final result = await _tendersRepository.getTenders(
|
||||
@@ -215,35 +218,55 @@ class TendersViewModel with ChangeNotifier {
|
||||
publicationDateFrom: publicationDateFrom ?? '',
|
||||
publicationDateTo: publicationDateTo ?? '',
|
||||
);
|
||||
switch (result) {
|
||||
case Ok<TendersResponse>():
|
||||
_tendersResponse = result.value;
|
||||
// Check if we have more data
|
||||
if (_tendersResponse?.data?.tenders != null) {
|
||||
_hasMoreData = _tendersResponse!.data!.tenders!.length >= _pageSize;
|
||||
}
|
||||
if (_tendersResponse?.data?.tenders?.isNotEmpty == true) {
|
||||
for (var tender in _tendersResponse!.data!.tenders!) {
|
||||
getTenderFeedback(tender.id!);
|
||||
}
|
||||
}
|
||||
if (_tendersResponse?.meta?.pages != null) {
|
||||
_totalPages = _tendersResponse!.meta!.pages!;
|
||||
} else {
|
||||
_totalPages = 1;
|
||||
}
|
||||
notifyListeners();
|
||||
break;
|
||||
case Error<TendersResponse>():
|
||||
_errorMessage = result.error.toString();
|
||||
break;
|
||||
}
|
||||
_handleListResult(result, fetchAllFeedbacks: true);
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _handleListResult(
|
||||
Result<TendersResponse> result, {
|
||||
required bool fetchAllFeedbacks,
|
||||
}) {
|
||||
switch (result) {
|
||||
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!;
|
||||
}
|
||||
final tenders = _tendersResponse?.data?.tenders ?? [];
|
||||
_hasMoreData =
|
||||
meta?.hasMore ?? (tenders.length >= _pageSize);
|
||||
if (tenders.isNotEmpty) {
|
||||
if (fetchAllFeedbacks) {
|
||||
for (final tender in tenders) {
|
||||
if (tender.id != null) {
|
||||
getTenderFeedback(tender.id!);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getTenderFeedback(tenders.first.id!);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Error<TendersResponse>():
|
||||
_handleListError(result.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleListError(Exception error) {
|
||||
_errorMessage = error.toString();
|
||||
}
|
||||
|
||||
Future<void> loadMoreTenders() async {
|
||||
if (_isLoadingMore || !_hasMoreData) {
|
||||
return;
|
||||
@@ -253,7 +276,10 @@ class TendersViewModel with ChangeNotifier {
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
_currentOffset += _pageSize;
|
||||
final meta = _tendersResponse?.meta;
|
||||
final limit = meta?.limit ?? _pageSize;
|
||||
_currentOffset += limit;
|
||||
|
||||
final result = await _tendersRepository.getTenders(
|
||||
limit: _pageSize,
|
||||
offset: _currentOffset,
|
||||
@@ -264,30 +290,31 @@ class TendersViewModel with ChangeNotifier {
|
||||
|
||||
switch (result) {
|
||||
case Ok<TendersResponse>():
|
||||
if (result.value.data?.tenders != null &&
|
||||
result.value.data!.tenders!.isNotEmpty) {
|
||||
// Create a new modifiable list by combining existing and new tenders
|
||||
if (_tendersResponse?.data?.tenders != null) {
|
||||
final existingTenders = _tendersResponse!.data!.tenders!;
|
||||
final newTenders = result.value.data!.tenders!;
|
||||
final combinedTenders = [...existingTenders, ...newTenders];
|
||||
|
||||
// Create a new TendersData with the combined list
|
||||
_tendersResponse = _tendersResponse!.copyWith(
|
||||
data: _tendersResponse!.data!.copyWith(tenders: combinedTenders),
|
||||
final newTenders = result.value.data?.tenders ?? <TenderData>[];
|
||||
if (newTenders.isNotEmpty) {
|
||||
final existingTenders =
|
||||
_tendersResponse?.data?.tenders ?? <TenderData>[];
|
||||
final combined = <TenderData>[...existingTenders, ...newTenders];
|
||||
_tendersResponse = result.value.copyWith(
|
||||
data: result.value.data?.copyWith(tenders: combined),
|
||||
);
|
||||
} else {
|
||||
_tendersResponse = result.value;
|
||||
final newMeta = result.value.meta;
|
||||
if (newMeta?.pages != null && newMeta!.pages! > 0) {
|
||||
_lastValidPages = newMeta.pages!;
|
||||
}
|
||||
// Check if we have more data
|
||||
_hasMoreData = result.value.data!.tenders!.length >= _pageSize;
|
||||
if (newMeta?.limit != null && newMeta!.limit! > 0) {
|
||||
_lastValidLimit = newMeta.limit!;
|
||||
}
|
||||
_hasMoreData =
|
||||
newMeta?.hasMore ?? (newTenders.length >= _pageSize);
|
||||
} else {
|
||||
_hasMoreData = false;
|
||||
}
|
||||
break;
|
||||
case Error<TendersResponse>():
|
||||
_errorMessage = result.error.toString();
|
||||
_currentOffset -= _pageSize; // Revert offset on error
|
||||
final limit = meta?.limit ?? _pageSize;
|
||||
_currentOffset = (_currentOffset - limit).clamp(0, 1 << 30);
|
||||
_handleListError(result.error);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -298,7 +325,6 @@ class TendersViewModel with ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> getTenderFeedback(String tenderId) async {
|
||||
// _isLoading = true;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
@@ -308,7 +334,6 @@ class TendersViewModel with ChangeNotifier {
|
||||
switch (result) {
|
||||
case Ok<FeedbackResponse>():
|
||||
_feedbackData = result.value.data;
|
||||
// Add or update _feedbackData in feedbacks list by tender id
|
||||
if (_feedbackData != null) {
|
||||
final index = feedbacks.indexWhere(
|
||||
(f) => f.tenderId == _feedbackData!.tenderId,
|
||||
@@ -331,7 +356,6 @@ class TendersViewModel with ChangeNotifier {
|
||||
_errorMessage = result.error.toString();
|
||||
break;
|
||||
}
|
||||
// _isLoading = false;
|
||||
notifyListeners();
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
@@ -343,38 +367,32 @@ class TendersViewModel with ChangeNotifier {
|
||||
}) async {
|
||||
_feedbackErrorMessage = null;
|
||||
|
||||
// Get current feedback for this tender
|
||||
final existingFeedback = getFeedbackForTender(tenderId);
|
||||
final existingFeedbackIndex = feedbacks.indexWhere(
|
||||
(f) => f.tenderId == tenderId,
|
||||
);
|
||||
|
||||
// Create optimistic feedback data
|
||||
FeedbackData? optimisticFeedback;
|
||||
|
||||
if (existingFeedback == null) {
|
||||
// No existing feedback - create new optimistic feedback
|
||||
optimisticFeedback = FeedbackData(
|
||||
companyId: null, // Will be filled by server
|
||||
companyId: null,
|
||||
createdAt: DateTime.now().millisecondsSinceEpoch,
|
||||
customerId: null, // Will be filled by server
|
||||
customerId: null,
|
||||
feedbackType: feedbackType,
|
||||
id: null, // Will be filled by server
|
||||
id: null,
|
||||
tenderId: tenderId,
|
||||
tender: null,
|
||||
updatedAt: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
feedbacks.add(optimisticFeedback);
|
||||
} else {
|
||||
// Existing feedback - update optimistically
|
||||
if (existingFeedback.feedbackType == feedbackType) {
|
||||
// Same feedback type - remove feedback (toggle off)
|
||||
if (existingFeedbackIndex != -1) {
|
||||
feedbacks.removeAt(existingFeedbackIndex);
|
||||
}
|
||||
optimisticFeedback = null;
|
||||
} else {
|
||||
// Different feedback type - update to new type
|
||||
optimisticFeedback = existingFeedback.copyWith(
|
||||
feedbackType: feedbackType,
|
||||
updatedAt: DateTime.now().millisecondsSinceEpoch,
|
||||
@@ -385,10 +403,8 @@ class TendersViewModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
// Notify listeners immediately for optimistic update
|
||||
notifyListeners();
|
||||
|
||||
// Call API
|
||||
final result = await _tendersRepository.toggleFeedback(
|
||||
tenderId: tenderId,
|
||||
feedbackType: feedbackType,
|
||||
@@ -398,7 +414,6 @@ class TendersViewModel with ChangeNotifier {
|
||||
case Ok<FeedbackResponse>():
|
||||
_feedbackData = result.value.data;
|
||||
if (_feedbackData != null) {
|
||||
// Update with complete data from server
|
||||
final index = feedbacks.indexWhere(
|
||||
(f) => f.tender == _feedbackData!.tender,
|
||||
);
|
||||
@@ -408,17 +423,13 @@ class TendersViewModel with ChangeNotifier {
|
||||
feedbacks.add(_feedbackData!);
|
||||
}
|
||||
} else {
|
||||
// Server returned null data - remove feedback
|
||||
feedbacks.removeWhere((f) => f.tenderId == tenderId);
|
||||
}
|
||||
break;
|
||||
case Error<FeedbackResponse>():
|
||||
// Revert optimistic update on error
|
||||
if (existingFeedback == null) {
|
||||
// Remove the optimistic feedback we added
|
||||
feedbacks.removeWhere((f) => f.tenderId == tenderId);
|
||||
} else {
|
||||
// Restore the original feedback - find current index and update, or add if not found
|
||||
final currentIndex = feedbacks.indexWhere(
|
||||
(f) => f.tenderId == tenderId,
|
||||
);
|
||||
@@ -437,7 +448,6 @@ class TendersViewModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear all feedbacks
|
||||
void clearFeedbacks() {
|
||||
feedbacks.clear();
|
||||
_feedbackData = null;
|
||||
@@ -445,13 +455,10 @@ class TendersViewModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear all tenders
|
||||
void clearTenders() {
|
||||
_tendersResponse = null;
|
||||
_errorMessage = null;
|
||||
_currentOffset = 0;
|
||||
_hasMoreData = true;
|
||||
currentPage = 1;
|
||||
_resetPagination();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -464,7 +471,6 @@ class TendersViewModel with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
/// Search tenders with query
|
||||
Future<void> searchTenders(String query) async {
|
||||
await getTendersDesktop(
|
||||
query: query.trim(),
|
||||
@@ -475,7 +481,6 @@ class TendersViewModel with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
/// Clear all data (tenders, feedbacks, and approvals)
|
||||
void clearAll() {
|
||||
clearTenders();
|
||||
clearFeedbacks();
|
||||
@@ -488,17 +493,32 @@ class TendersViewModel with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void jumpToPage(int page) async {
|
||||
final meta = tendersResponse?.meta;
|
||||
if (meta?.pages == null) {
|
||||
/// [selected] is the absolute 0-based target page index.
|
||||
Future<void> handlePaginationChange(int selected) async {
|
||||
if (selected < 0) {
|
||||
return;
|
||||
}
|
||||
if (page < 1 || page > meta!.pages!) {
|
||||
final maxPage = totalPages - 1;
|
||||
if (maxPage >= 0 && selected > maxPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentPage = page;
|
||||
_currentOffset = (page - 1) * _pageSize;
|
||||
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,
|
||||
@@ -506,17 +526,20 @@ class TendersViewModel with ChangeNotifier {
|
||||
query: _searchQuery,
|
||||
sortBy: sortBy,
|
||||
sortOrder: sortOrder,
|
||||
publicationDateFrom: startDateUnix,
|
||||
publicationDateTo: endDateUnix,
|
||||
);
|
||||
|
||||
switch (result) {
|
||||
case Ok<TendersResponse>():
|
||||
_tendersResponse = result.value;
|
||||
break;
|
||||
case Error<TendersResponse>():
|
||||
_errorMessage = result.error.toString();
|
||||
break;
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +206,9 @@ class YourTendersViewModel with ChangeNotifier {
|
||||
offset: _tendersOffset,
|
||||
);
|
||||
await getTenders(requestModel: requestModel);
|
||||
} else if (selectedStatus == YourTendersStrings.tendersSubmitted) {
|
||||
} else if (selectedStatus == YourTendersStrings.tendersSubmitted ||
|
||||
selectedStatus == TenderApprovalStatus.submitted.value ||
|
||||
selectedStatus == TenderApprovalStatus.approved.value) {
|
||||
final requestModel = GetTendersRequestModel(
|
||||
status: TenderApprovalStatus.submitted.value,
|
||||
createdFrom: startDateUnix,
|
||||
@@ -299,10 +301,17 @@ class YourTendersViewModel with ChangeNotifier {
|
||||
reset: true,
|
||||
);
|
||||
} else if (selectedStatus == TenderApprovalStatus.approved.value) {
|
||||
await getTendersFeedback(
|
||||
feedbackType: TenderApprovalStatus.approved.value,
|
||||
reset: true,
|
||||
// "Approved tenders" maps to submitted tender-approvals. The API has no
|
||||
// `approved` value; approved tenders are the ones a company has submitted
|
||||
// for pursuit (GET /api/v1/tender-approvals?status=submitted).
|
||||
final requestModel = GetTendersRequestModel(
|
||||
status: TenderApprovalStatus.submitted.value,
|
||||
createdFrom: startDateUnix,
|
||||
createdTo: endDateUnix,
|
||||
limit: _tendersLimit,
|
||||
offset: _tendersOffset,
|
||||
);
|
||||
await getTenders(requestModel: requestModel, reset: true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tm_app/core/extensions/board_data_extension.dart';
|
||||
import 'package:tm_app/core/theme/colors.dart';
|
||||
import 'package:tm_app/core/utils/date_utils.dart';
|
||||
import 'package:tm_app/core/utils/result.dart';
|
||||
import 'package:tm_app/core/utils/size_config.dart';
|
||||
import 'package:tm_app/data/services/board_service.dart';
|
||||
import 'package:tm_app/data/services/model/board_response/board_response.dart';
|
||||
import 'package:tm_app/views/shared/desktop_navigation_widget.dart';
|
||||
|
||||
import '../../../core/constants/assets.dart';
|
||||
import '../../../core/enums/task_status.dart';
|
||||
import '../../../core/utils/app_toast.dart';
|
||||
import '../../../view_models/board_view_model.dart';
|
||||
import '../strings/board_strings.dart';
|
||||
@@ -32,38 +33,27 @@ class _BoardScreenState extends State<BoardScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
void _updateTaskStatus(BoardData task, TaskStatus newStatus) {
|
||||
if (task.statusEnum == newStatus) {
|
||||
return;
|
||||
}
|
||||
Future<void> _onCardDropped(BoardCard card, BoardColumn target) async {
|
||||
final source = _boardViewModel.columnForCard(card.id ?? '');
|
||||
if (source == null || source.id == target.id) return;
|
||||
|
||||
final oldStatus = task.statusEnum;
|
||||
|
||||
// update task status in ViewModel
|
||||
_boardViewModel.updateTaskStatus(task.id!, newStatus.value);
|
||||
|
||||
_showStatusChangeMessage(context, task, oldStatus, newStatus);
|
||||
}
|
||||
|
||||
void _showStatusChangeMessage(
|
||||
BuildContext context,
|
||||
BoardData task,
|
||||
TaskStatus oldStatus,
|
||||
TaskStatus newStatus,
|
||||
) {
|
||||
final statusNames = {
|
||||
TaskStatus.toDo: BoardStrings.toDo,
|
||||
TaskStatus.inProgress: BoardStrings.inProgress,
|
||||
TaskStatus.done: BoardStrings.done,
|
||||
};
|
||||
final result = await _boardViewModel.moveCard(
|
||||
card: card,
|
||||
targetColumn: target,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
if (result is Ok<MoveCardResponse>) {
|
||||
AppToast.success(
|
||||
context,
|
||||
BoardStrings.taskMovedMessage(
|
||||
statusNames[oldStatus]!,
|
||||
statusNames[newStatus]!,
|
||||
BoardStrings.cardMovedMessage(
|
||||
source.name ?? '',
|
||||
target.name ?? '',
|
||||
),
|
||||
);
|
||||
} else if (result is Error<MoveCardResponse>) {
|
||||
AppToast.error(context, BoardStrings.moveFailed);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -73,9 +63,7 @@ class _BoardScreenState extends State<BoardScreen> {
|
||||
backgroundColor: AppColors.backgroundColor,
|
||||
body: Column(
|
||||
children: [
|
||||
const DesktopNavigationWidget(
|
||||
currentIndex: 2, // Tenders index
|
||||
),
|
||||
const DesktopNavigationWidget(currentIndex: 2),
|
||||
Consumer<BoardViewModel>(
|
||||
builder: (context, viewModel, child) {
|
||||
if (viewModel.isLoading) {
|
||||
@@ -92,12 +80,16 @@ class _BoardScreenState extends State<BoardScreen> {
|
||||
child: Center(child: Text(viewModel.errorMessage!)),
|
||||
);
|
||||
}
|
||||
if (viewModel.hasData == false) {
|
||||
if (!viewModel.hasData) {
|
||||
return const Expanded(
|
||||
child: Center(child: Text(BoardStrings.emptyListText)),
|
||||
);
|
||||
}
|
||||
|
||||
final columns = [...viewModel.columns]..sort(
|
||||
(a, b) => (a.order ?? 0).compareTo(b.order ?? 0),
|
||||
);
|
||||
|
||||
return Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
@@ -108,7 +100,7 @@ class _BoardScreenState extends State<BoardScreen> {
|
||||
children: [
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
BoardStrings.boardTitle,
|
||||
viewModel.board?.name ?? BoardStrings.boardTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20.0.sp(),
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -126,51 +118,16 @@ class _BoardScreenState extends State<BoardScreen> {
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// To Do
|
||||
for (int i = 0; i < columns.length; i++) ...[
|
||||
if (i > 0) const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _BoardColumn(
|
||||
title: BoardStrings.toDo,
|
||||
color: AppColors.primary20,
|
||||
textColor: const Color(0xFF1976D2),
|
||||
tasks: viewModel.getTasksByStatus(
|
||||
TaskStatus.toDo,
|
||||
),
|
||||
status: TaskStatus.toDo,
|
||||
onTaskDropped: _updateTaskStatus,
|
||||
isValidMove: viewModel.isValidMove,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// In Progress
|
||||
Expanded(
|
||||
child: _BoardColumn(
|
||||
title: BoardStrings.inProgress,
|
||||
color: AppColors.orange20,
|
||||
textColor: AppColors.orange,
|
||||
tasks: viewModel.getTasksByStatus(
|
||||
TaskStatus.inProgress,
|
||||
),
|
||||
status: TaskStatus.inProgress,
|
||||
onTaskDropped: _updateTaskStatus,
|
||||
isValidMove: viewModel.isValidMove,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Done
|
||||
Expanded(
|
||||
child: _BoardColumn(
|
||||
title: BoardStrings.done,
|
||||
color: AppColors.secondary20,
|
||||
textColor: AppColors.secondary70,
|
||||
tasks: viewModel.getTasksByStatus(
|
||||
TaskStatus.done,
|
||||
),
|
||||
status: TaskStatus.done,
|
||||
onTaskDropped: _updateTaskStatus,
|
||||
isValidMove: viewModel.isValidMove,
|
||||
child: _BoardColumnView(
|
||||
column: columns[i],
|
||||
onCardDropped: _onCardDropped,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -187,43 +144,29 @@ class _BoardScreenState extends State<BoardScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
class _BoardColumn extends StatelessWidget {
|
||||
final String title;
|
||||
final Color color;
|
||||
final Color textColor;
|
||||
final List<BoardData> tasks;
|
||||
final TaskStatus status;
|
||||
final Function(BoardData, TaskStatus) onTaskDropped;
|
||||
final bool Function(TaskStatus, TaskStatus) isValidMove;
|
||||
class _BoardColumnView extends StatelessWidget {
|
||||
final BoardColumn column;
|
||||
final Future<void> Function(BoardCard, BoardColumn) onCardDropped;
|
||||
|
||||
const _BoardColumn({
|
||||
required this.title,
|
||||
required this.color,
|
||||
required this.textColor,
|
||||
required this.tasks,
|
||||
required this.status,
|
||||
required this.onTaskDropped,
|
||||
required this.isValidMove,
|
||||
});
|
||||
const _BoardColumnView({required this.column, required this.onCardDropped});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DragTarget<BoardData>(
|
||||
onWillAcceptWithDetails: (details) {
|
||||
return isValidMove(details.data.statusEnum, status);
|
||||
},
|
||||
onAcceptWithDetails: (details) {
|
||||
onTaskDropped(details.data, status);
|
||||
},
|
||||
final accent = _parseHexColor(column.color) ?? AppColors.textBlue;
|
||||
return DragTarget<BoardCard>(
|
||||
onWillAcceptWithDetails: (details) => details.data.columnId != column.id,
|
||||
onAcceptWithDetails: (details) => onCardDropped(details.data, column),
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
final isHovering = candidateData.isNotEmpty;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isHovering ? color.withValues(alpha: 0.3) : AppColors.primary0,
|
||||
isHovering
|
||||
? accent.withValues(alpha: 0.15)
|
||||
: AppColors.primary0,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isHovering ? textColor : AppColors.borderBlue,
|
||||
color: isHovering ? accent : AppColors.borderBlue,
|
||||
width: isHovering ? 2 : 1,
|
||||
),
|
||||
),
|
||||
@@ -235,26 +178,25 @@ class _BoardColumn extends StatelessWidget {
|
||||
height: 34,
|
||||
margin: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
color: accent.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
title,
|
||||
_columnHeader(column),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor,
|
||||
color: accent,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: tasks.length,
|
||||
itemCount: column.cards.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _TaskCard(task: tasks[index]);
|
||||
return _TaskCard(card: column.cards[index], accent: accent);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -264,21 +206,25 @@ class _BoardColumn extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _columnHeader(BoardColumn column) {
|
||||
final name = column.name ?? '';
|
||||
final limit = column.limit;
|
||||
if (limit == null || limit <= 0) return name;
|
||||
return '$name (${column.cards.length}/$limit)';
|
||||
}
|
||||
}
|
||||
|
||||
class _TaskCard extends StatelessWidget {
|
||||
final BoardData task;
|
||||
final BoardCard card;
|
||||
final Color accent;
|
||||
|
||||
const _TaskCard({required this.task});
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
const _TaskCard({required this.card, required this.accent});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Draggable<BoardData>(
|
||||
data: task,
|
||||
return Draggable<BoardCard>(
|
||||
data: card,
|
||||
feedback: Material(
|
||||
elevation: 8,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -319,19 +265,17 @@ class _TaskCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildCardContent(BuildContext context) {
|
||||
final due = card.dueDate;
|
||||
final labels = card.labels;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (due != null)
|
||||
Container(
|
||||
height: 24.0,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0.w()),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
task.statusEnum == TaskStatus.toDo
|
||||
? AppColors.primary20
|
||||
: task.statusEnum == TaskStatus.inProgress
|
||||
? AppColors.orange10
|
||||
: AppColors.secondary10,
|
||||
color: accent.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
@@ -346,7 +290,7 @@ class _TaskCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatDate(task.deadline ?? DateTime.now()),
|
||||
timeConvertor(due),
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -356,19 +300,56 @@ class _TaskCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (due != null) const SizedBox(height: 12),
|
||||
Text(
|
||||
task.description!,
|
||||
style: TextStyle(fontSize: 14, color: AppColors.grey70),
|
||||
card.title ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey70,
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (card.priority != null) _PriorityChip(priority: card.priority!),
|
||||
if (labels.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [for (final label in labels) _LabelChip(label: label)],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
SvgPicture.asset(AssetsManager.profile),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
card.tenderId ?? '',
|
||||
style: TextStyle(fontSize: 12, color: AppColors.grey60),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Container(
|
||||
class _PriorityChip extends StatelessWidget {
|
||||
final String priority;
|
||||
const _PriorityChip({required this.priority});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (bg, fg, dot) = _styleFor(priority);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: task.isReady ?? false ? AppColors.primary20 : AppColors.red0,
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
@@ -377,37 +358,63 @@ class _TaskCard extends StatelessWidget {
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: task.isReady! ? AppColors.textBlue : AppColors.error,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
decoration: BoxDecoration(color: dot, shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
task.isReady!
|
||||
? BoardStrings.readyLabel
|
||||
: BoardStrings.incompleteLabel,
|
||||
priority,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: task.isReady! ? AppColors.textBlue : AppColors.red20,
|
||||
color: fg,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
(Color bg, Color fg, Color dot) _styleFor(String priority) {
|
||||
switch (priority.toLowerCase()) {
|
||||
case 'urgent':
|
||||
case 'high':
|
||||
return (AppColors.red0, AppColors.red20, AppColors.error);
|
||||
case 'low':
|
||||
return (AppColors.primary20, AppColors.textBlue, AppColors.textBlue);
|
||||
case 'medium':
|
||||
default:
|
||||
return (AppColors.orange10, AppColors.orange, AppColors.orange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _LabelChip extends StatelessWidget {
|
||||
final String label;
|
||||
const _LabelChip({required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary20,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
SvgPicture.asset(AssetsManager.profile),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
task.assignedTo!,
|
||||
style: TextStyle(fontSize: 12, color: AppColors.grey60),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 11, color: AppColors.textBlue),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Color? _parseHexColor(String? hex) {
|
||||
if (hex == null) return null;
|
||||
var value = hex.trim();
|
||||
if (value.startsWith('#')) value = value.substring(1);
|
||||
if (value.length == 6) value = 'FF$value';
|
||||
if (value.length != 8) return null;
|
||||
final parsed = int.tryParse(value, radix: 16);
|
||||
if (parsed == null) return null;
|
||||
return Color(parsed);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,9 @@ class BoardStrings {
|
||||
BoardStrings._();
|
||||
|
||||
static const String boardTitle = 'My Board';
|
||||
static const String toDo = 'To Do';
|
||||
static const String inProgress = 'In Progress';
|
||||
static const String done = 'Done';
|
||||
static const String emptyListText = 'No tasks found';
|
||||
static const String deadlineLabel = 'Deadline';
|
||||
static const String readyLabel = 'Ready';
|
||||
static const String incompleteLabel = 'Incomplete';
|
||||
static String taskMovedMessage(String oldStatus, String newStatus) =>
|
||||
'Task moved from $oldStatus to $newStatus';
|
||||
static const String moveFailed = 'Could not move card';
|
||||
static String cardMovedMessage(String fromColumn, String toColumn) =>
|
||||
'Card moved from $fromColumn to $toColumn';
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import '../../../core/utils/size_config.dart';
|
||||
import '../../shared/responsive_builder.dart';
|
||||
|
||||
class CompletionOfDocumentsScreen extends StatefulWidget {
|
||||
const CompletionOfDocumentsScreen({super.key});
|
||||
final String? tenderId;
|
||||
const CompletionOfDocumentsScreen({this.tenderId, super.key});
|
||||
|
||||
@override
|
||||
State<CompletionOfDocumentsScreen> createState() => _CompletionOfDocumentsScreenState();
|
||||
@@ -41,10 +42,10 @@ class _CompletionOfDocumentsScreenState extends State<CompletionOfDocumentsScree
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SizeConfig.init(context);
|
||||
return const ResponsiveBuilder(
|
||||
mobile: CompletionOfDocumentsMobilePage(),
|
||||
tablet: CompletionOfDocumentsTabletPage(),
|
||||
desktop: CompletionOfDocumentsDesktopPage(),
|
||||
return ResponsiveBuilder(
|
||||
mobile: CompletionOfDocumentsMobilePage(tenderId: widget.tenderId),
|
||||
tablet: CompletionOfDocumentsTabletPage(tenderId: widget.tenderId),
|
||||
desktop: CompletionOfDocumentsDesktopPage(tenderId: widget.tenderId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:tm_app/core/constants/assets.dart';
|
||||
import 'package:tm_app/core/theme/colors.dart';
|
||||
import 'package:tm_app/core/utils/size_config.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tm_app/core/utils/app_toast.dart';
|
||||
import 'package:tm_app/view_models/tender_detail_view_model.dart';
|
||||
import 'package:tm_app/views/completion_of_documents/strings/completion_of_documents_string.dart';
|
||||
import 'package:tm_app/views/shared/base_button.dart';
|
||||
import 'package:tm_app/views/shared/desktop_navigation_widget.dart';
|
||||
@@ -10,7 +14,8 @@ import 'package:tm_app/views/shared/tablet_desktop_appbar.dart'
|
||||
show TabletDesktopAppbar;
|
||||
|
||||
class CompletionOfDocumentsDesktopPage extends StatelessWidget {
|
||||
const CompletionOfDocumentsDesktopPage({super.key});
|
||||
final String? tenderId;
|
||||
const CompletionOfDocumentsDesktopPage({this.tenderId, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -77,15 +82,18 @@ class CompletionOfDocumentsDesktopPage extends StatelessWidget {
|
||||
|
||||
SizedBox(height: 24.0.h()),
|
||||
|
||||
BaseButton(
|
||||
isEnabled: true,
|
||||
onPressed: () {},
|
||||
Consumer<TenderDetailViewModel>(
|
||||
builder: (context, viewModel, _) => BaseButton(
|
||||
isEnabled: !viewModel.isSubmitApprovalLoading,
|
||||
isLoading: viewModel.isSubmitApprovalLoading,
|
||||
onPressed: () => _submitForCompletion(context),
|
||||
text:
|
||||
CompletionOfDocumentsStrings
|
||||
.submitForCompletion,
|
||||
textColor: AppColors.textBlue,
|
||||
backgroundColor: AppColors.primary30,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24.0.h()),
|
||||
],
|
||||
),
|
||||
@@ -304,6 +312,26 @@ class CompletionOfDocumentsDesktopPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _submitForCompletion(BuildContext context) async {
|
||||
final id = tenderId;
|
||||
if (id == null || id.isEmpty) {
|
||||
AppToast.error(context, CompletionOfDocumentsStrings.submitError);
|
||||
return;
|
||||
}
|
||||
final viewModel = context.read<TenderDetailViewModel>();
|
||||
final success = await viewModel.submitForCompletion(tenderId: id);
|
||||
if (!context.mounted) return;
|
||||
if (success) {
|
||||
AppToast.success(context, CompletionOfDocumentsStrings.submitSuccess);
|
||||
context.pop();
|
||||
} else {
|
||||
AppToast.error(
|
||||
context,
|
||||
viewModel.errorMessage ?? CompletionOfDocumentsStrings.submitError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _docCard({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
|
||||
@@ -3,13 +3,18 @@ import 'package:flutter_svg/svg.dart';
|
||||
import 'package:tm_app/core/constants/assets.dart';
|
||||
import 'package:tm_app/core/theme/colors.dart';
|
||||
import 'package:tm_app/core/utils/size_config.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tm_app/core/utils/app_toast.dart';
|
||||
import 'package:tm_app/view_models/tender_detail_view_model.dart';
|
||||
import 'package:tm_app/views/completion_of_documents/strings/completion_of_documents_string.dart';
|
||||
import 'package:tm_app/views/shared/base_button.dart';
|
||||
|
||||
import '../../shared/tender_app_bar.dart';
|
||||
|
||||
class CompletionOfDocumentsMobilePage extends StatelessWidget {
|
||||
const CompletionOfDocumentsMobilePage({super.key});
|
||||
final String? tenderId;
|
||||
const CompletionOfDocumentsMobilePage({this.tenderId, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -242,13 +247,16 @@ class CompletionOfDocumentsMobilePage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24.0.h()),
|
||||
BaseButton(
|
||||
isEnabled: true,
|
||||
onPressed: () {},
|
||||
Consumer<TenderDetailViewModel>(
|
||||
builder: (context, viewModel, _) => BaseButton(
|
||||
isEnabled: !viewModel.isSubmitApprovalLoading,
|
||||
isLoading: viewModel.isSubmitApprovalLoading,
|
||||
onPressed: () => _submitForCompletion(context),
|
||||
text: CompletionOfDocumentsStrings.submitForCompletion,
|
||||
textColor: AppColors.textBlue,
|
||||
backgroundColor: AppColors.primary30,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -256,6 +264,26 @@ class CompletionOfDocumentsMobilePage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _submitForCompletion(BuildContext context) async {
|
||||
final id = tenderId;
|
||||
if (id == null || id.isEmpty) {
|
||||
AppToast.error(context, CompletionOfDocumentsStrings.submitError);
|
||||
return;
|
||||
}
|
||||
final viewModel = context.read<TenderDetailViewModel>();
|
||||
final success = await viewModel.submitForCompletion(tenderId: id);
|
||||
if (!context.mounted) return;
|
||||
if (success) {
|
||||
AppToast.success(context, CompletionOfDocumentsStrings.submitSuccess);
|
||||
context.pop();
|
||||
} else {
|
||||
AppToast.error(
|
||||
context,
|
||||
viewModel.errorMessage ?? CompletionOfDocumentsStrings.submitError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _docCard({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
|
||||
@@ -3,6 +3,10 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:tm_app/core/constants/assets.dart';
|
||||
import 'package:tm_app/core/theme/colors.dart';
|
||||
import 'package:tm_app/core/utils/size_config.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tm_app/core/utils/app_toast.dart';
|
||||
import 'package:tm_app/view_models/tender_detail_view_model.dart';
|
||||
import 'package:tm_app/views/completion_of_documents/strings/completion_of_documents_string.dart';
|
||||
import 'package:tm_app/views/shared/base_button.dart';
|
||||
import 'package:tm_app/views/shared/tablet_desktop_appbar.dart';
|
||||
@@ -10,7 +14,8 @@ import 'package:tm_app/views/shared/tablet_navigation_widget.dart';
|
||||
import 'package:tm_app/views/shared/tender_app_bar.dart';
|
||||
|
||||
class CompletionOfDocumentsTabletPage extends StatelessWidget {
|
||||
const CompletionOfDocumentsTabletPage({super.key});
|
||||
final String? tenderId;
|
||||
const CompletionOfDocumentsTabletPage({this.tenderId, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -83,15 +88,18 @@ class CompletionOfDocumentsTabletPage extends StatelessWidget {
|
||||
|
||||
SizedBox(height: 24.0.h()),
|
||||
|
||||
BaseButton(
|
||||
isEnabled: true,
|
||||
onPressed: () {},
|
||||
Consumer<TenderDetailViewModel>(
|
||||
builder: (context, viewModel, _) => BaseButton(
|
||||
isEnabled: !viewModel.isSubmitApprovalLoading,
|
||||
isLoading: viewModel.isSubmitApprovalLoading,
|
||||
onPressed: () => _submitForCompletion(context),
|
||||
text:
|
||||
CompletionOfDocumentsStrings
|
||||
.submitForCompletion,
|
||||
textColor: AppColors.textBlue,
|
||||
backgroundColor: AppColors.primary30,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -309,6 +317,26 @@ class CompletionOfDocumentsTabletPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _submitForCompletion(BuildContext context) async {
|
||||
final id = tenderId;
|
||||
if (id == null || id.isEmpty) {
|
||||
AppToast.error(context, CompletionOfDocumentsStrings.submitError);
|
||||
return;
|
||||
}
|
||||
final viewModel = context.read<TenderDetailViewModel>();
|
||||
final success = await viewModel.submitForCompletion(tenderId: id);
|
||||
if (!context.mounted) return;
|
||||
if (success) {
|
||||
AppToast.success(context, CompletionOfDocumentsStrings.submitSuccess);
|
||||
context.pop();
|
||||
} else {
|
||||
AppToast.error(
|
||||
context,
|
||||
viewModel.errorMessage ?? CompletionOfDocumentsStrings.submitError,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- Document Card --------------------
|
||||
Widget _docCard({
|
||||
required String title,
|
||||
|
||||
@@ -7,4 +7,6 @@ class CompletionOfDocumentsStrings {
|
||||
'Set Deadline for Completion of documents :';
|
||||
static const String note = 'Note:';
|
||||
static const String submitForCompletion = 'Submit for completion';
|
||||
static const String submitSuccess = 'Tender submitted for completion';
|
||||
static const String submitError = 'Failed to submit tender for completion';
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ class DeadlineItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (detail.submissionDeadline == null && detail.applicationDeadline == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return isScreenBig
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0),
|
||||
@@ -51,7 +54,7 @@ class DeadlineItem extends StatelessWidget {
|
||||
),
|
||||
SizedBox(height: 4.0.h()),
|
||||
Text(
|
||||
timeConvertor(detail.submissionDeadline!),
|
||||
timeConvertor(detail.submissionDeadline),
|
||||
style: TextStyle(
|
||||
color: AppColors.grey80,
|
||||
fontWeight: FontWeight.w400,
|
||||
@@ -80,7 +83,7 @@ class DeadlineItem extends StatelessWidget {
|
||||
),
|
||||
SizedBox(height: 4.0.h()),
|
||||
Text(
|
||||
timeConvertor(detail.applicationDeadline!),
|
||||
timeConvertor(detail.applicationDeadline),
|
||||
style: TextStyle(
|
||||
color: AppColors.grey80,
|
||||
fontWeight: FontWeight.w400,
|
||||
@@ -124,7 +127,7 @@ class DeadlineItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
timeConvertor(detail.submissionDeadline!),
|
||||
timeConvertor(detail.submissionDeadline),
|
||||
style: TextStyle(
|
||||
color: AppColors.grey70,
|
||||
fontWeight: FontWeight.w400,
|
||||
@@ -146,7 +149,7 @@ class DeadlineItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
timeConvertor(detail.applicationDeadline!),
|
||||
timeConvertor(detail.applicationDeadline),
|
||||
style: TextStyle(
|
||||
color: AppColors.grey70,
|
||||
fontWeight: FontWeight.w400,
|
||||
|
||||
@@ -75,9 +75,9 @@ class _TenderDetailActionsState extends State<TenderDetailActions> {
|
||||
// submissionMode: value,
|
||||
// );
|
||||
|
||||
const CompletionOfDocumentsRouteData().push(
|
||||
context,
|
||||
);
|
||||
CompletionOfDocumentsRouteData(
|
||||
tenderId: widget.detail.id!,
|
||||
).push(context);
|
||||
} else if (value ==
|
||||
TenderSubmissionMode.partnership.value) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
@@ -213,8 +213,9 @@ class _TenderDetailActionsState extends State<TenderDetailActions> {
|
||||
const FinalCompletionOfDocumentsRouteData()
|
||||
.push(context);
|
||||
} else {
|
||||
const CompletionOfDocumentsRouteData()
|
||||
.push(context);
|
||||
CompletionOfDocumentsRouteData(
|
||||
tenderId: widget.detail.id!,
|
||||
).push(context);
|
||||
}
|
||||
}
|
||||
} else if (value ==
|
||||
|
||||
@@ -41,8 +41,7 @@ class DesktopHomePage extends StatelessWidget {
|
||||
}
|
||||
|
||||
if (homeViewModel.tenderApprovalsStateResponse != null &&
|
||||
homeViewModel.data != null &&
|
||||
homeViewModel.feedbackStats != null) {
|
||||
homeViewModel.data != null) {
|
||||
return Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
@@ -181,13 +180,15 @@ class DesktopHomePage extends StatelessWidget {
|
||||
backgroundColor: AppColors.grey10,
|
||||
iconPath: AssetsManager.thumbLike,
|
||||
title: HomeStrings.likedTenders,
|
||||
amount: homeViewModel.feedbackStats?.totalLikes.toString() ?? '0',
|
||||
amount: homeViewModel.userLikedTendersCount.toString(),
|
||||
textColor: AppColors.grey50,
|
||||
enableTap: (homeViewModel.feedbackStats?.totalLikes ?? 0) != 0,
|
||||
enableTap: true,
|
||||
width: 178,
|
||||
height: 148,
|
||||
onTap: () {
|
||||
const LikedTendersRouteData().push(context);
|
||||
const LikedTendersRouteData().push(context).then((value) {
|
||||
homeViewModel.init();
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -33,8 +33,7 @@ class MobileHomePage extends StatelessWidget {
|
||||
}
|
||||
|
||||
if (homeViewModel.tenderApprovalsStateResponse != null &&
|
||||
homeViewModel.data != null &&
|
||||
homeViewModel.feedbackStats != null) {
|
||||
homeViewModel.data != null) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -156,9 +155,9 @@ class MobileHomePage extends StatelessWidget {
|
||||
backgroundColor: AppColors.grey10,
|
||||
iconPath: AssetsManager.thumbLike,
|
||||
title: HomeStrings.likedTenders,
|
||||
amount: homeViewModel.feedbackStats?.totalLikes.toString() ?? '0',
|
||||
amount: homeViewModel.userLikedTendersCount.toString(),
|
||||
textColor: AppColors.grey50,
|
||||
enableTap: (homeViewModel.feedbackStats?.totalLikes ?? 0) != 0,
|
||||
enableTap: true,
|
||||
onTap: () {
|
||||
const LikedTendersRouteData().push(context).then((value) {
|
||||
homeViewModel.init();
|
||||
|
||||
@@ -41,8 +41,7 @@ class TabletHomePage extends StatelessWidget {
|
||||
}
|
||||
|
||||
if (homeViewModel.tenderApprovalsStateResponse != null &&
|
||||
homeViewModel.data != null &&
|
||||
homeViewModel.feedbackStats != null) {
|
||||
homeViewModel.data != null) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.fromSTEB(
|
||||
@@ -186,13 +185,15 @@ class TabletHomePage extends StatelessWidget {
|
||||
backgroundColor: AppColors.grey10,
|
||||
iconPath: AssetsManager.thumbLike,
|
||||
title: HomeStrings.likedTenders,
|
||||
amount: homeViewModel.feedbackStats?.totalLikes.toString() ?? '0',
|
||||
amount: homeViewModel.userLikedTendersCount.toString(),
|
||||
textColor: AppColors.grey50,
|
||||
enableTap: (homeViewModel.feedbackStats?.totalLikes ?? 0) != 0,
|
||||
enableTap: true,
|
||||
width: double.infinity,
|
||||
height: 148,
|
||||
onTap: () {
|
||||
const LikedTendersRouteData().push(context);
|
||||
const LikedTendersRouteData().push(context).then((value) {
|
||||
homeViewModel.init();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -34,7 +34,7 @@ class TendersListItem extends StatelessWidget {
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
timeConvertor(tender.publicationDate ?? 0),
|
||||
timeConvertor(tender.publicationDate),
|
||||
style: TextStyle(
|
||||
fontSize: 12.0.sp(),
|
||||
fontWeight: FontWeight.w400,
|
||||
@@ -64,7 +64,7 @@ class TendersListItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
timeConvertor(tender.tenderDeadline ?? 0),
|
||||
timeConvertor(tender.tenderDeadline),
|
||||
style: TextStyle(
|
||||
fontSize: 14.0.sp(),
|
||||
fontWeight: FontWeight.w500,
|
||||
|
||||
@@ -70,7 +70,7 @@ class LikedListItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
timeConvertor(tender.tenderDeadline!),
|
||||
timeConvertor(tender.tenderDeadline),
|
||||
style: TextStyle(
|
||||
fontSize: 14.0.sp(),
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -117,7 +117,7 @@ class LikedListItem extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
timeConvertor(tender.tenderDeadline!),
|
||||
timeConvertor(tender.tenderDeadline),
|
||||
style: TextStyle(
|
||||
fontSize: 14.0.sp(),
|
||||
fontWeight: FontWeight.w500,
|
||||
|
||||
@@ -87,16 +87,25 @@ class _DesktopNotificationPageState extends State<DesktopNotificationPage>
|
||||
padding: EdgeInsetsDirectional.only(end: 24.0.w()),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
viewModel.markAllAsRead(context);
|
||||
},
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final hasNotifications = (viewModel
|
||||
.allNotificationResponse
|
||||
?.data
|
||||
?.isNotEmpty ??
|
||||
false);
|
||||
return InkWell(
|
||||
onTap: hasNotifications
|
||||
? () => viewModel.markAllAsRead(context)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
child: Container(
|
||||
width: 132.0.w(),
|
||||
height: 32.0.h(),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary20,
|
||||
color: hasNotifications
|
||||
? AppColors.primary20
|
||||
: AppColors.grey20,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
child: Center(
|
||||
@@ -105,11 +114,15 @@ class _DesktopNotificationPageState extends State<DesktopNotificationPage>
|
||||
style: TextStyle(
|
||||
fontSize: 14.0.sp(),
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.mainBlue,
|
||||
color: hasNotifications
|
||||
? AppColors.mainBlue
|
||||
: AppColors.grey50,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -101,16 +101,23 @@ class _MobileNotificationPageState extends State<MobileNotificationPage>
|
||||
padding: EdgeInsetsDirectional.only(end: 24.0.w()),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
viewModel.markAllAsRead(context);
|
||||
},
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final hasNotifications =
|
||||
(viewModel.allNotificationResponse?.data?.isNotEmpty ??
|
||||
false);
|
||||
return InkWell(
|
||||
onTap: hasNotifications
|
||||
? () => viewModel.markAllAsRead(context)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
child: Container(
|
||||
width: 132.0.w(),
|
||||
height: 32.0.h(),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary20,
|
||||
color: hasNotifications
|
||||
? AppColors.primary20
|
||||
: AppColors.grey20,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
child: Center(
|
||||
@@ -119,11 +126,15 @@ class _MobileNotificationPageState extends State<MobileNotificationPage>
|
||||
style: TextStyle(
|
||||
fontSize: 14.0.sp(),
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.mainBlue,
|
||||
color: hasNotifications
|
||||
? AppColors.mainBlue
|
||||
: AppColors.grey50,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -90,16 +90,25 @@ class _TabletNotificationPageState extends State<TabletNotificationPage>
|
||||
padding: EdgeInsetsDirectional.only(end: 24.0.w()),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
viewModel.markAllAsRead(context);
|
||||
},
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final hasNotifications = (viewModel
|
||||
.allNotificationResponse
|
||||
?.data
|
||||
?.isNotEmpty ??
|
||||
false);
|
||||
return InkWell(
|
||||
onTap: hasNotifications
|
||||
? () => viewModel.markAllAsRead(context)
|
||||
: null,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
child: Container(
|
||||
width: 132.0.w(),
|
||||
height: 32.0.h(),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary20,
|
||||
color: hasNotifications
|
||||
? AppColors.primary20
|
||||
: AppColors.grey20,
|
||||
borderRadius: BorderRadius.circular(99),
|
||||
),
|
||||
child: Center(
|
||||
@@ -108,11 +117,15 @@ class _TabletNotificationPageState extends State<TabletNotificationPage>
|
||||
style: TextStyle(
|
||||
fontSize: 14.0.sp(),
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.mainBlue,
|
||||
color: hasNotifications
|
||||
? AppColors.mainBlue
|
||||
: AppColors.grey50,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -23,4 +23,5 @@ class ProfileStrings {
|
||||
static const String mainDataTab = 'Main Data';
|
||||
static const String membersTab = 'Members';
|
||||
static const String subjectTab = 'Subject';
|
||||
static const String appVersion = 'Version 1.0.7';
|
||||
}
|
||||
|
||||
@@ -98,6 +98,15 @@ class MainDataTab extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24.0.h()),
|
||||
Text(
|
||||
ProfileStrings.appVersion,
|
||||
style: TextStyle(
|
||||
fontSize: 12.0.sp(),
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.grey70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -9,11 +9,11 @@ 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/base_button.dart';
|
||||
import 'package:tm_app/views/shared/desktop_navigation_widget.dart';
|
||||
import 'package:tm_app/views/shared/page_selection_dialog.dart';
|
||||
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';
|
||||
@@ -95,10 +95,13 @@ class _DesktopTendersPageState extends State<DesktopTendersPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
_DesktopPagination(
|
||||
SizedBox(
|
||||
width: 740,
|
||||
child: WindowedPagination(
|
||||
currentPage: vm.currentPageIndex,
|
||||
totalPages: vm.totalPages,
|
||||
currentPage: vm.currentPage,
|
||||
onPageSelected: vm.jumpToPage,
|
||||
onPageChanged: vm.handlePaginationChange,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -264,93 +267,3 @@ class _ActionButtons extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _DesktopPagination extends StatelessWidget {
|
||||
final int totalPages;
|
||||
final int currentPage;
|
||||
final ValueChanged<int> onPageSelected;
|
||||
|
||||
const _DesktopPagination({
|
||||
required this.totalPages,
|
||||
required this.currentPage,
|
||||
required this.onPageSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 680,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12.0.h()),
|
||||
child: Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
Text(
|
||||
TendersStrings.page,
|
||||
style: TextStyle(
|
||||
fontSize: 13.0.sp(),
|
||||
fontWeight: FontWeight.w300,
|
||||
color: AppColors.grey60,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10.0.w()),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
final selectedPage = await showDialog<int>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => PageSelectionDialog(
|
||||
totalPages: totalPages,
|
||||
currentPage: currentPage,
|
||||
),
|
||||
);
|
||||
if (selectedPage != null) {
|
||||
onPageSelected(selectedPage);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: AppColors.grey30, width: 1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: 5.0.w()),
|
||||
Text(
|
||||
'$currentPage',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0.sp(),
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey70,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5.0.w()),
|
||||
Icon(Icons.arrow_drop_down, color: AppColors.grey80),
|
||||
SizedBox(width: 5.0.w()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5.0.w()),
|
||||
Text(
|
||||
TendersStrings.of,
|
||||
style: TextStyle(
|
||||
fontSize: 14.0.sp(),
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.grey60,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5.0.w()),
|
||||
Text(
|
||||
'$totalPages',
|
||||
style: TextStyle(
|
||||
fontSize: 13.0.sp(),
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppColors.grey60,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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});
|
||||
@@ -61,13 +62,29 @@ class _MobileTendersPageState extends State<MobileTendersPage> {
|
||||
return const Center(child: Text(CommonStrings.noData));
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 18.0.h()),
|
||||
child: MainTendersSlider(
|
||||
tenders: viewModel.tendersResponse?.data?.tenders ?? [],
|
||||
key: ValueKey(
|
||||
'tenders-slider-${viewModel.currentPageIndex}',
|
||||
),
|
||||
tenders:
|
||||
viewModel.tendersResponse?.data?.tenders ?? [],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
WindowedPagination(
|
||||
currentPage: viewModel.currentPageIndex,
|
||||
totalPages: viewModel.totalPages,
|
||||
onPageChanged: viewModel.handlePaginationChange,
|
||||
compact: true,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@ 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';
|
||||
@@ -53,16 +54,30 @@ class _TabletTendersPageState extends State<TabletTendersPage> {
|
||||
viewModel.isLoading == false) {
|
||||
return const Center(child: Text(CommonStrings.noData));
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: 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,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -27,31 +27,6 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
|
||||
final PageController pageController = PageController(initialPage: 0);
|
||||
int currentPage = 1;
|
||||
|
||||
ScrollController? _scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (widget.isDesktop) {
|
||||
_scrollController = ScrollController();
|
||||
_scrollController!.addListener(_onScroll);
|
||||
}
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (!widget.isDesktop) {
|
||||
return;
|
||||
}
|
||||
final viewModel = context.read<TendersViewModel>();
|
||||
if (_scrollController!.position.pixels >=
|
||||
_scrollController!.position.maxScrollExtent - 200 &&
|
||||
viewModel.hasMoreData &&
|
||||
!viewModel.isLoadingMore) {
|
||||
viewModel.loadMoreTenders();
|
||||
}
|
||||
}
|
||||
|
||||
void _goToPreviousPage() {
|
||||
if (currentPage == 1) {
|
||||
return;
|
||||
@@ -107,7 +82,6 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
|
||||
@override
|
||||
void dispose() {
|
||||
pageController.dispose();
|
||||
_scrollController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -141,12 +115,6 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
|
||||
if (!viewModel.hasFeedbackForTender(id)) {
|
||||
viewModel.getTenderFeedback(id);
|
||||
}
|
||||
|
||||
if (index >= widget.tenders.length - 1 &&
|
||||
viewModel.hasMoreData &&
|
||||
!viewModel.isLoadingMore) {
|
||||
viewModel.loadMoreTenders();
|
||||
}
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final tender = widget.tenders[index];
|
||||
|
||||
@@ -115,7 +115,7 @@ class TenderCard extends StatelessWidget {
|
||||
|
||||
Widget _dateText() {
|
||||
return Text(
|
||||
timeConvertor(tender.publicationDate ?? 0),
|
||||
timeConvertor(tender.publicationDate),
|
||||
style: TextStyle(
|
||||
color: AppColors.grey60,
|
||||
fontSize: 12.0.sp(),
|
||||
@@ -150,7 +150,7 @@ class TenderCard extends StatelessWidget {
|
||||
),
|
||||
SizedBox(width: 4.0.w()),
|
||||
Text(
|
||||
timeConvertor(tender.submissionDeadline ?? 0),
|
||||
timeConvertor(tender.submissionDeadline),
|
||||
style: TextStyle(
|
||||
color: AppColors.grey80,
|
||||
fontSize: 14.0.sp(),
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ class TenderCard extends StatelessWidget {
|
||||
children: [
|
||||
// Date
|
||||
Text(
|
||||
timeConvertor(int.parse(tender.publicationDate!.toString())),
|
||||
timeConvertor(tender.publicationDate),
|
||||
style: TextStyle(
|
||||
color: AppColors.grey60,
|
||||
fontSize: 12.0.sp(),
|
||||
|
||||
+319
-295
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,56 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tm_app/core/utils/date_utils.dart';
|
||||
|
||||
void main() {
|
||||
group('timeConvertor', () {
|
||||
test('returns dash for null, zero, and negative', () {
|
||||
expect(timeConvertor(null), '-');
|
||||
expect(timeConvertor(0), '-');
|
||||
expect(timeConvertor(-1), '-');
|
||||
});
|
||||
|
||||
test('formats valid seconds as yyyy-MM-dd', () {
|
||||
final formatted = timeConvertor(1700000000);
|
||||
expect(formatted, matches(RegExp(r'^\d{4}-\d{2}-\d{2}$')));
|
||||
});
|
||||
|
||||
test('formats valid seconds with time when hasTime is true', () {
|
||||
final formatted = timeConvertor(1700000000, hasTime: true);
|
||||
expect(
|
||||
formatted,
|
||||
matches(RegExp(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$')),
|
||||
);
|
||||
});
|
||||
|
||||
test('treats values above 9999999999 as milliseconds', () {
|
||||
// 1700000000000 ms and 1700000000 s should format to the same day.
|
||||
expect(timeConvertor(1700000000000), timeConvertor(1700000000));
|
||||
});
|
||||
});
|
||||
|
||||
group('unixTimestampFromJson', () {
|
||||
test('parses int and num', () {
|
||||
expect(unixTimestampFromJson(1700000000), 1700000000);
|
||||
expect(unixTimestampFromJson(1700000000.7), 1700000000);
|
||||
});
|
||||
|
||||
test('parses numeric strings', () {
|
||||
expect(unixTimestampFromJson('1700000000'), 1700000000);
|
||||
expect(unixTimestampFromJson(' 1700000000 '), 1700000000);
|
||||
});
|
||||
|
||||
test('returns null for empty, null, and non-numeric input', () {
|
||||
expect(unixTimestampFromJson(null), isNull);
|
||||
expect(unixTimestampFromJson(''), isNull);
|
||||
expect(unixTimestampFromJson('abc'), isNull);
|
||||
expect(unixTimestampFromJson(true), isNull);
|
||||
});
|
||||
|
||||
test('rejects non-positive values', () {
|
||||
expect(unixTimestampFromJson(0), isNull);
|
||||
expect(unixTimestampFromJson(-1), isNull);
|
||||
expect(unixTimestampFromJson('-1'), isNull);
|
||||
expect(unixTimestampFromJson('0'), isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user