From e03c87f99dbc1112d29b69409fac6ffe15cf690a Mon Sep 17 00:00:00 2001 From: amirrezaghabeli Date: Mon, 10 Nov 2025 16:14:50 +0330 Subject: [PATCH] Add board navigation and assets - Introduced new board-related assets in AssetsManager. - Updated NetworkManager to increase connection and receive timeouts to 20 seconds. - Added BoardRouteData for navigation to the BoardScreen. - Updated app routes to include the new board route. - Modified DesktopNavigationWidget to include a navigation item for the board. - Added a new string constant for the board label in TendersStrings. --- assets/icons/board.svg | 8 + assets/icons/board_active.svg | 5 + lib/core/constants/assets.dart | 2 + lib/core/network/network_manager.dart | 4 +- lib/core/routes/app_routes.dart | 11 + lib/core/routes/app_routes.g.dart | 25 + lib/views/board/board_screen.dart | 468 ++++++++++++++++++ .../shared/desktop_navigation_widget.dart | 11 +- .../tenders/strings/tenders_strings.dart | 1 + 9 files changed, 529 insertions(+), 6 deletions(-) create mode 100644 assets/icons/board.svg create mode 100644 assets/icons/board_active.svg create mode 100644 lib/views/board/board_screen.dart diff --git a/assets/icons/board.svg b/assets/icons/board.svg new file mode 100644 index 0000000..799a5c9 --- /dev/null +++ b/assets/icons/board.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/board_active.svg b/assets/icons/board_active.svg new file mode 100644 index 0000000..02a3cf5 --- /dev/null +++ b/assets/icons/board_active.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/core/constants/assets.dart b/lib/core/constants/assets.dart index ddcb9f8..0f4f39f 100644 --- a/lib/core/constants/assets.dart +++ b/lib/core/constants/assets.dart @@ -16,6 +16,8 @@ class AssetsManager { static const contracts = 'assets/icons/icon_contracts.svg'; static const notify = 'assets/icons/notify_icon.svg'; static const notifyActive = 'assets/icons/notify_icon_active.svg'; + static const board = 'assets/icons/board.svg'; + static const boardActive = 'assets/icons/board_active.svg'; // login page static const logo = 'assets/pngs/logo.png'; diff --git a/lib/core/network/network_manager.dart b/lib/core/network/network_manager.dart index 997c69e..60f56c2 100644 --- a/lib/core/network/network_manager.dart +++ b/lib/core/network/network_manager.dart @@ -21,8 +21,8 @@ class NetworkManager { : mainDio = Dio( BaseOptions( baseUrl: AppConfig.apiBaseUrl, - connectTimeout: const Duration(seconds: 10), - receiveTimeout: const Duration(seconds: 10), + connectTimeout: const Duration(seconds: 20), + receiveTimeout: const Duration(seconds: 20), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', diff --git a/lib/core/routes/app_routes.dart b/lib/core/routes/app_routes.dart index b129eea..248e957 100644 --- a/lib/core/routes/app_routes.dart +++ b/lib/core/routes/app_routes.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tm_app/views/board/board_screen.dart'; import 'package:tm_app/views/completion_of_documents/pages/complectopn_of_documents_screen.dart'; import 'package:tm_app/views/detail/pages/detail_screen.dart'; import 'package:tm_app/views/forget_password_create/pages/forgot_password_create_screen.dart'; @@ -265,6 +266,16 @@ class LikedTendersRouteData extends GoRouteData with _$LikedTendersRouteData { } } +@TypedGoRoute(path: '/board') +class BoardRouteData extends GoRouteData with _$BoardRouteData { + const BoardRouteData(); + + @override + Widget build(BuildContext context, GoRouterState state) { + return const BoardScreen(); + } +} + // @TypedGoRoute( // path: '/completion-of-documents', // ) diff --git a/lib/core/routes/app_routes.g.dart b/lib/core/routes/app_routes.g.dart index b1430a1..f227601 100644 --- a/lib/core/routes/app_routes.g.dart +++ b/lib/core/routes/app_routes.g.dart @@ -14,6 +14,7 @@ List get $appRoutes => [ $finalCompletionOfDocumentsRouteData, $completionOfDocumentsRouteData, $likedTendersRouteData, + $boardRouteData, $tenderDetailRouteData, $forgotPasswordRouteData, $forgotPasswordOtpRouteData, @@ -353,6 +354,30 @@ mixin _$LikedTendersRouteData on GoRouteData { void replace(BuildContext context) => context.replace(location); } +RouteBase get $boardRouteData => + GoRouteData.$route(path: '/board', factory: _$BoardRouteData._fromState); + +mixin _$BoardRouteData on GoRouteData { + static BoardRouteData _fromState(GoRouterState state) => + const BoardRouteData(); + + @override + String get location => GoRouteData.$location('/board'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} + RouteBase get $tenderDetailRouteData => GoRouteData.$route( path: '/tender-detail', diff --git a/lib/views/board/board_screen.dart b/lib/views/board/board_screen.dart new file mode 100644 index 0000000..3b0a127 --- /dev/null +++ b/lib/views/board/board_screen.dart @@ -0,0 +1,468 @@ +import 'package:flutter/material.dart'; +import 'package:tm_app/core/theme/colors.dart'; +import 'package:tm_app/core/utils/size_config.dart'; +import 'package:tm_app/views/shared/desktop_navigation_widget.dart'; + +// مدل Task برای نگهداری اطلاعات هر کارت +class TaskItem { + final String id; + final String title; + final String description; + final DateTime deadline; + final String assignedTo; + final bool isReady; + TaskStatus status; + + TaskItem({ + required this.id, + required this.title, + required this.description, + required this.deadline, + required this.assignedTo, + required this.isReady, + required this.status, + }); +} + +enum TaskStatus { toDo, inProgress, done } + +class BoardScreen extends StatefulWidget { + const BoardScreen({super.key}); + + @override + State createState() => _BoardScreenState(); +} + +class _BoardScreenState extends State { + // لیست تسک‌های نمونه + List tasks = [ + TaskItem( + id: '1', + title: 'موضوع مناقصه', + description: 'لورم سد می تورتور اوی پوروس سنکتوس. موربی لائورت تورتور...', + deadline: DateTime(2025, 6, 15), + assignedTo: 'Pratyush Tewari', + isReady: false, + status: TaskStatus.toDo, + ), + TaskItem( + id: '2', + title: 'موضوع مناقصه', + description: 'لورم سد موربی لائورت تورتور...', + deadline: DateTime(2025, 6, 15), + assignedTo: 'Pratyush Tewari', + isReady: true, + status: TaskStatus.toDo, + ), + TaskItem( + id: '3', + title: 'موضوع مناقصه', + description: + 'لورم ایپسوم. سد می تورتور اوی پوروس سنکتوس. موربی لائورت تورتور...', + deadline: DateTime(2025, 6, 15), + assignedTo: 'Pratyush Tewari', + isReady: false, + status: TaskStatus.inProgress, + ), + TaskItem( + id: '4', + title: 'موضوع مناقصه', + description: 'لورم ایپسوم امت...', + deadline: DateTime(2025, 6, 15), + assignedTo: 'Pratyush Tewari', + isReady: true, + status: TaskStatus.inProgress, + ), + TaskItem( + id: '5', + title: 'موضوع مناقصه', + description: 'لورم سد می تورتور اوی پوروس سنکتوس. موربی لائورت تورتور...', + deadline: DateTime(2025, 6, 15), + assignedTo: 'Pratyush Tewari', + isReady: false, + status: TaskStatus.done, + ), + ]; + + // متد برای تغییر وضعیت task + void _updateTaskStatus(TaskItem task, TaskStatus newStatus) { + // اگر وضعیت تغییر نکرده، نیازی به آپدیت نیست + if (task.status == newStatus) { + return; + } + + final oldStatus = task.status; + setState(() { + task.status = newStatus; + }); + + // نمایش پیام تایید + _showStatusChangeMessage(context, task, oldStatus, newStatus); + } + + // نمایش پیام تغییر وضعیت + void _showStatusChangeMessage( + BuildContext context, + TaskItem task, + TaskStatus oldStatus, + TaskStatus newStatus, + ) { + final statusNames = { + TaskStatus.toDo: 'To Do', + TaskStatus.inProgress: 'In Progress', + TaskStatus.done: 'Done', + }; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'کارت "${task.title}" از ${statusNames[oldStatus]} به ${statusNames[newStatus]} منتقل شد', + style: const TextStyle(fontFamily: 'Peyda'), + ), + duration: const Duration(seconds: 2), + backgroundColor: AppColors.successColor, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + ); + } + + // گرفتن تسک‌های هر دسته + List _getTasksByStatus(TaskStatus status) { + return tasks.where((task) => task.status == status).toList(); + } + + @override + Widget build(BuildContext context) { + SizeConfig.init(context); + return Scaffold( + backgroundColor: AppColors.backgroundColor, + + body: Column( + children: [ + const DesktopNavigationWidget( + currentIndex: 2, // Tenders index + ), + SizedBox( + width: 740, + height: 735, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 64), + Text( + 'My board', + style: TextStyle( + fontSize: 20.0.sp(), + fontWeight: FontWeight.w600, + color: AppColors.titleColor, + ), + ), + const SizedBox(height: 20), + Expanded( + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: AppColors.primary20, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ستون To Do + Expanded( + child: _BoardColumn( + title: 'To do', + color: AppColors.primary20, + textColor: const Color(0xFF1976D2), + tasks: _getTasksByStatus(TaskStatus.toDo), + status: TaskStatus.toDo, + onTaskDropped: _updateTaskStatus, + ), + ), + const SizedBox(width: 12), + // ستون In Progress + Expanded( + child: _BoardColumn( + title: 'In progress', + color: const Color(0xFFFFF3E0), + textColor: const Color(0xFFF57C00), + tasks: _getTasksByStatus(TaskStatus.inProgress), + status: TaskStatus.inProgress, + onTaskDropped: _updateTaskStatus, + ), + ), + const SizedBox(width: 12), + // ستون Done + Expanded( + child: _BoardColumn( + title: 'Done', + color: const Color(0xFFE8F5E9), + textColor: const Color(0xFF388E3C), + tasks: _getTasksByStatus(TaskStatus.done), + status: TaskStatus.done, + onTaskDropped: _updateTaskStatus, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +// ویجت ستون بورد +class _BoardColumn extends StatelessWidget { + final String title; + final Color color; + final Color textColor; + final List tasks; + final TaskStatus status; + final Function(TaskItem, TaskStatus) onTaskDropped; + + const _BoardColumn({ + required this.title, + required this.color, + required this.textColor, + required this.tasks, + required this.status, + required this.onTaskDropped, + }); + + @override + Widget build(BuildContext context) { + return DragTarget( + onWillAcceptWithDetails: (details) { + // فقط اگر task از ستون دیگری باشد، قبول کن + return details.data.status != status; + }, + onAcceptWithDetails: (details) { + onTaskDropped(details.data, status); + }, + builder: (context, candidateData, rejectedData) { + final isHovering = candidateData.isNotEmpty; + return Container( + decoration: BoxDecoration( + color: isHovering ? color.withOpacity(0.3) : AppColors.primary0, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isHovering ? textColor : AppColors.borderBlue, + width: isHovering ? 2 : 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // هدر ستون + Container( + width: double.infinity, + height: 34, + margin: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(4), + ), + alignment: Alignment.center, + child: Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.primaryColor, + ), + ), + ), + // لیست تسک‌ها + Expanded( + child: ListView.builder( + padding: const EdgeInsets.all(12), + itemCount: tasks.length, + itemBuilder: (context, index) { + return _TaskCard(task: tasks[index]); + }, + ), + ), + ], + ), + ); + }, + ); + } +} + +// ویجت کارت Task +class _TaskCard extends StatelessWidget { + final TaskItem task; + + const _TaskCard({required this.task}); + + String _formatDate(DateTime date) { + return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; + } + + @override + Widget build(BuildContext context) { + return Draggable( + data: task, + feedback: Material( + elevation: 8, + borderRadius: BorderRadius.circular(12), + child: Container( + width: 280, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.primary0, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.borderBlue, width: 2), + ), + child: _buildCardContent(context), + ), + ), + childWhenDragging: Opacity(opacity: 0.3, child: _buildCard(context)), + child: _buildCard(context), + ); + } + + Widget _buildCard(BuildContext context) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.primary0, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.grey30), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: _buildCardContent(context), + ); + } + + Widget _buildCardContent(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Deadline + Container( + height: 24.0, + padding: EdgeInsets.symmetric(horizontal: 10.0.w()), + decoration: BoxDecoration( + color: AppColors.primary20, + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Deadline :', + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w500, + color: AppColors.textBlue, + ), + ), + Text( + _formatDate(task.deadline), + style: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w500, + color: AppColors.grey80, + ), + ), + ], + ), + ), + // Row( + // children: [ + // Text( + // 'Deadline:', + // style: TextStyle( + // fontSize: 12, + // color: AppColors.grey60, + // fontWeight: FontWeight.w500, + // ), + // ), + // const SizedBox(width: 8), + // Text( + // _formatDate(task.deadline), + // style: TextStyle( + // fontSize: 12, + // color: AppColors.primaryTextColor, + // fontWeight: FontWeight.w600, + // ), + // ), + // ], + // ), + const SizedBox(height: 12), + // عنوان + Text( + task.title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: AppColors.primaryTextColor, + ), + ), + const SizedBox(height: 8), + // توضیحات + Text( + task.description, + style: TextStyle(fontSize: 12, color: AppColors.grey60, height: 1.5), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 12), + // وضعیت و اطلاعات کاربر + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // وضعیت + Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: task.isReady ? Colors.blue : Colors.red, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + Text( + task.isReady ? 'Ready' : 'Incomplete', + style: TextStyle( + fontSize: 11, + color: task.isReady ? Colors.blue : Colors.red, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + // آیکون کاربر و نام + Row( + children: [ + Icon(Icons.account_circle, size: 16, color: AppColors.grey60), + const SizedBox(width: 4), + Text( + task.assignedTo, + style: TextStyle(fontSize: 11, color: AppColors.grey60), + ), + ], + ), + ], + ), + ], + ); + } +} diff --git a/lib/views/shared/desktop_navigation_widget.dart b/lib/views/shared/desktop_navigation_widget.dart index 8e71198..5d7244c 100644 --- a/lib/views/shared/desktop_navigation_widget.dart +++ b/lib/views/shared/desktop_navigation_widget.dart @@ -86,18 +86,21 @@ class DesktopNavigationWidget extends StatelessWidget { const SizedBox(width: 24), _navigationItem( context: context, - text: TendersStrings.contracts, + text: TendersStrings.board, isActive: currentIndex == 2, onTap: () { if (currentIndex == 2) { return; } else { - // Router.neglect(context, () => TendersRouteData().go(context)); + Router.neglect( + context, + () => const BoardRouteData().go(context), + ); // context.read().getTenders(); } }, - iconPath: AssetsManager.contracts, - activeIconPath: AssetsManager.contracts, + iconPath: AssetsManager.board, + activeIconPath: AssetsManager.boardActive, ), const SizedBox(width: 24), _navigationItem( diff --git a/lib/views/tenders/strings/tenders_strings.dart b/lib/views/tenders/strings/tenders_strings.dart index 9f8af32..80d85e4 100644 --- a/lib/views/tenders/strings/tenders_strings.dart +++ b/lib/views/tenders/strings/tenders_strings.dart @@ -26,4 +26,5 @@ class TendersStrings { static const String mostOfTheTime = 'Most of the time'; static const String minimumTime = 'Minimum time'; static const String tenderBudgetLabel = 'Budget :'; + static const String board = 'Board'; }