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';
}