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.
This commit is contained in:
amirrezaghabeli
2025-11-10 16:14:50 +03:30
parent 8dc5e0ed29
commit e03c87f99d
9 changed files with 529 additions and 6 deletions
+8
View File
@@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.90024 17H18.0902C19.9902 17 20.9902 16 20.9902 14.1V2H2.99023V14.1C3.00023 16 4.00024 17 5.90024 17Z" stroke="#777777" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 2H22" stroke="#777777" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 22L12 20V17" fill="#0164FF"/>
<path d="M8 22L12 20V17" stroke="#777777" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 22L12 20" stroke="#777777" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 11L10.65 8.37C10.9 8.16 11.23 8.22 11.4 8.5L12.6 10.5C12.77 10.78 13.1 10.83 13.35 10.63L16.5 8" stroke="#777777" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 976 B

+5
View File
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 2.75H2C1.59 2.75 1.25 2.41 1.25 2C1.25 1.59 1.59 1.25 2 1.25H22C22.41 1.25 22.75 1.59 22.75 2C22.75 2.41 22.41 2.75 22 2.75Z" fill="#0164FF"/>
<path d="M16.6703 22.34C16.5403 22.6 16.2703 22.75 16.0003 22.75C15.8903 22.75 15.7703 22.72 15.6703 22.67L12.0003 20.84L8.33026 22.67C8.23026 22.72 8.11026 22.75 8.00026 22.75C7.73026 22.75 7.46026 22.6 7.33026 22.34C7.14026 21.96 7.29026 21.51 7.67026 21.33L11.2503 19.54V17H12.7503V19.54L16.3303 21.33C16.7103 21.51 16.8603 21.96 16.6703 22.34Z" fill="#0164FF"/>
<path d="M3 2V14.1C3 16 4 17 5.9 17H18.1C20 17 21 16 21 14.1V2H3ZM16.98 8.58L13.83 11.21C13.54 11.45 13.17 11.55 12.82 11.49C12.46 11.43 12.15 11.21 11.96 10.89L10.91 9.14L7.98 11.58C7.84 11.7 7.67 11.75 7.5 11.75C7.29 11.75 7.07 11.66 6.92 11.48C6.65 11.16 6.7 10.69 7.02 10.42L10.17 7.79C10.46 7.55 10.83 7.45 11.18 7.51C11.54 7.57 11.85 7.79 12.04 8.11L13.09 9.86L16.02 7.42C16.34 7.15 16.81 7.2 17.08 7.52C17.34 7.84 17.3 8.31 16.98 8.58Z" fill="#0164FF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+2
View File
@@ -16,6 +16,8 @@ class AssetsManager {
static const contracts = 'assets/icons/icon_contracts.svg'; static const contracts = 'assets/icons/icon_contracts.svg';
static const notify = 'assets/icons/notify_icon.svg'; static const notify = 'assets/icons/notify_icon.svg';
static const notifyActive = 'assets/icons/notify_icon_active.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 // login page
static const logo = 'assets/pngs/logo.png'; static const logo = 'assets/pngs/logo.png';
+2 -2
View File
@@ -21,8 +21,8 @@ class NetworkManager {
: mainDio = Dio( : mainDio = Dio(
BaseOptions( BaseOptions(
baseUrl: AppConfig.apiBaseUrl, baseUrl: AppConfig.apiBaseUrl,
connectTimeout: const Duration(seconds: 10), connectTimeout: const Duration(seconds: 20),
receiveTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 20),
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
+11
View File
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.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/completion_of_documents/pages/complectopn_of_documents_screen.dart';
import 'package:tm_app/views/detail/pages/detail_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'; 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<BoardRouteData>(path: '/board')
class BoardRouteData extends GoRouteData with _$BoardRouteData {
const BoardRouteData();
@override
Widget build(BuildContext context, GoRouterState state) {
return const BoardScreen();
}
}
// @TypedGoRoute<CompletionOfDocumentsRouteData>( // @TypedGoRoute<CompletionOfDocumentsRouteData>(
// path: '/completion-of-documents', // path: '/completion-of-documents',
// ) // )
+25
View File
@@ -14,6 +14,7 @@ List<RouteBase> get $appRoutes => [
$finalCompletionOfDocumentsRouteData, $finalCompletionOfDocumentsRouteData,
$completionOfDocumentsRouteData, $completionOfDocumentsRouteData,
$likedTendersRouteData, $likedTendersRouteData,
$boardRouteData,
$tenderDetailRouteData, $tenderDetailRouteData,
$forgotPasswordRouteData, $forgotPasswordRouteData,
$forgotPasswordOtpRouteData, $forgotPasswordOtpRouteData,
@@ -353,6 +354,30 @@ mixin _$LikedTendersRouteData on GoRouteData {
void replace(BuildContext context) => context.replace(location); 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<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}
RouteBase get $tenderDetailRouteData => GoRouteData.$route( RouteBase get $tenderDetailRouteData => GoRouteData.$route(
path: '/tender-detail', path: '/tender-detail',
+468
View File
@@ -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<BoardScreen> createState() => _BoardScreenState();
}
class _BoardScreenState extends State<BoardScreen> {
// لیست تسک‌های نمونه
List<TaskItem> 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<TaskItem> _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<TaskItem> 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<TaskItem>(
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<TaskItem>(
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),
),
],
),
],
),
],
);
}
}
@@ -86,18 +86,21 @@ class DesktopNavigationWidget extends StatelessWidget {
const SizedBox(width: 24), const SizedBox(width: 24),
_navigationItem( _navigationItem(
context: context, context: context,
text: TendersStrings.contracts, text: TendersStrings.board,
isActive: currentIndex == 2, isActive: currentIndex == 2,
onTap: () { onTap: () {
if (currentIndex == 2) { if (currentIndex == 2) {
return; return;
} else { } else {
// Router.neglect(context, () => TendersRouteData().go(context)); Router.neglect(
context,
() => const BoardRouteData().go(context),
);
// context.read<TendersViewModel>().getTenders(); // context.read<TendersViewModel>().getTenders();
} }
}, },
iconPath: AssetsManager.contracts, iconPath: AssetsManager.board,
activeIconPath: AssetsManager.contracts, activeIconPath: AssetsManager.boardActive,
), ),
const SizedBox(width: 24), const SizedBox(width: 24),
_navigationItem( _navigationItem(
@@ -26,4 +26,5 @@ class TendersStrings {
static const String mostOfTheTime = 'Most of the time'; static const String mostOfTheTime = 'Most of the time';
static const String minimumTime = 'Minimum time'; static const String minimumTime = 'Minimum time';
static const String tenderBudgetLabel = 'Budget :'; static const String tenderBudgetLabel = 'Budget :';
static const String board = 'Board';
} }