Files
tm_app/lib/views/board/pages/board_screen.dart
T
amirrezaghabeli abf1cc3f7d Add board service and repository, update routes and assets
- Introduced BoardService and BoardRepository for managing board-related data.
- Updated app routes to reflect the new board screen structure.
- Added new board-related assets to the asset manager.
- Refactored BoardScreen navigation to utilize the new provider structure.
2025-11-11 12:58:05 +03:30

414 lines
14 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.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/utils/size_config.dart';
import 'package:tm_app/data/services/model/board_response/board_response.dart';
import 'package:tm_app/views/shared/desktop_navigation_widget.dart';
import '../../../core/constants/assets.dart';
import '../../../core/enums/task_status.dart';
import '../../../core/utils/app_toast.dart';
import '../../../view_models/board_view_model.dart';
import '../strings/board_strings.dart';
class BoardScreen extends StatefulWidget {
const BoardScreen({super.key});
@override
State<BoardScreen> createState() => _BoardScreenState();
}
class _BoardScreenState extends State<BoardScreen> {
late BoardViewModel _boardViewModel;
@override
void initState() {
super.initState();
_boardViewModel = context.read<BoardViewModel>();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_boardViewModel.getBoard();
});
}
void _updateTaskStatus(BoardData task, TaskStatus newStatus) {
if (task.statusEnum == newStatus) {
return;
}
final oldStatus = task.statusEnum;
// update task status in ViewModel
_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]!,
),
);
}
@override
Widget build(BuildContext context) {
SizeConfig.init(context);
return Scaffold(
backgroundColor: AppColors.backgroundColor,
body: Column(
children: [
const DesktopNavigationWidget(
currentIndex: 2, // Tenders index
),
Consumer<BoardViewModel>(
builder: (context, viewModel, child) {
if (viewModel.isLoading) {
return const Expanded(
child: Center(
child: CircularProgressIndicator(
color: AppColors.secondary50,
),
),
);
}
if (viewModel.errorMessage != null) {
return Expanded(
child: Center(child: Text(viewModel.errorMessage!)),
);
}
if (viewModel.hasData == false) {
return const Expanded(
child: Center(child: Text(BoardStrings.emptyListText)),
);
}
return Expanded(
child: SingleChildScrollView(
child: SizedBox(
width: 740,
height: 735,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 32),
Text(
BoardStrings.boardTitle,
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: BoardStrings.toDo,
color: AppColors.primary20,
textColor: const Color(0xFF1976D2),
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,
),
),
],
),
),
),
],
),
),
),
);
},
),
],
),
);
}
}
class _BoardColumn extends StatelessWidget {
final String title;
final Color color;
final Color textColor;
final List<BoardData> tasks;
final TaskStatus status;
final Function(BoardData, TaskStatus) onTaskDropped;
final bool Function(TaskStatus, TaskStatus) isValidMove;
const _BoardColumn({
required this.title,
required this.color,
required this.textColor,
required this.tasks,
required this.status,
required this.onTaskDropped,
required this.isValidMove,
});
@override
Widget build(BuildContext context) {
return DragTarget<BoardData>(
onWillAcceptWithDetails: (details) {
return isValidMove(details.data.statusEnum, status);
},
onAcceptWithDetails: (details) {
onTaskDropped(details.data, status);
},
builder: (context, candidateData, rejectedData) {
final isHovering = candidateData.isNotEmpty;
return Container(
decoration: BoxDecoration(
color:
isHovering ? color.withValues(alpha: 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: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: textColor,
),
),
),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: tasks.length,
itemBuilder: (context, index) {
return _TaskCard(task: tasks[index]);
},
),
),
],
),
);
},
);
}
}
class _TaskCard extends StatelessWidget {
final BoardData 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<BoardData>(
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.withValues(alpha: 0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: _buildCardContent(context),
);
}
Widget _buildCardContent(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 24.0,
padding: EdgeInsets.symmetric(horizontal: 10.0.w()),
decoration: BoxDecoration(
color:
task.statusEnum == TaskStatus.toDo
? AppColors.primary20
: task.statusEnum == TaskStatus.inProgress
? AppColors.orange10
: AppColors.secondary10,
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${BoardStrings.deadlineLabel} :',
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w500,
color: AppColors.textBlue,
),
),
Text(
_formatDate(task.deadline ?? DateTime.now()),
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w500,
color: AppColors.grey80,
),
),
],
),
),
const SizedBox(height: 12),
Text(
task.description!,
style: TextStyle(fontSize: 14, color: AppColors.grey70),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
decoration: BoxDecoration(
color: task.isReady ?? false ? AppColors.primary20 : AppColors.red0,
borderRadius: BorderRadius.circular(8),
),
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),
Row(
children: [
SvgPicture.asset(AssetsManager.profile),
const SizedBox(width: 4),
Text(
task.assignedTo!,
style: TextStyle(fontSize: 12, color: AppColors.grey60),
),
],
),
],
);
}
}