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:
a.jamali
2026-05-30 18:47:21 +03:30
61 changed files with 2097 additions and 1766 deletions
-2
View File
@@ -20,7 +20,5 @@
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict> </dict>
</plist> </plist>
+1 -1
View File
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project # 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. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+25 -3
View File
@@ -14,6 +14,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -55,6 +56,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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 */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -62,6 +64,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -79,6 +82,7 @@
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@@ -142,6 +146,9 @@
productType = "com.apple.product-type.bundle.unit-test"; productType = "com.apple.product-type.bundle.unit-test";
}; };
97C146ED1CF9000F007C117D /* Runner */ = { 97C146ED1CF9000F007C117D /* Runner */ = {
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
@@ -165,6 +172,9 @@
/* Begin PBXProject section */ /* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
BuildIndependentTargetsInParallel = YES; BuildIndependentTargetsInParallel = YES;
@@ -346,7 +356,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@@ -472,7 +482,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -523,7 +533,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@@ -611,6 +621,18 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* 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 */; 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 <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -26,6 +44,7 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
@@ -54,6 +73,7 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0" launchStyle = "0"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"
+5 -2
View File
@@ -2,12 +2,15 @@ import Flutter
import UIKit import UIKit
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
} }
+25 -4
View File
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
@@ -24,6 +26,29 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <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> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
@@ -41,9 +66,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict> </dict>
</plist> </plist>
+1
View File
@@ -121,6 +121,7 @@ List<SingleChildWidget> get repositories {
(context) => HomeRepository( (context) => HomeRepository(
homeService: context.read(), homeService: context.read(),
yourTendersService: context.read(), yourTendersService: context.read(),
likedTendersService: context.read(),
), ),
lazy: true, lazy: true,
), ),
-29
View File
@@ -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);
}
}
+5 -2
View File
@@ -250,11 +250,14 @@ class FinalCompletionOfDocumentsRouteData extends GoRouteData
@TypedGoRoute<CompletionOfDocumentsRouteData>(path: '/completion_of_documents') @TypedGoRoute<CompletionOfDocumentsRouteData>(path: '/completion_of_documents')
class CompletionOfDocumentsRouteData extends GoRouteData class CompletionOfDocumentsRouteData extends GoRouteData
with _$CompletionOfDocumentsRouteData { with _$CompletionOfDocumentsRouteData {
const CompletionOfDocumentsRouteData(); final String? tenderId;
const CompletionOfDocumentsRouteData({this.tenderId});
@override @override
Widget build(BuildContext context, GoRouterState state) { Widget build(BuildContext context, GoRouterState state) {
return const CompletionOfDocumentsScreen(); return tenderDetailProvider(
child: CompletionOfDocumentsScreen(tenderId: tenderId),
);
} }
} }
+12 -2
View File
@@ -308,10 +308,20 @@ RouteBase get $completionOfDocumentsRouteData => GoRouteData.$route(
mixin _$CompletionOfDocumentsRouteData on GoRouteData { mixin _$CompletionOfDocumentsRouteData on GoRouteData {
static CompletionOfDocumentsRouteData _fromState(GoRouterState state) => static CompletionOfDocumentsRouteData _fromState(GoRouterState state) =>
const CompletionOfDocumentsRouteData(); CompletionOfDocumentsRouteData(
tenderId: state.uri.queryParameters['tender-id'],
);
CompletionOfDocumentsRouteData get _self =>
this as CompletionOfDocumentsRouteData;
@override @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 @override
void go(BuildContext context) => context.go(location); void go(BuildContext context) => context.go(location);
+36 -4
View File
@@ -1,9 +1,41 @@
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
String timeConvertor(int timestamp) { /// Parses an API timestamp field that may arrive as int, double, or string.
// Convert seconds → milliseconds /// Returns null for missing, empty, unparseable, or non-positive values
final date = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); /// (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); return DateFormat('yyyy-MM-dd').format(date);
} }
+12 -2
View File
@@ -7,7 +7,17 @@ class BoardRepository {
BoardRepository({required BoardService boardService}) BoardRepository({required BoardService boardService})
: _boardService = boardService; : _boardService = boardService;
Future<Result<BoardResponse>> getBoard() async { Future<Result<BoardResponse>> getBoard() => _boardService.getBoard();
return _boardService.getBoard();
Future<Result<MoveCardResponse>> moveCard({
required String cardId,
required String columnId,
required int newOrder,
}) {
return _boardService.moveCard(
cardId: cardId,
columnId: columnId,
newOrder: newOrder,
);
} }
} }
+26 -8
View File
@@ -1,21 +1,25 @@
import 'package:tm_app/core/utils/result.dart'; import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/services/home_service.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/recommended_tenders_response/recommended_tenders_response.dart';
import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart'; import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart';
import 'package:tm_app/data/services/model/tender_approvals_stats_response/tender_approvals_stats_response.dart'; import 'package:tm_app/data/services/model/tender_approvals_stats_response/tender_approvals_stats_response.dart';
import '../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/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'; import '../services/your_tenders_service.dart';
class HomeRepository { class HomeRepository {
HomeRepository({ HomeRepository({
required HomeService homeService, required HomeService homeService,
required YourTendersService yourTendersService, required YourTendersService yourTendersService,
required LikedTendersService likedTendersService,
}) : _homeService = homeService, }) : _homeService = homeService,
_yourTendersService = yourTendersService; _yourTendersService = yourTendersService,
_likedTendersService = likedTendersService;
final HomeService _homeService; final HomeService _homeService;
final YourTendersService _yourTendersService; final YourTendersService _yourTendersService;
final LikedTendersService _likedTendersService;
Future<Result<RecommendedTendersResponse>> getHomeRecommendedTenders({ Future<Result<RecommendedTendersResponse>> getHomeRecommendedTenders({
required int limit, required int limit,
@@ -34,11 +38,25 @@ class HomeRepository {
return _homeService.getTendersApprovalsStats(); return _homeService.getTendersApprovalsStats();
} }
Future<Result<FeedbackStatResponse>> getFeedbackStats() { /// Returns how many tenders the current user has liked (not company-wide).
return _homeService.getFeedbackStats(); Future<Result<int>> getUserLikedTendersCount() async {
} final result = await _likedTendersService.getLikedTenders(
Future<Result<Map<String, dynamic>>> checkUnreadNotifications() async { requestModel: TenderFeedbackRequestModel(
return _homeService.checkUnreadNotifications(); 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({ Future<Result<TendersResponse>> getTenders({
required int limit, required int limit,
required int offset, int? offset,
String? cursor,
String? query, String? query,
String? sortBy, String? sortBy,
String? sortOrder, String? sortOrder,
@@ -23,6 +24,7 @@ class TendersRepository {
return _tendersService.getTenders( return _tendersService.getTenders(
limit: limit, limit: limit,
offset: offset, offset: offset,
cursor: cursor,
query: query, query: query,
sortBy: sortBy, sortBy: sortBy,
sortOrder: sortOrder, sortOrder: sortOrder,
+8 -2
View File
@@ -4,14 +4,20 @@ class TendersApi {
static const String tenders = '/api/v1/tenders'; static const String tenders = '/api/v1/tenders';
static String getTenders({ static String getTenders({
required int limit, required int limit,
required int offset, int? offset,
String? cursor,
String? query, String? query,
String? sortBy, String? sortBy,
String? sortOrder, String? sortOrder,
String? publicationDateFrom, String? publicationDateFrom,
String? publicationDateTo, 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) { if (query != null && query.isNotEmpty) {
url += '&q=${Uri.encodeComponent(query)}'; url += '&q=${Uri.encodeComponent(query)}';
} }
+39 -65
View File
@@ -1,5 +1,6 @@
import '../../../core/network/network_manager.dart'; import 'dart:convert';
import '../../core/config/app_config.dart';
import '../../core/network/network_manager.dart';
import '../../core/utils/result.dart'; import '../../core/utils/result.dart';
import 'model/board_response/board_response.dart'; import 'model/board_response/board_response.dart';
@@ -9,72 +10,45 @@ class BoardService {
final NetworkManager _networkManager; final NetworkManager _networkManager;
static const _basePath = '/admin/v1/kanban/kanban';
Future<Result<BoardResponse>> getBoard() async { Future<Result<BoardResponse>> getBoard() async {
final uri = '${AppConfig.apiBaseUrl}/api/v1/board'; return _networkManager.makeRequest(
await Future.delayed(const Duration(seconds: 2)); '$_basePath/boards/default',
(json) => BoardResponse.fromJson(json),
method: 'GET',
);
}
// final result = await _networkManager.makeRequest( Future<Result<MoveCardResponse>> moveCard({
// uri, required String cardId,
// (json) => BoardResponse.fromJson(json), required String columnId,
// method: 'GET', required int newOrder,
// ); }) async {
// return result; final body = jsonEncode({
'card_id': cardId,
return Result.ok( 'column_id': columnId,
BoardResponse( 'new_order': newOrder,
success: true, });
message: 'Board data fetched successfully', return _networkManager.makeRequest(
data: tasks, '$_basePath/cards/move',
error: null, (json) => MoveCardResponse.fromJson(json),
), method: 'PUT',
data: body,
); );
} }
} }
List<BoardData> tasks = [ class MoveCardResponse {
BoardData( MoveCardResponse({required this.success, required this.message});
id: '1',
description: final bool? success;
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', final String? message;
deadline: DateTime(2025, 6, 15),
assignedTo: 'Pratyush Tewari', factory MoveCardResponse.fromJson(Map<String, dynamic> json) {
isReady: false, return MoveCardResponse(
status: 'toDo', success: json['success'] as bool?,
), message: json['message'] as String?,
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',
),
];
@@ -1,34 +1,229 @@
import 'package:freezed_annotation/freezed_annotation.dart'; import '../../../../core/utils/date_utils.dart';
import '../error/error_model.dart'; import '../error/error_model.dart';
part 'board_response.freezed.dart'; class BoardResponse {
part 'board_response.g.dart'; BoardResponse({
required this.success,
required this.message,
required this.data,
required this.error,
});
@freezed final bool? success;
abstract class BoardResponse with _$BoardResponse { final String? message;
const factory BoardResponse({ final BoardDetailData? data;
required bool? success, final ErrorModel? error;
required String? message,
required List<BoardData>? data,
required ErrorModel? error,
}) = _BoardResponse;
factory BoardResponse.fromJson(Map<String, Object?> json) => factory BoardResponse.fromJson(Map<String, dynamic> json) {
_$BoardResponseFromJson(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 class BoardDetailData {
abstract class BoardData with _$BoardData { BoardDetailData({required this.board});
const factory BoardData({
required String? id,
required String? description,
required DateTime? deadline,
required String? assignedTo,
required bool? isReady,
required String? status,
}) = _BoardData;
factory BoardData.fromJson(Map<String, Object?> json) => final Board? board;
_$BoardDataFromJson(json);
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,
};
+3
View File
@@ -13,6 +13,9 @@ abstract class Meta with _$Meta {
required int? page, required int? page,
required int? pages, required int? pages,
required int? total, required int? total,
@JsonKey(name: 'has_more') bool? hasMore,
@JsonKey(name: 'next_cursor') String? nextCursor,
@JsonKey(name: 'prev_cursor') String? prevCursor,
}) = _Meta; }) = _Meta;
factory Meta.fromJson(Map<String, dynamic> json) => _$MetaFromJson(json); factory Meta.fromJson(Map<String, dynamic> json) => _$MetaFromJson(json);
+29 -20
View File
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$Meta { 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 /// Create a copy of Meta
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $MetaCopyWith<Meta> get copyWith => _$MetaCopyWithImpl<Meta>(this as Meta, _$ide
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory $MetaCopyWith(Meta value, $Res Function(Meta) _then) = _$MetaCopyWithImpl;
@useResult @useResult
$Res call({ $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 /// Create a copy of Meta
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? 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( return _then(_self.copyWith(
limit: freezed == limit ? _self.limit : limit // ignore: cast_nullable_to_non_nullable 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?,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?,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?,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?,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) { switch (_that) {
case _Meta() when $default != null: 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(); 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) { switch (_that) {
case _Meta(): 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'); 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) { switch (_that) {
case _Meta() when $default != null: 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; return null;
} }
@@ -213,7 +216,7 @@ return $default(_that.limit,_that.offset,_that.page,_that.pages,_that.total);cas
@JsonSerializable() @JsonSerializable()
class _Meta implements Meta { 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); factory _Meta.fromJson(Map<String, dynamic> json) => _$MetaFromJson(json);
@override final int? limit; @override final int? limit;
@@ -221,6 +224,9 @@ class _Meta implements Meta {
@override final int? page; @override final int? page;
@override final int? pages; @override final int? pages;
@override final int? total; @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 /// Create a copy of Meta
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -235,16 +241,16 @@ Map<String, dynamic> toJson() {
@override @override
bool operator ==(Object other) { 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) @JsonKey(includeFromJson: false, includeToJson: false)
@override @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 @override
String toString() { 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; factory _$MetaCopyWith(_Meta value, $Res Function(_Meta) _then) = __$MetaCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $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 /// Create a copy of Meta
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? 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( return _then(_Meta(
limit: freezed == limit ? _self.limit : limit // ignore: cast_nullable_to_non_nullable 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?,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?,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?,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?,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?,
)); ));
} }
+6
View File
@@ -12,6 +12,9 @@ _Meta _$MetaFromJson(Map<String, dynamic> json) => _Meta(
page: (json['page'] as num?)?.toInt(), page: (json['page'] as num?)?.toInt(),
pages: (json['pages'] as num?)?.toInt(), pages: (json['pages'] as num?)?.toInt(),
total: (json['total'] 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>{ Map<String, dynamic> _$MetaToJson(_Meta instance) => <String, dynamic>{
@@ -20,4 +23,7 @@ Map<String, dynamic> _$MetaToJson(_Meta instance) => <String, dynamic>{
'page': instance.page, 'page': instance.page,
'pages': instance.pages, 'pages': instance.pages,
'total': instance.total, '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 // ignore_for_file: invalid_annotation_target
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:tm_app/core/utils/date_utils.dart';
import 'package:tm_app/data/services/model/organization/organization.dart'; import 'package:tm_app/data/services/model/organization/organization.dart';
part 'tender_data.freezed.dart'; part 'tender_data.freezed.dart';
@@ -15,7 +16,8 @@ abstract class TenderData with _$TenderData {
@JsonKey(name: 'tender_id') required String? tenderId, @JsonKey(name: 'tender_id') required String? tenderId,
@JsonKey(name: 'notice_publication_id') @JsonKey(name: 'notice_publication_id')
required String? noticePublicationId, required String? noticePublicationId,
@JsonKey(name: 'publication_date') required int? publicationDate, @JsonKey(name: 'publication_date', fromJson: unixTimestampFromJson)
required int? publicationDate,
required String? title, required String? title,
required String? description, required String? description,
@JsonKey(name: 'procurement_type_code') @JsonKey(name: 'procurement_type_code')
@@ -24,9 +26,12 @@ abstract class TenderData with _$TenderData {
@JsonKey(name: 'estimated_value') required int? estimatedValue, @JsonKey(name: 'estimated_value') required int? estimatedValue,
required String? currency, required String? currency,
required String? duration, required String? duration,
@JsonKey(name: 'tender_deadline') required int? tenderDeadline, @JsonKey(name: 'tender_deadline', fromJson: unixTimestampFromJson)
@JsonKey(name: 'submission_deadline') required int? submissionDeadline, required int? tenderDeadline,
@JsonKey(name: 'application_deadline') required int? applicationDeadline, @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: 'submission_url') required String? submissionUrl,
@JsonKey(name: 'country_code') required String? countryCode, @JsonKey(name: 'country_code') required String? countryCode,
@JsonKey(name: 'duration_unit') required String? durationUnit, @JsonKey(name: 'duration_unit') required String? durationUnit,
@@ -12,7 +12,7 @@ _TenderData _$TenderDataFromJson(Map<String, dynamic> json) => _TenderData(
id: json['id'] as String?, id: json['id'] as String?,
tenderId: json['tender_id'] as String?, tenderId: json['tender_id'] as String?,
noticePublicationId: json['notice_publication_id'] as String?, noticePublicationId: json['notice_publication_id'] as String?,
publicationDate: (json['publication_date'] as num?)?.toInt(), publicationDate: unixTimestampFromJson(json['publication_date']),
title: json['title'] as String?, title: json['title'] as String?,
description: json['description'] as String?, description: json['description'] as String?,
procurementTypeCode: json['procurement_type_code'] 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(), estimatedValue: (json['estimated_value'] as num?)?.toInt(),
currency: json['currency'] as String?, currency: json['currency'] as String?,
duration: json['duration'] as String?, duration: json['duration'] as String?,
tenderDeadline: (json['tender_deadline'] as num?)?.toInt(), tenderDeadline: unixTimestampFromJson(json['tender_deadline']),
submissionDeadline: (json['submission_deadline'] as num?)?.toInt(), submissionDeadline: unixTimestampFromJson(json['submission_deadline']),
applicationDeadline: (json['application_deadline'] as num?)?.toInt(), applicationDeadline: unixTimestampFromJson(json['application_deadline']),
submissionUrl: json['submission_url'] as String?, submissionUrl: json['submission_url'] as String?,
countryCode: json['country_code'] as String?, countryCode: json['country_code'] as String?,
durationUnit: json['duration_unit'] as String?, durationUnit: json['duration_unit'] as String?,
+3 -2
View File
@@ -16,7 +16,8 @@ class TendersService {
Future<Result<TendersResponse>> getTenders({ Future<Result<TendersResponse>> getTenders({
required int limit, required int limit,
required int offset, int? offset,
String? cursor,
String? query, String? query,
String? sortBy, String? sortBy,
String? sortOrder, String? sortOrder,
@@ -27,6 +28,7 @@ class TendersService {
TendersApi.getTenders( TendersApi.getTenders(
limit: limit, limit: limit,
offset: offset, offset: offset,
cursor: cursor,
query: query, query: query,
sortBy: sortBy, sortBy: sortBy,
sortOrder: sortOrder, sortOrder: sortOrder,
@@ -37,7 +39,6 @@ class TendersService {
method: 'GET', method: 'GET',
(json) => TendersResponse.fromJson(json), (json) => TendersResponse.fromJson(json),
); );
print(result);
return result; return result;
} }
+5 -4
View File
@@ -2,6 +2,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@@ -130,6 +131,9 @@ class _MyAppState extends State<MyApp> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Listen to theme changes // Listen to theme changes
final themeProvider = context.watch<ThemeProvider>(); final themeProvider = context.watch<ThemeProvider>();
final pageTransition = kIsWeb
? const FadeUpwardsPageTransitionsBuilder()
: const CupertinoPageTransitionsBuilder();
return ToastificationWrapper( return ToastificationWrapper(
child: MaterialApp.router( child: MaterialApp.router(
@@ -147,10 +151,7 @@ class _MyAppState extends State<MyApp> {
pageTransitionsTheme: PageTransitionsTheme( pageTransitionsTheme: PageTransitionsTheme(
builders: { builders: {
for (final platform in TargetPlatform.values) for (final platform in TargetPlatform.values)
platform: platform: pageTransition,
kIsWeb
? const FadeUpwardsPageTransitionsBuilder()
: const CupertinoPageTransitionsBuilder(),
}, },
), ),
brightness: brightness:
+84 -34
View File
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../core/enums/task_status.dart';
import '../core/utils/result.dart'; import '../core/utils/result.dart';
import '../data/repositories/board_repositories.dart'; import '../data/repositories/board_repositories.dart';
import '../data/services/board_service.dart';
import '../data/services/model/board_response/board_response.dart'; import '../data/services/model/board_response/board_response.dart';
class BoardViewModel with ChangeNotifier { class BoardViewModel with ChangeNotifier {
@@ -12,23 +12,27 @@ class BoardViewModel with ChangeNotifier {
bool _isLoading = false; bool _isLoading = false;
String? _errorMessage; String? _errorMessage;
List<BoardData> _boardData = []; Board? _board;
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage; String? get errorMessage => _errorMessage;
List<BoardData> get boardData => _boardData; Board? get board => _board;
bool get hasData => _boardData.isNotEmpty; List<BoardColumn> get columns => _board?.columns ?? const [];
bool get hasData => _board != null && columns.isNotEmpty;
Future<void> getBoard() async { Future<void> getBoard() async {
_isLoading = true; _isLoading = true;
_errorMessage = null;
notifyListeners(); notifyListeners();
final result = await _boardRepository.getBoard(); final result = await _boardRepository.getBoard();
switch (result) { switch (result) {
case Ok<BoardResponse>(): case Ok<BoardResponse>():
// تبدیل به لیست قابل تغییر _board = result.value.data?.board;
_boardData = List.from(result.value.data ?? []); if (_board == null) {
_errorMessage = result.value.error?.message ?? 'No board data';
}
break; break;
case Error<BoardResponse>(): case Error<BoardResponse>():
_errorMessage = result.error.toString(); _errorMessage = result.error.toString();
@@ -38,40 +42,86 @@ class BoardViewModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
// update task status /// Look up the column a given card currently lives in.
void updateTaskStatus(String taskId, String newStatus) { BoardColumn? columnForCard(String cardId) {
final taskIndex = _boardData.indexWhere((task) => task.id == taskId); for (final column in columns) {
if (taskIndex != -1) { if (column.cards.any((c) => c.id == cardId)) return column;
_boardData[taskIndex] = _boardData[taskIndex].copyWith(status: newStatus);
notifyListeners();
} }
return null;
} }
bool isValidMove(TaskStatus currentStatus, TaskStatus targetStatus) { /// Move a card to [targetColumnId] at the end of that column.
if (currentStatus == TaskStatus.toDo) { /// Optimistically updates local state, then calls the API; rolls back on
return targetStatus == TaskStatus.inProgress; /// failure.
} else if (currentStatus == TaskStatus.inProgress) { Future<Result<MoveCardResponse>> moveCard({
return targetStatus == TaskStatus.toDo || targetStatus == TaskStatus.done; required BoardCard card,
} else if (currentStatus == TaskStatus.done) { required BoardColumn targetColumn,
return targetStatus == TaskStatus.inProgress; }) async {
} final cardId = card.id;
return false; final targetColumnId = targetColumn.id;
} if (cardId == null || targetColumnId == null || _board == null) {
return Result.error(Exception('Invalid card or column'));
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; 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'));
}
// update task status in ViewModel final previousColumns = _board!.columns;
updateTaskStatus(task.id!, newStatus.value); final newOrder = targetColumn.cards.length;
// _showStatusChangeMessage(context, task, oldStatus, newStatus); _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();
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;
}
_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;
}
} }
} }
+16 -10
View File
@@ -1,13 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tm_app/core/utils/result.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/recommended_tenders_response/recommended_tenders_response.dart';
import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart'; import 'package:tm_app/data/services/model/tender_approvals_response/tender_approvals_response.dart';
import 'package:tm_app/data/services/model/tender_approvals_stats_response/tender_approvals_stats_response.dart'; import 'package:tm_app/data/services/model/tender_approvals_stats_response/tender_approvals_stats_response.dart';
import 'package:tm_app/data/services/model/tender_data/tender_data.dart'; import 'package:tm_app/data/services/model/tender_data/tender_data.dart';
import '../data/repositories/home_repository.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/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/model/request_models/get_tenders_request_model/get_tenders_request_model.dart';
import '../data/services/notification_state_service.dart'; import '../data/services/notification_state_service.dart';
@@ -46,7 +44,7 @@ class HomeViewModel with ChangeNotifier {
RecommendedTendersResponse? _recommendedTendersResponse; RecommendedTendersResponse? _recommendedTendersResponse;
TenderApprovalsStatsResponse? tenderApprovalsStateResponse; TenderApprovalsStatsResponse? tenderApprovalsStateResponse;
FeedbackStats? feedbackStats; int userLikedTendersCount = 0;
final List<TenderData> _tenders = []; final List<TenderData> _tenders = [];
final int _pageSize = 10; final int _pageSize = 10;
int _currentPage = 0; int _currentPage = 0;
@@ -65,11 +63,19 @@ class HomeViewModel with ChangeNotifier {
void init() async { void init() async {
_isLoading = true; _isLoading = true;
_errorMessage = null;
userLikedTendersCount = 0;
_tenders.clear();
_currentPage = 0;
_hasMore = true;
_isRecommendedMode = false;
data = null;
tenderApprovalsStateResponse = null;
notifyListeners(); notifyListeners();
await getApprovedStates(); await getApprovedStates();
await getYourTenders(reset: true); await getYourTenders(reset: true);
await getFeedbackStats(); await loadUserLikedTendersCount();
if (_errorMessage != null) { if (_errorMessage != null) {
_isLoading = false; _isLoading = false;
@@ -192,15 +198,15 @@ class HomeViewModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<void> getFeedbackStats() async { Future<void> loadUserLikedTendersCount() async {
final result = await _homeRepository.getFeedbackStats(); final result = await _homeRepository.getUserLikedTendersCount();
switch (result) { switch (result) {
case Ok<FeedbackStatResponse>(): case Ok<int>():
feedbackStats = result.value.data; userLikedTendersCount = result.value;
break; break;
case Error<FeedbackStatResponse>(): case Error<int>():
_errorMessage = result.error.toString(); userLikedTendersCount = 0;
break; break;
} }
notifyListeners(); 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 '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_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_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_data/tender_approvals_data.dart';
import '../data/services/model/tender_approvals_toggle_response/tender_approvals_toggle_response.dart'; import '../data/services/model/tender_approvals_toggle_response/tender_approvals_toggle_response.dart';
@@ -143,4 +144,33 @@ class TenderDetailViewModel with ChangeNotifier {
_errorMessage = null; _errorMessage = null;
notifyListeners(); 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;
}
} }
+147 -124
View File
@@ -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/repositories/tenders_repository.dart';
import 'package:tm_app/data/services/model/feedback_data/feedback_data.dart'; import 'package:tm_app/data/services/model/feedback_data/feedback_data.dart';
import 'package:tm_app/data/services/model/feedback_response/feedback_response.dart'; import 'package:tm_app/data/services/model/feedback_response/feedback_response.dart';
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/data/services/model/tenders/tenders_response/tenders_response.dart';
import 'package:tm_app/views/tenders/strings/tenders_strings.dart'; import 'package:tm_app/views/tenders/strings/tenders_strings.dart';
@@ -36,15 +37,44 @@ class TendersViewModel with ChangeNotifier {
List<FeedbackData> feedbacks = []; List<FeedbackData> feedbacks = [];
int currentPage = 1;
int _currentOffset = 0;
int _totalPages = 0;
int get totalPages => _totalPages;
static const int _pageSize = 10; 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; bool get hasMoreData => _hasMoreData;
/// 0-based current page index.
int get currentPageIndex {
final meta = _tendersResponse?.meta;
final limit = meta?.limit ?? _lastValidLimit;
if (limit > 0) {
return _currentOffset ~/ limit;
}
if (meta?.offset != null && limit > 0) {
return meta!.offset! ~/ limit;
}
if (meta?.page != null) {
return meta!.page! - 1;
}
return 0;
}
/// 1-based current page (for display).
int get currentPage => currentPageIndex + 1;
int get totalPages {
final pages = _tendersResponse?.meta?.pages;
if (pages != null && pages > 0) {
return pages;
}
return _lastValidPages;
}
// Filter States // Filter States
DateTimeRange? selectedDateRange; DateTimeRange? selectedDateRange;
String selectedStatus = 'All'; String selectedStatus = 'All';
@@ -80,15 +110,12 @@ class TendersViewModel with ChangeNotifier {
} }
void setStatus(String status) { void setStatus(String status) {
// Clear data when status changes
_clearAllData(); _clearAllData();
selectedStatus = status; selectedStatus = status;
currentPage = 1;
notifyListeners(); notifyListeners();
} }
void setSort(String sort) { void setSort(String sort) {
// Clear data when status changes
_clearAllData(); _clearAllData();
selectedSort = sort; selectedSort = sort;
if (sort == TendersStrings.minimumTime) { if (sort == TendersStrings.minimumTime) {
@@ -98,16 +125,13 @@ class TendersViewModel with ChangeNotifier {
sortBy = 'submission_deadline'; sortBy = 'submission_deadline';
sortOrder = 'desc'; sortOrder = 'desc';
} }
currentPage = 1;
notifyListeners(); notifyListeners();
} }
void _clearAllData() { void _clearAllData() {
_tendersResponse = null; _tendersResponse = null;
_errorMessage = null; _errorMessage = null;
_currentOffset = 0; _resetPagination();
_hasMoreData = true;
currentPage = 1;
sortBy = ''; sortBy = '';
sortOrder = ''; sortOrder = '';
selectedSort = ''; selectedSort = '';
@@ -116,6 +140,11 @@ class TendersViewModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void _resetPagination() {
_currentOffset = 0;
_hasMoreData = true;
}
void init(BuildContext context) async { void init(BuildContext context) async {
_isLoading = true; _isLoading = true;
notifyListeners(); notifyListeners();
@@ -151,10 +180,7 @@ class TendersViewModel with ChangeNotifier {
} }
Future<void> getTendersMobile({String? query}) async { Future<void> getTendersMobile({String? query}) async {
// Clear all data before fetching new tenders _resetPagination();
_currentOffset = 0;
_hasMoreData = true;
_searchQuery = query; _searchQuery = query;
final result = await _tendersRepository.getTenders( final result = await _tendersRepository.getTenders(
@@ -164,28 +190,7 @@ class TendersViewModel with ChangeNotifier {
sortBy: '', sortBy: '',
sortOrder: '', sortOrder: '',
); );
switch (result) { _handleListResult(result, fetchAllFeedbacks: false);
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;
}
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
_errorMessage = null; _errorMessage = null;
@@ -199,11 +204,9 @@ class TendersViewModel with ChangeNotifier {
String? publicationDateFrom, String? publicationDateFrom,
String? publicationDateTo, String? publicationDateTo,
}) async { }) async {
// Clear all data before fetching new tenders
_isLoading = true; _isLoading = true;
notifyListeners(); notifyListeners();
_currentOffset = 0; _resetPagination();
_hasMoreData = true;
_searchQuery = query; _searchQuery = query;
final result = await _tendersRepository.getTenders( final result = await _tendersRepository.getTenders(
@@ -215,35 +218,55 @@ class TendersViewModel with ChangeNotifier {
publicationDateFrom: publicationDateFrom ?? '', publicationDateFrom: publicationDateFrom ?? '',
publicationDateTo: publicationDateTo ?? '', publicationDateTo: publicationDateTo ?? '',
); );
switch (result) { _handleListResult(result, fetchAllFeedbacks: true);
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;
}
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
_errorMessage = null; _errorMessage = null;
notifyListeners(); 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 { Future<void> loadMoreTenders() async {
if (_isLoadingMore || !_hasMoreData) { if (_isLoadingMore || !_hasMoreData) {
return; return;
@@ -253,7 +276,10 @@ class TendersViewModel with ChangeNotifier {
_errorMessage = null; _errorMessage = null;
notifyListeners(); notifyListeners();
_currentOffset += _pageSize; final meta = _tendersResponse?.meta;
final limit = meta?.limit ?? _pageSize;
_currentOffset += limit;
final result = await _tendersRepository.getTenders( final result = await _tendersRepository.getTenders(
limit: _pageSize, limit: _pageSize,
offset: _currentOffset, offset: _currentOffset,
@@ -264,30 +290,31 @@ class TendersViewModel with ChangeNotifier {
switch (result) { switch (result) {
case Ok<TendersResponse>(): case Ok<TendersResponse>():
if (result.value.data?.tenders != null && final newTenders = result.value.data?.tenders ?? <TenderData>[];
result.value.data!.tenders!.isNotEmpty) { if (newTenders.isNotEmpty) {
// Create a new modifiable list by combining existing and new tenders final existingTenders =
if (_tendersResponse?.data?.tenders != null) { _tendersResponse?.data?.tenders ?? <TenderData>[];
final existingTenders = _tendersResponse!.data!.tenders!; final combined = <TenderData>[...existingTenders, ...newTenders];
final newTenders = result.value.data!.tenders!; _tendersResponse = result.value.copyWith(
final combinedTenders = [...existingTenders, ...newTenders]; data: result.value.data?.copyWith(tenders: combined),
);
// Create a new TendersData with the combined list final newMeta = result.value.meta;
_tendersResponse = _tendersResponse!.copyWith( if (newMeta?.pages != null && newMeta!.pages! > 0) {
data: _tendersResponse!.data!.copyWith(tenders: combinedTenders), _lastValidPages = newMeta.pages!;
);
} else {
_tendersResponse = result.value;
} }
// Check if we have more data if (newMeta?.limit != null && newMeta!.limit! > 0) {
_hasMoreData = result.value.data!.tenders!.length >= _pageSize; _lastValidLimit = newMeta.limit!;
}
_hasMoreData =
newMeta?.hasMore ?? (newTenders.length >= _pageSize);
} else { } else {
_hasMoreData = false; _hasMoreData = false;
} }
break; break;
case Error<TendersResponse>(): case Error<TendersResponse>():
_errorMessage = result.error.toString(); final limit = meta?.limit ?? _pageSize;
_currentOffset -= _pageSize; // Revert offset on error _currentOffset = (_currentOffset - limit).clamp(0, 1 << 30);
_handleListError(result.error);
break; break;
} }
@@ -298,7 +325,6 @@ class TendersViewModel with ChangeNotifier {
} }
Future<void> getTenderFeedback(String tenderId) async { Future<void> getTenderFeedback(String tenderId) async {
// _isLoading = true;
_errorMessage = null; _errorMessage = null;
notifyListeners(); notifyListeners();
@@ -308,7 +334,6 @@ class TendersViewModel with ChangeNotifier {
switch (result) { switch (result) {
case Ok<FeedbackResponse>(): case Ok<FeedbackResponse>():
_feedbackData = result.value.data; _feedbackData = result.value.data;
// Add or update _feedbackData in feedbacks list by tender id
if (_feedbackData != null) { if (_feedbackData != null) {
final index = feedbacks.indexWhere( final index = feedbacks.indexWhere(
(f) => f.tenderId == _feedbackData!.tenderId, (f) => f.tenderId == _feedbackData!.tenderId,
@@ -331,7 +356,6 @@ class TendersViewModel with ChangeNotifier {
_errorMessage = result.error.toString(); _errorMessage = result.error.toString();
break; break;
} }
// _isLoading = false;
notifyListeners(); notifyListeners();
_errorMessage = null; _errorMessage = null;
notifyListeners(); notifyListeners();
@@ -343,38 +367,32 @@ class TendersViewModel with ChangeNotifier {
}) async { }) async {
_feedbackErrorMessage = null; _feedbackErrorMessage = null;
// Get current feedback for this tender
final existingFeedback = getFeedbackForTender(tenderId); final existingFeedback = getFeedbackForTender(tenderId);
final existingFeedbackIndex = feedbacks.indexWhere( final existingFeedbackIndex = feedbacks.indexWhere(
(f) => f.tenderId == tenderId, (f) => f.tenderId == tenderId,
); );
// Create optimistic feedback data
FeedbackData? optimisticFeedback; FeedbackData? optimisticFeedback;
if (existingFeedback == null) { if (existingFeedback == null) {
// No existing feedback - create new optimistic feedback
optimisticFeedback = FeedbackData( optimisticFeedback = FeedbackData(
companyId: null, // Will be filled by server companyId: null,
createdAt: DateTime.now().millisecondsSinceEpoch, createdAt: DateTime.now().millisecondsSinceEpoch,
customerId: null, // Will be filled by server customerId: null,
feedbackType: feedbackType, feedbackType: feedbackType,
id: null, // Will be filled by server id: null,
tenderId: tenderId, tenderId: tenderId,
tender: null, tender: null,
updatedAt: DateTime.now().millisecondsSinceEpoch, updatedAt: DateTime.now().millisecondsSinceEpoch,
); );
feedbacks.add(optimisticFeedback); feedbacks.add(optimisticFeedback);
} else { } else {
// Existing feedback - update optimistically
if (existingFeedback.feedbackType == feedbackType) { if (existingFeedback.feedbackType == feedbackType) {
// Same feedback type - remove feedback (toggle off)
if (existingFeedbackIndex != -1) { if (existingFeedbackIndex != -1) {
feedbacks.removeAt(existingFeedbackIndex); feedbacks.removeAt(existingFeedbackIndex);
} }
optimisticFeedback = null; optimisticFeedback = null;
} else { } else {
// Different feedback type - update to new type
optimisticFeedback = existingFeedback.copyWith( optimisticFeedback = existingFeedback.copyWith(
feedbackType: feedbackType, feedbackType: feedbackType,
updatedAt: DateTime.now().millisecondsSinceEpoch, updatedAt: DateTime.now().millisecondsSinceEpoch,
@@ -385,10 +403,8 @@ class TendersViewModel with ChangeNotifier {
} }
} }
// Notify listeners immediately for optimistic update
notifyListeners(); notifyListeners();
// Call API
final result = await _tendersRepository.toggleFeedback( final result = await _tendersRepository.toggleFeedback(
tenderId: tenderId, tenderId: tenderId,
feedbackType: feedbackType, feedbackType: feedbackType,
@@ -398,7 +414,6 @@ class TendersViewModel with ChangeNotifier {
case Ok<FeedbackResponse>(): case Ok<FeedbackResponse>():
_feedbackData = result.value.data; _feedbackData = result.value.data;
if (_feedbackData != null) { if (_feedbackData != null) {
// Update with complete data from server
final index = feedbacks.indexWhere( final index = feedbacks.indexWhere(
(f) => f.tender == _feedbackData!.tender, (f) => f.tender == _feedbackData!.tender,
); );
@@ -408,17 +423,13 @@ class TendersViewModel with ChangeNotifier {
feedbacks.add(_feedbackData!); feedbacks.add(_feedbackData!);
} }
} else { } else {
// Server returned null data - remove feedback
feedbacks.removeWhere((f) => f.tenderId == tenderId); feedbacks.removeWhere((f) => f.tenderId == tenderId);
} }
break; break;
case Error<FeedbackResponse>(): case Error<FeedbackResponse>():
// Revert optimistic update on error
if (existingFeedback == null) { if (existingFeedback == null) {
// Remove the optimistic feedback we added
feedbacks.removeWhere((f) => f.tenderId == tenderId); feedbacks.removeWhere((f) => f.tenderId == tenderId);
} else { } else {
// Restore the original feedback - find current index and update, or add if not found
final currentIndex = feedbacks.indexWhere( final currentIndex = feedbacks.indexWhere(
(f) => f.tenderId == tenderId, (f) => f.tenderId == tenderId,
); );
@@ -437,7 +448,6 @@ class TendersViewModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
/// Clear all feedbacks
void clearFeedbacks() { void clearFeedbacks() {
feedbacks.clear(); feedbacks.clear();
_feedbackData = null; _feedbackData = null;
@@ -445,13 +455,10 @@ class TendersViewModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
/// Clear all tenders
void clearTenders() { void clearTenders() {
_tendersResponse = null; _tendersResponse = null;
_errorMessage = null; _errorMessage = null;
_currentOffset = 0; _resetPagination();
_hasMoreData = true;
currentPage = 1;
notifyListeners(); notifyListeners();
} }
@@ -464,7 +471,6 @@ class TendersViewModel with ChangeNotifier {
); );
} }
/// Search tenders with query
Future<void> searchTenders(String query) async { Future<void> searchTenders(String query) async {
await getTendersDesktop( await getTendersDesktop(
query: query.trim(), query: query.trim(),
@@ -475,7 +481,6 @@ class TendersViewModel with ChangeNotifier {
); );
} }
/// Clear all data (tenders, feedbacks, and approvals)
void clearAll() { void clearAll() {
clearTenders(); clearTenders();
clearFeedbacks(); clearFeedbacks();
@@ -488,17 +493,32 @@ class TendersViewModel with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void jumpToPage(int page) async { /// [selected] is the absolute 0-based target page index.
final meta = tendersResponse?.meta; Future<void> handlePaginationChange(int selected) async {
if (meta?.pages == null) { if (selected < 0) {
return; return;
} }
if (page < 1 || page > meta!.pages!) { final maxPage = totalPages - 1;
if (maxPage >= 0 && selected > maxPage) {
return; return;
} }
currentPage = page; if (selected == currentPageIndex) {
_currentOffset = (page - 1) * _pageSize; 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( final result = await _tendersRepository.getTenders(
limit: _pageSize, limit: _pageSize,
@@ -506,17 +526,20 @@ class TendersViewModel with ChangeNotifier {
query: _searchQuery, query: _searchQuery,
sortBy: sortBy, sortBy: sortBy,
sortOrder: sortOrder, sortOrder: sortOrder,
publicationDateFrom: startDateUnix,
publicationDateTo: endDateUnix,
); );
switch (result) { _handleListResult(result, fetchAllFeedbacks: true);
case Ok<TendersResponse>(): _isLoading = false;
_tendersResponse = result.value; notifyListeners();
break; _errorMessage = null;
case Error<TendersResponse>():
_errorMessage = result.error.toString();
break;
}
notifyListeners(); notifyListeners();
} }
/// Kept for backwards compatibility with existing callers.
/// Accepts a 1-based page number.
void jumpToPage(int page) {
handlePaginationChange(page - 1);
}
} }
+13 -4
View File
@@ -206,7 +206,9 @@ class YourTendersViewModel with ChangeNotifier {
offset: _tendersOffset, offset: _tendersOffset,
); );
await getTenders(requestModel: requestModel); 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( final requestModel = GetTendersRequestModel(
status: TenderApprovalStatus.submitted.value, status: TenderApprovalStatus.submitted.value,
createdFrom: startDateUnix, createdFrom: startDateUnix,
@@ -299,10 +301,17 @@ class YourTendersViewModel with ChangeNotifier {
reset: true, reset: true,
); );
} else if (selectedStatus == TenderApprovalStatus.approved.value) { } else if (selectedStatus == TenderApprovalStatus.approved.value) {
await getTendersFeedback( // "Approved tenders" maps to submitted tender-approvals. The API has no
feedbackType: TenderApprovalStatus.approved.value, // `approved` value; approved tenders are the ones a company has submitted
reset: true, // 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);
} }
} }
+192 -185
View File
@@ -1,14 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.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/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/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/data/services/model/board_response/board_response.dart';
import 'package:tm_app/views/shared/desktop_navigation_widget.dart'; import 'package:tm_app/views/shared/desktop_navigation_widget.dart';
import '../../../core/constants/assets.dart'; import '../../../core/constants/assets.dart';
import '../../../core/enums/task_status.dart';
import '../../../core/utils/app_toast.dart'; import '../../../core/utils/app_toast.dart';
import '../../../view_models/board_view_model.dart'; import '../../../view_models/board_view_model.dart';
import '../strings/board_strings.dart'; import '../strings/board_strings.dart';
@@ -32,38 +33,27 @@ class _BoardScreenState extends State<BoardScreen> {
}); });
} }
void _updateTaskStatus(BoardData task, TaskStatus newStatus) { Future<void> _onCardDropped(BoardCard card, BoardColumn target) async {
if (task.statusEnum == newStatus) { final source = _boardViewModel.columnForCard(card.id ?? '');
return; if (source == null || source.id == target.id) return;
}
final oldStatus = task.statusEnum; final result = await _boardViewModel.moveCard(
card: card,
// update task status in ViewModel targetColumn: target,
_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,
};
AppToast.success(
context,
BoardStrings.taskMovedMessage(
statusNames[oldStatus]!,
statusNames[newStatus]!,
),
); );
if (!mounted) return;
if (result is Ok<MoveCardResponse>) {
AppToast.success(
context,
BoardStrings.cardMovedMessage(
source.name ?? '',
target.name ?? '',
),
);
} else if (result is Error<MoveCardResponse>) {
AppToast.error(context, BoardStrings.moveFailed);
}
} }
@override @override
@@ -73,9 +63,7 @@ class _BoardScreenState extends State<BoardScreen> {
backgroundColor: AppColors.backgroundColor, backgroundColor: AppColors.backgroundColor,
body: Column( body: Column(
children: [ children: [
const DesktopNavigationWidget( const DesktopNavigationWidget(currentIndex: 2),
currentIndex: 2, // Tenders index
),
Consumer<BoardViewModel>( Consumer<BoardViewModel>(
builder: (context, viewModel, child) { builder: (context, viewModel, child) {
if (viewModel.isLoading) { if (viewModel.isLoading) {
@@ -92,12 +80,16 @@ class _BoardScreenState extends State<BoardScreen> {
child: Center(child: Text(viewModel.errorMessage!)), child: Center(child: Text(viewModel.errorMessage!)),
); );
} }
if (viewModel.hasData == false) { if (!viewModel.hasData) {
return const Expanded( return const Expanded(
child: Center(child: Text(BoardStrings.emptyListText)), child: Center(child: Text(BoardStrings.emptyListText)),
); );
} }
final columns = [...viewModel.columns]..sort(
(a, b) => (a.order ?? 0).compareTo(b.order ?? 0),
);
return Expanded( return Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
child: SizedBox( child: SizedBox(
@@ -108,7 +100,7 @@ class _BoardScreenState extends State<BoardScreen> {
children: [ children: [
const SizedBox(height: 32), const SizedBox(height: 32),
Text( Text(
BoardStrings.boardTitle, viewModel.board?.name ?? BoardStrings.boardTitle,
style: TextStyle( style: TextStyle(
fontSize: 20.0.sp(), fontSize: 20.0.sp(),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -126,50 +118,15 @@ class _BoardScreenState extends State<BoardScreen> {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// To Do for (int i = 0; i < columns.length; i++) ...[
Expanded( if (i > 0) const SizedBox(width: 12),
child: _BoardColumn( Expanded(
title: BoardStrings.toDo, child: _BoardColumnView(
color: AppColors.primary20, column: columns[i],
textColor: const Color(0xFF1976D2), onCardDropped: _onCardDropped,
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,
),
),
], ],
), ),
), ),
@@ -187,43 +144,29 @@ class _BoardScreenState extends State<BoardScreen> {
} }
} }
class _BoardColumn extends StatelessWidget { class _BoardColumnView extends StatelessWidget {
final String title; final BoardColumn column;
final Color color; final Future<void> Function(BoardCard, BoardColumn) onCardDropped;
final Color textColor;
final List<BoardData> tasks;
final TaskStatus status;
final Function(BoardData, TaskStatus) onTaskDropped;
final bool Function(TaskStatus, TaskStatus) isValidMove;
const _BoardColumn({ const _BoardColumnView({required this.column, required this.onCardDropped});
required this.title,
required this.color,
required this.textColor,
required this.tasks,
required this.status,
required this.onTaskDropped,
required this.isValidMove,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DragTarget<BoardData>( final accent = _parseHexColor(column.color) ?? AppColors.textBlue;
onWillAcceptWithDetails: (details) { return DragTarget<BoardCard>(
return isValidMove(details.data.statusEnum, status); onWillAcceptWithDetails: (details) => details.data.columnId != column.id,
}, onAcceptWithDetails: (details) => onCardDropped(details.data, column),
onAcceptWithDetails: (details) {
onTaskDropped(details.data, status);
},
builder: (context, candidateData, rejectedData) { builder: (context, candidateData, rejectedData) {
final isHovering = candidateData.isNotEmpty; final isHovering = candidateData.isNotEmpty;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: color:
isHovering ? color.withValues(alpha: 0.3) : AppColors.primary0, isHovering
? accent.withValues(alpha: 0.15)
: AppColors.primary0,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: isHovering ? textColor : AppColors.borderBlue, color: isHovering ? accent : AppColors.borderBlue,
width: isHovering ? 2 : 1, width: isHovering ? 2 : 1,
), ),
), ),
@@ -235,26 +178,25 @@ class _BoardColumn extends StatelessWidget {
height: 34, height: 34,
margin: const EdgeInsets.all(8), margin: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color, color: accent.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
title, _columnHeader(column),
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: textColor, color: accent,
), ),
), ),
), ),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
itemCount: tasks.length, itemCount: column.cards.length,
itemBuilder: (context, index) { 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 { class _TaskCard extends StatelessWidget {
final BoardData task; final BoardCard card;
final Color accent;
const _TaskCard({required this.task}); const _TaskCard({required this.card, required this.accent});
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Draggable<BoardData>( return Draggable<BoardCard>(
data: task, data: card,
feedback: Material( feedback: Material(
elevation: 8, elevation: 8,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@@ -319,91 +265,72 @@ class _TaskCard extends StatelessWidget {
} }
Widget _buildCardContent(BuildContext context) { Widget _buildCardContent(BuildContext context) {
final due = card.dueDate;
final labels = card.labels;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Container( if (due != null)
height: 24.0, Container(
padding: EdgeInsets.symmetric(horizontal: 10.0.w()), height: 24.0,
decoration: BoxDecoration( padding: EdgeInsets.symmetric(horizontal: 10.0.w()),
color: decoration: BoxDecoration(
task.statusEnum == TaskStatus.toDo color: accent.withValues(alpha: 0.15),
? AppColors.primary20 borderRadius: BorderRadius.circular(4),
: task.statusEnum == TaskStatus.inProgress ),
? AppColors.orange10 child: Row(
: AppColors.secondary10, mainAxisAlignment: MainAxisAlignment.spaceBetween,
borderRadius: BorderRadius.circular(4), children: [
), Text(
child: Row( '${BoardStrings.deadlineLabel} :',
mainAxisAlignment: MainAxisAlignment.spaceBetween, style: TextStyle(
children: [ fontSize: 14.0,
Text( fontWeight: FontWeight.w500,
'${BoardStrings.deadlineLabel} :', color: AppColors.textBlue,
style: TextStyle( ),
fontSize: 14.0,
fontWeight: FontWeight.w500,
color: AppColors.textBlue,
), ),
), Text(
Text( timeConvertor(due),
_formatDate(task.deadline ?? DateTime.now()), style: TextStyle(
style: TextStyle( fontSize: 14.0,
fontSize: 14.0, fontWeight: FontWeight.w500,
fontWeight: FontWeight.w500, color: AppColors.grey80,
color: AppColors.grey80, ),
), ),
), ],
], ),
), ),
), if (due != null) const SizedBox(height: 12),
const SizedBox(height: 12),
Text( Text(
task.description!, card.title ?? '',
style: TextStyle(fontSize: 14, color: AppColors.grey70), style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey70,
),
maxLines: 3, maxLines: 3,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
if (card.priority != null) _PriorityChip(priority: card.priority!),
Container( if (labels.isNotEmpty) ...[
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), const SizedBox(height: 8),
decoration: BoxDecoration( Wrap(
color: task.isReady ?? false ? AppColors.primary20 : AppColors.red0, spacing: 6,
borderRadius: BorderRadius.circular(8), runSpacing: 6,
children: [for (final label in labels) _LabelChip(label: label)],
), ),
child: Row( ],
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: task.isReady! ? AppColors.textBlue : AppColors.error,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text(
task.isReady!
? BoardStrings.readyLabel
: BoardStrings.incompleteLabel,
style: TextStyle(
fontSize: 12,
color: task.isReady! ? AppColors.textBlue : AppColors.red20,
fontWeight: FontWeight.w400,
),
),
],
),
),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
children: [ children: [
SvgPicture.asset(AssetsManager.profile), SvgPicture.asset(AssetsManager.profile),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
task.assignedTo!, card.tenderId ?? '',
style: TextStyle(fontSize: 12, color: AppColors.grey60), style: TextStyle(fontSize: 12, color: AppColors.grey60),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
], ],
), ),
@@ -411,3 +338,83 @@ class _TaskCard extends StatelessWidget {
); );
} }
} }
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: bg,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(color: dot, shape: BoxShape.circle),
),
const SizedBox(width: 6),
Text(
priority,
style: TextStyle(
fontSize: 12,
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),
),
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);
}
+3 -7
View File
@@ -2,13 +2,9 @@ class BoardStrings {
BoardStrings._(); BoardStrings._();
static const String boardTitle = 'My Board'; 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 emptyListText = 'No tasks found';
static const String deadlineLabel = 'Deadline'; static const String deadlineLabel = 'Deadline';
static const String readyLabel = 'Ready'; static const String moveFailed = 'Could not move card';
static const String incompleteLabel = 'Incomplete'; static String cardMovedMessage(String fromColumn, String toColumn) =>
static String taskMovedMessage(String oldStatus, String newStatus) => 'Card moved from $fromColumn to $toColumn';
'Task moved from $oldStatus to $newStatus';
} }
@@ -7,7 +7,8 @@ import '../../../core/utils/size_config.dart';
import '../../shared/responsive_builder.dart'; import '../../shared/responsive_builder.dart';
class CompletionOfDocumentsScreen extends StatefulWidget { class CompletionOfDocumentsScreen extends StatefulWidget {
const CompletionOfDocumentsScreen({super.key}); final String? tenderId;
const CompletionOfDocumentsScreen({this.tenderId, super.key});
@override @override
State<CompletionOfDocumentsScreen> createState() => _CompletionOfDocumentsScreenState(); State<CompletionOfDocumentsScreen> createState() => _CompletionOfDocumentsScreenState();
@@ -41,10 +42,10 @@ class _CompletionOfDocumentsScreenState extends State<CompletionOfDocumentsScree
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
SizeConfig.init(context); SizeConfig.init(context);
return const ResponsiveBuilder( return ResponsiveBuilder(
mobile: CompletionOfDocumentsMobilePage(), mobile: CompletionOfDocumentsMobilePage(tenderId: widget.tenderId),
tablet: CompletionOfDocumentsTabletPage(), tablet: CompletionOfDocumentsTabletPage(tenderId: widget.tenderId),
desktop: CompletionOfDocumentsDesktopPage(), 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/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
import 'package: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/completion_of_documents/strings/completion_of_documents_string.dart';
import 'package:tm_app/views/shared/base_button.dart'; import 'package:tm_app/views/shared/base_button.dart';
import 'package:tm_app/views/shared/desktop_navigation_widget.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; show TabletDesktopAppbar;
class CompletionOfDocumentsDesktopPage extends StatelessWidget { class CompletionOfDocumentsDesktopPage extends StatelessWidget {
const CompletionOfDocumentsDesktopPage({super.key}); final String? tenderId;
const CompletionOfDocumentsDesktopPage({this.tenderId, super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -77,14 +82,17 @@ class CompletionOfDocumentsDesktopPage extends StatelessWidget {
SizedBox(height: 24.0.h()), SizedBox(height: 24.0.h()),
BaseButton( Consumer<TenderDetailViewModel>(
isEnabled: true, builder: (context, viewModel, _) => BaseButton(
onPressed: () {}, isEnabled: !viewModel.isSubmitApprovalLoading,
text: isLoading: viewModel.isSubmitApprovalLoading,
CompletionOfDocumentsStrings onPressed: () => _submitForCompletion(context),
.submitForCompletion, text:
textColor: AppColors.textBlue, CompletionOfDocumentsStrings
backgroundColor: AppColors.primary30, .submitForCompletion,
textColor: AppColors.textBlue,
backgroundColor: AppColors.primary30,
),
), ),
SizedBox(height: 24.0.h()), 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({ Widget _docCard({
required String title, required String title,
required String subtitle, 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/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
import 'package: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/completion_of_documents/strings/completion_of_documents_string.dart';
import 'package:tm_app/views/shared/base_button.dart'; import 'package:tm_app/views/shared/base_button.dart';
import '../../shared/tender_app_bar.dart'; import '../../shared/tender_app_bar.dart';
class CompletionOfDocumentsMobilePage extends StatelessWidget { class CompletionOfDocumentsMobilePage extends StatelessWidget {
const CompletionOfDocumentsMobilePage({super.key}); final String? tenderId;
const CompletionOfDocumentsMobilePage({this.tenderId, super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -242,12 +247,15 @@ class CompletionOfDocumentsMobilePage extends StatelessWidget {
), ),
), ),
SizedBox(height: 24.0.h()), SizedBox(height: 24.0.h()),
BaseButton( Consumer<TenderDetailViewModel>(
isEnabled: true, builder: (context, viewModel, _) => BaseButton(
onPressed: () {}, isEnabled: !viewModel.isSubmitApprovalLoading,
text: CompletionOfDocumentsStrings.submitForCompletion, isLoading: viewModel.isSubmitApprovalLoading,
textColor: AppColors.textBlue, onPressed: () => _submitForCompletion(context),
backgroundColor: AppColors.primary30, 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({ Widget _docCard({
required String title, required String title,
required String subtitle, 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/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
import 'package: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/completion_of_documents/strings/completion_of_documents_string.dart';
import 'package:tm_app/views/shared/base_button.dart'; import 'package:tm_app/views/shared/base_button.dart';
import 'package:tm_app/views/shared/tablet_desktop_appbar.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'; import 'package:tm_app/views/shared/tender_app_bar.dart';
class CompletionOfDocumentsTabletPage extends StatelessWidget { class CompletionOfDocumentsTabletPage extends StatelessWidget {
const CompletionOfDocumentsTabletPage({super.key}); final String? tenderId;
const CompletionOfDocumentsTabletPage({this.tenderId, super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -83,14 +88,17 @@ class CompletionOfDocumentsTabletPage extends StatelessWidget {
SizedBox(height: 24.0.h()), SizedBox(height: 24.0.h()),
BaseButton( Consumer<TenderDetailViewModel>(
isEnabled: true, builder: (context, viewModel, _) => BaseButton(
onPressed: () {}, isEnabled: !viewModel.isSubmitApprovalLoading,
text: isLoading: viewModel.isSubmitApprovalLoading,
CompletionOfDocumentsStrings onPressed: () => _submitForCompletion(context),
.submitForCompletion, text:
textColor: AppColors.textBlue, CompletionOfDocumentsStrings
backgroundColor: AppColors.primary30, .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 -------------------- // -------------------- Document Card --------------------
Widget _docCard({ Widget _docCard({
required String title, required String title,
@@ -7,4 +7,6 @@ class CompletionOfDocumentsStrings {
'Set Deadline for Completion of documents :'; 'Set Deadline for Completion of documents :';
static const String note = 'Note:'; static const String note = 'Note:';
static const String submitForCompletion = 'Submit for completion'; 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';
} }
+7 -4
View File
@@ -19,6 +19,9 @@ class DeadlineItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (detail.submissionDeadline == null && detail.applicationDeadline == null) {
return const SizedBox.shrink();
}
return isScreenBig return isScreenBig
? Padding( ? Padding(
padding: const EdgeInsets.symmetric(vertical: 0), padding: const EdgeInsets.symmetric(vertical: 0),
@@ -51,7 +54,7 @@ class DeadlineItem extends StatelessWidget {
), ),
SizedBox(height: 4.0.h()), SizedBox(height: 4.0.h()),
Text( Text(
timeConvertor(detail.submissionDeadline!), timeConvertor(detail.submissionDeadline),
style: TextStyle( style: TextStyle(
color: AppColors.grey80, color: AppColors.grey80,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
@@ -80,7 +83,7 @@ class DeadlineItem extends StatelessWidget {
), ),
SizedBox(height: 4.0.h()), SizedBox(height: 4.0.h()),
Text( Text(
timeConvertor(detail.applicationDeadline!), timeConvertor(detail.applicationDeadline),
style: TextStyle( style: TextStyle(
color: AppColors.grey80, color: AppColors.grey80,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
@@ -124,7 +127,7 @@ class DeadlineItem extends StatelessWidget {
), ),
), ),
Text( Text(
timeConvertor(detail.submissionDeadline!), timeConvertor(detail.submissionDeadline),
style: TextStyle( style: TextStyle(
color: AppColors.grey70, color: AppColors.grey70,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
@@ -146,7 +149,7 @@ class DeadlineItem extends StatelessWidget {
), ),
), ),
Text( Text(
timeConvertor(detail.applicationDeadline!), timeConvertor(detail.applicationDeadline),
style: TextStyle( style: TextStyle(
color: AppColors.grey70, color: AppColors.grey70,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
@@ -75,9 +75,9 @@ class _TenderDetailActionsState extends State<TenderDetailActions> {
// submissionMode: value, // submissionMode: value,
// ); // );
const CompletionOfDocumentsRouteData().push( CompletionOfDocumentsRouteData(
context, tenderId: widget.detail.id!,
); ).push(context);
} else if (value == } else if (value ==
TenderSubmissionMode.partnership.value) { TenderSubmissionMode.partnership.value) {
Future.delayed(Duration.zero, () { Future.delayed(Duration.zero, () {
@@ -213,8 +213,9 @@ class _TenderDetailActionsState extends State<TenderDetailActions> {
const FinalCompletionOfDocumentsRouteData() const FinalCompletionOfDocumentsRouteData()
.push(context); .push(context);
} else { } else {
const CompletionOfDocumentsRouteData() CompletionOfDocumentsRouteData(
.push(context); tenderId: widget.detail.id!,
).push(context);
} }
} }
} else if (value == } else if (value ==
+6 -5
View File
@@ -41,8 +41,7 @@ class DesktopHomePage extends StatelessWidget {
} }
if (homeViewModel.tenderApprovalsStateResponse != null && if (homeViewModel.tenderApprovalsStateResponse != null &&
homeViewModel.data != null && homeViewModel.data != null) {
homeViewModel.feedbackStats != null) {
return Expanded( return Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
child: SizedBox( child: SizedBox(
@@ -181,13 +180,15 @@ class DesktopHomePage extends StatelessWidget {
backgroundColor: AppColors.grey10, backgroundColor: AppColors.grey10,
iconPath: AssetsManager.thumbLike, iconPath: AssetsManager.thumbLike,
title: HomeStrings.likedTenders, title: HomeStrings.likedTenders,
amount: homeViewModel.feedbackStats?.totalLikes.toString() ?? '0', amount: homeViewModel.userLikedTendersCount.toString(),
textColor: AppColors.grey50, textColor: AppColors.grey50,
enableTap: (homeViewModel.feedbackStats?.totalLikes ?? 0) != 0, enableTap: true,
width: 178, width: 178,
height: 148, height: 148,
onTap: () { onTap: () {
const LikedTendersRouteData().push(context); const LikedTendersRouteData().push(context).then((value) {
homeViewModel.init();
});
}, },
), ),
], ],
+3 -4
View File
@@ -33,8 +33,7 @@ class MobileHomePage extends StatelessWidget {
} }
if (homeViewModel.tenderApprovalsStateResponse != null && if (homeViewModel.tenderApprovalsStateResponse != null &&
homeViewModel.data != null && homeViewModel.data != null) {
homeViewModel.feedbackStats != null) {
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -156,9 +155,9 @@ class MobileHomePage extends StatelessWidget {
backgroundColor: AppColors.grey10, backgroundColor: AppColors.grey10,
iconPath: AssetsManager.thumbLike, iconPath: AssetsManager.thumbLike,
title: HomeStrings.likedTenders, title: HomeStrings.likedTenders,
amount: homeViewModel.feedbackStats?.totalLikes.toString() ?? '0', amount: homeViewModel.userLikedTendersCount.toString(),
textColor: AppColors.grey50, textColor: AppColors.grey50,
enableTap: (homeViewModel.feedbackStats?.totalLikes ?? 0) != 0, enableTap: true,
onTap: () { onTap: () {
const LikedTendersRouteData().push(context).then((value) { const LikedTendersRouteData().push(context).then((value) {
homeViewModel.init(); homeViewModel.init();
+6 -5
View File
@@ -41,8 +41,7 @@ class TabletHomePage extends StatelessWidget {
} }
if (homeViewModel.tenderApprovalsStateResponse != null && if (homeViewModel.tenderApprovalsStateResponse != null &&
homeViewModel.data != null && homeViewModel.data != null) {
homeViewModel.feedbackStats != null) {
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Padding(
padding: EdgeInsetsDirectional.fromSTEB( padding: EdgeInsetsDirectional.fromSTEB(
@@ -186,13 +185,15 @@ class TabletHomePage extends StatelessWidget {
backgroundColor: AppColors.grey10, backgroundColor: AppColors.grey10,
iconPath: AssetsManager.thumbLike, iconPath: AssetsManager.thumbLike,
title: HomeStrings.likedTenders, title: HomeStrings.likedTenders,
amount: homeViewModel.feedbackStats?.totalLikes.toString() ?? '0', amount: homeViewModel.userLikedTendersCount.toString(),
textColor: AppColors.grey50, textColor: AppColors.grey50,
enableTap: (homeViewModel.feedbackStats?.totalLikes ?? 0) != 0, enableTap: true,
width: double.infinity, width: double.infinity,
height: 148, height: 148,
onTap: () { onTap: () {
const LikedTendersRouteData().push(context); const LikedTendersRouteData().push(context).then((value) {
homeViewModel.init();
});
}, },
), ),
), ),
+2 -2
View File
@@ -34,7 +34,7 @@ class TendersListItem extends StatelessWidget {
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: Text( child: Text(
timeConvertor(tender.publicationDate ?? 0), timeConvertor(tender.publicationDate),
style: TextStyle( style: TextStyle(
fontSize: 12.0.sp(), fontSize: 12.0.sp(),
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
@@ -64,7 +64,7 @@ class TendersListItem extends StatelessWidget {
), ),
), ),
Text( Text(
timeConvertor(tender.tenderDeadline ?? 0), timeConvertor(tender.tenderDeadline),
style: TextStyle( style: TextStyle(
fontSize: 14.0.sp(), fontSize: 14.0.sp(),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@@ -70,7 +70,7 @@ class LikedListItem extends StatelessWidget {
), ),
), ),
Text( Text(
timeConvertor(tender.tenderDeadline!), timeConvertor(tender.tenderDeadline),
style: TextStyle( style: TextStyle(
fontSize: 14.0.sp(), fontSize: 14.0.sp(),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@@ -117,7 +117,7 @@ class LikedListItem extends StatelessWidget {
), ),
), ),
Text( Text(
timeConvertor(tender.tenderDeadline!), timeConvertor(tender.tenderDeadline),
style: TextStyle( style: TextStyle(
fontSize: 14.0.sp(), fontSize: 14.0.sp(),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@@ -87,29 +87,42 @@ class _DesktopNotificationPageState extends State<DesktopNotificationPage>
padding: EdgeInsetsDirectional.only(end: 24.0.w()), padding: EdgeInsetsDirectional.only(end: 24.0.w()),
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: InkWell( child: Builder(
onTap: () { builder: (context) {
viewModel.markAllAsRead(context); final hasNotifications = (viewModel
}, .allNotificationResponse
borderRadius: BorderRadius.circular(99), ?.data
child: Container( ?.isNotEmpty ??
width: 132.0.w(), false);
height: 32.0.h(), return InkWell(
decoration: BoxDecoration( onTap: hasNotifications
color: AppColors.primary20, ? () => viewModel.markAllAsRead(context)
: null,
borderRadius: BorderRadius.circular(99), borderRadius: BorderRadius.circular(99),
), child: Container(
child: Center( width: 132.0.w(),
child: Text( height: 32.0.h(),
NotificationStrings.markAllAsReadButton, decoration: BoxDecoration(
style: TextStyle( color: hasNotifications
fontSize: 14.0.sp(), ? AppColors.primary20
fontWeight: FontWeight.w500, : AppColors.grey20,
color: AppColors.mainBlue, borderRadius: BorderRadius.circular(99),
),
child: Center(
child: Text(
NotificationStrings.markAllAsReadButton,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w500,
color: hasNotifications
? AppColors.mainBlue
: AppColors.grey50,
),
),
), ),
), ),
), );
), },
), ),
), ),
), ),
@@ -101,29 +101,40 @@ class _MobileNotificationPageState extends State<MobileNotificationPage>
padding: EdgeInsetsDirectional.only(end: 24.0.w()), padding: EdgeInsetsDirectional.only(end: 24.0.w()),
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: InkWell( child: Builder(
onTap: () { builder: (context) {
viewModel.markAllAsRead(context); final hasNotifications =
}, (viewModel.allNotificationResponse?.data?.isNotEmpty ??
borderRadius: BorderRadius.circular(99), false);
child: Container( return InkWell(
width: 132.0.w(), onTap: hasNotifications
height: 32.0.h(), ? () => viewModel.markAllAsRead(context)
decoration: BoxDecoration( : null,
color: AppColors.primary20,
borderRadius: BorderRadius.circular(99), borderRadius: BorderRadius.circular(99),
), child: Container(
child: Center( width: 132.0.w(),
child: Text( height: 32.0.h(),
NotificationStrings.markAllAsReadButton, decoration: BoxDecoration(
style: TextStyle( color: hasNotifications
fontSize: 14.0.sp(), ? AppColors.primary20
fontWeight: FontWeight.w500, : AppColors.grey20,
color: AppColors.mainBlue, borderRadius: BorderRadius.circular(99),
),
child: Center(
child: Text(
NotificationStrings.markAllAsReadButton,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w500,
color: hasNotifications
? AppColors.mainBlue
: AppColors.grey50,
),
),
), ),
), ),
), );
), },
), ),
), ),
), ),
@@ -90,29 +90,42 @@ class _TabletNotificationPageState extends State<TabletNotificationPage>
padding: EdgeInsetsDirectional.only(end: 24.0.w()), padding: EdgeInsetsDirectional.only(end: 24.0.w()),
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: InkWell( child: Builder(
onTap: () { builder: (context) {
viewModel.markAllAsRead(context); final hasNotifications = (viewModel
}, .allNotificationResponse
borderRadius: BorderRadius.circular(99), ?.data
child: Container( ?.isNotEmpty ??
width: 132.0.w(), false);
height: 32.0.h(), return InkWell(
decoration: BoxDecoration( onTap: hasNotifications
color: AppColors.primary20, ? () => viewModel.markAllAsRead(context)
: null,
borderRadius: BorderRadius.circular(99), borderRadius: BorderRadius.circular(99),
), child: Container(
child: Center( width: 132.0.w(),
child: Text( height: 32.0.h(),
NotificationStrings.markAllAsReadButton, decoration: BoxDecoration(
style: TextStyle( color: hasNotifications
fontSize: 14.0.sp(), ? AppColors.primary20
fontWeight: FontWeight.w500, : AppColors.grey20,
color: AppColors.mainBlue, borderRadius: BorderRadius.circular(99),
),
child: Center(
child: Text(
NotificationStrings.markAllAsReadButton,
style: TextStyle(
fontSize: 14.0.sp(),
fontWeight: FontWeight.w500,
color: hasNotifications
? AppColors.mainBlue
: AppColors.grey50,
),
),
), ),
), ),
), );
), },
), ),
), ),
), ),
@@ -23,4 +23,5 @@ class ProfileStrings {
static const String mainDataTab = 'Main Data'; static const String mainDataTab = 'Main Data';
static const String membersTab = 'Members'; static const String membersTab = 'Members';
static const String subjectTab = 'Subject'; 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,
),
),
], ],
), ),
); );
+8 -95
View File
@@ -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/view_models/tenders_view_model.dart';
import 'package:tm_app/views/shared/base_button.dart'; import 'package:tm_app/views/shared/base_button.dart';
import 'package:tm_app/views/shared/desktop_navigation_widget.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/strings/tenders_strings.dart';
import 'package:tm_app/views/tenders/widgets/main_tenders_slider.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_filter_dialog.dart';
import 'package:tm_app/views/tenders/widgets/tenders_sort_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/constants/common_strings.dart';
import '../../../core/utils/app_toast.dart'; import '../../../core/utils/app_toast.dart';
@@ -95,10 +95,13 @@ class _DesktopTendersPageState extends State<DesktopTendersPage> {
), ),
), ),
), ),
_DesktopPagination( SizedBox(
totalPages: vm.totalPages, width: 740,
currentPage: vm.currentPage, child: WindowedPagination(
onPageSelected: vm.jumpToPage, currentPage: vm.currentPageIndex,
totalPages: vm.totalPages,
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,
),
),
],
),
),
);
}
}
+23 -6
View File
@@ -9,6 +9,7 @@ import '../../../core/utils/app_toast.dart';
import '../../shared/tender_app_bar.dart'; import '../../shared/tender_app_bar.dart';
import '../strings/tenders_strings.dart'; import '../strings/tenders_strings.dart';
import '../widgets/main_tenders_slider.dart'; import '../widgets/main_tenders_slider.dart';
import '../widgets/windowed_pagination.dart';
class MobileTendersPage extends StatefulWidget { class MobileTendersPage extends StatefulWidget {
const MobileTendersPage({super.key}); const MobileTendersPage({super.key});
@@ -61,13 +62,29 @@ class _MobileTendersPageState extends State<MobileTendersPage> {
return const Center(child: Text(CommonStrings.noData)); return const Center(child: Text(CommonStrings.noData));
} }
return SingleChildScrollView( return Column(
child: Padding( children: [
padding: EdgeInsets.symmetric(vertical: 18.0.h()), Expanded(
child: MainTendersSlider( child: SingleChildScrollView(
tenders: viewModel.tendersResponse?.data?.tenders ?? [], child: Padding(
padding: EdgeInsets.symmetric(vertical: 18.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,
compact: true,
),
],
); );
}, },
), ),
+23 -8
View File
@@ -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/view_models/tenders_view_model.dart';
import 'package:tm_app/views/shared/tablet_navigation_widget.dart'; import 'package:tm_app/views/shared/tablet_navigation_widget.dart';
import 'package:tm_app/views/tenders/widgets/main_tenders_slider.dart'; import 'package:tm_app/views/tenders/widgets/main_tenders_slider.dart';
import 'package:tm_app/views/tenders/widgets/windowed_pagination.dart';
import '../../../core/constants/common_strings.dart'; import '../../../core/constants/common_strings.dart';
import '../../shared/tender_app_bar.dart'; import '../../shared/tender_app_bar.dart';
@@ -53,16 +54,30 @@ class _TabletTendersPageState extends State<TabletTendersPage> {
viewModel.isLoading == false) { viewModel.isLoading == false) {
return const Center(child: Text(CommonStrings.noData)); return const Center(child: Text(CommonStrings.noData));
} }
return SingleChildScrollView( return Column(
child: Padding( children: [
padding: EdgeInsetsDirectional.only( Expanded(
end: 24.0.w(), child: SingleChildScrollView(
top: 128.0.h(), 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 ?? [],
),
),
),
), ),
child: MainTendersSlider( WindowedPagination(
tenders: viewModel.tendersResponse?.data?.tenders ?? [], currentPage: viewModel.currentPageIndex,
totalPages: viewModel.totalPages,
onPageChanged: viewModel.handlePaginationChange,
), ),
), ],
); );
}, },
), ),
@@ -27,31 +27,6 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
final PageController pageController = PageController(initialPage: 0); final PageController pageController = PageController(initialPage: 0);
int currentPage = 1; 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() { void _goToPreviousPage() {
if (currentPage == 1) { if (currentPage == 1) {
return; return;
@@ -107,7 +82,6 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
@override @override
void dispose() { void dispose() {
pageController.dispose(); pageController.dispose();
_scrollController?.dispose();
super.dispose(); super.dispose();
} }
@@ -141,12 +115,6 @@ class _MainTendersSliderState extends State<MainTendersSlider> {
if (!viewModel.hasFeedbackForTender(id)) { if (!viewModel.hasFeedbackForTender(id)) {
viewModel.getTenderFeedback(id); viewModel.getTenderFeedback(id);
} }
if (index >= widget.tenders.length - 1 &&
viewModel.hasMoreData &&
!viewModel.isLoadingMore) {
viewModel.loadMoreTenders();
}
}, },
itemBuilder: (context, index) { itemBuilder: (context, index) {
final tender = widget.tenders[index]; final tender = widget.tenders[index];
+2 -2
View File
@@ -115,7 +115,7 @@ class TenderCard extends StatelessWidget {
Widget _dateText() { Widget _dateText() {
return Text( return Text(
timeConvertor(tender.publicationDate ?? 0), timeConvertor(tender.publicationDate),
style: TextStyle( style: TextStyle(
color: AppColors.grey60, color: AppColors.grey60,
fontSize: 12.0.sp(), fontSize: 12.0.sp(),
@@ -150,7 +150,7 @@ class TenderCard extends StatelessWidget {
), ),
SizedBox(width: 4.0.w()), SizedBox(width: 4.0.w()),
Text( Text(
timeConvertor(tender.submissionDeadline ?? 0), timeConvertor(tender.submissionDeadline),
style: TextStyle( style: TextStyle(
color: AppColors.grey80, color: AppColors.grey80,
fontSize: 14.0.sp(), 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: [ children: [
// Date // Date
Text( Text(
timeConvertor(int.parse(tender.publicationDate!.toString())), timeConvertor(tender.publicationDate),
style: TextStyle( style: TextStyle(
color: AppColors.grey60, color: AppColors.grey60,
fontSize: 12.0.sp(), fontSize: 12.0.sp(),
+319 -295
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -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);
});
});
}