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
+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),
_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<TendersViewModel>().getTenders();
}
},
iconPath: AssetsManager.contracts,
activeIconPath: AssetsManager.contracts,
iconPath: AssetsManager.board,
activeIconPath: AssetsManager.boardActive,
),
const SizedBox(width: 24),
_navigationItem(
@@ -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';
}