Remove TaskStatus enum and related extensions, refactor BoardService and BoardResponse models for improved API integration, and update BoardViewModel to handle card movements between columns. Adjust UI components to reflect these changes and enhance error handling for card operations.
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
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/date_utils.dart';
|
||||
import 'package:tm_app/core/utils/result.dart';
|
||||
import 'package:tm_app/core/utils/size_config.dart';
|
||||
import 'package:tm_app/data/services/board_service.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';
|
||||
@@ -32,38 +33,27 @@ class _BoardScreenState extends State<BoardScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
void _updateTaskStatus(BoardData task, TaskStatus newStatus) {
|
||||
if (task.statusEnum == newStatus) {
|
||||
return;
|
||||
}
|
||||
Future<void> _onCardDropped(BoardCard card, BoardColumn target) async {
|
||||
final source = _boardViewModel.columnForCard(card.id ?? '');
|
||||
if (source == null || source.id == target.id) 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]!,
|
||||
),
|
||||
final result = await _boardViewModel.moveCard(
|
||||
card: card,
|
||||
targetColumn: target,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
if (result is Ok<MoveCardResponse>) {
|
||||
AppToast.success(
|
||||
context,
|
||||
BoardStrings.cardMovedMessage(
|
||||
source.name ?? '',
|
||||
target.name ?? '',
|
||||
),
|
||||
);
|
||||
} else if (result is Error<MoveCardResponse>) {
|
||||
AppToast.error(context, BoardStrings.moveFailed);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -73,9 +63,7 @@ class _BoardScreenState extends State<BoardScreen> {
|
||||
backgroundColor: AppColors.backgroundColor,
|
||||
body: Column(
|
||||
children: [
|
||||
const DesktopNavigationWidget(
|
||||
currentIndex: 2, // Tenders index
|
||||
),
|
||||
const DesktopNavigationWidget(currentIndex: 2),
|
||||
Consumer<BoardViewModel>(
|
||||
builder: (context, viewModel, child) {
|
||||
if (viewModel.isLoading) {
|
||||
@@ -92,12 +80,16 @@ class _BoardScreenState extends State<BoardScreen> {
|
||||
child: Center(child: Text(viewModel.errorMessage!)),
|
||||
);
|
||||
}
|
||||
if (viewModel.hasData == false) {
|
||||
if (!viewModel.hasData) {
|
||||
return const Expanded(
|
||||
child: Center(child: Text(BoardStrings.emptyListText)),
|
||||
);
|
||||
}
|
||||
|
||||
final columns = [...viewModel.columns]..sort(
|
||||
(a, b) => (a.order ?? 0).compareTo(b.order ?? 0),
|
||||
);
|
||||
|
||||
return Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
@@ -108,7 +100,7 @@ class _BoardScreenState extends State<BoardScreen> {
|
||||
children: [
|
||||
const SizedBox(height: 32),
|
||||
Text(
|
||||
BoardStrings.boardTitle,
|
||||
viewModel.board?.name ?? BoardStrings.boardTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20.0.sp(),
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -126,50 +118,15 @@ class _BoardScreenState extends State<BoardScreen> {
|
||||
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,
|
||||
for (int i = 0; i < columns.length; i++) ...[
|
||||
if (i > 0) const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _BoardColumnView(
|
||||
column: columns[i],
|
||||
onCardDropped: _onCardDropped,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -187,43 +144,29 @@ class _BoardScreenState extends State<BoardScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
class _BoardColumnView extends StatelessWidget {
|
||||
final BoardColumn column;
|
||||
final Future<void> Function(BoardCard, BoardColumn) onCardDropped;
|
||||
|
||||
const _BoardColumn({
|
||||
required this.title,
|
||||
required this.color,
|
||||
required this.textColor,
|
||||
required this.tasks,
|
||||
required this.status,
|
||||
required this.onTaskDropped,
|
||||
required this.isValidMove,
|
||||
});
|
||||
const _BoardColumnView({required this.column, required this.onCardDropped});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DragTarget<BoardData>(
|
||||
onWillAcceptWithDetails: (details) {
|
||||
return isValidMove(details.data.statusEnum, status);
|
||||
},
|
||||
onAcceptWithDetails: (details) {
|
||||
onTaskDropped(details.data, status);
|
||||
},
|
||||
final accent = _parseHexColor(column.color) ?? AppColors.textBlue;
|
||||
return DragTarget<BoardCard>(
|
||||
onWillAcceptWithDetails: (details) => details.data.columnId != column.id,
|
||||
onAcceptWithDetails: (details) => onCardDropped(details.data, column),
|
||||
builder: (context, candidateData, rejectedData) {
|
||||
final isHovering = candidateData.isNotEmpty;
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isHovering ? color.withValues(alpha: 0.3) : AppColors.primary0,
|
||||
isHovering
|
||||
? accent.withValues(alpha: 0.15)
|
||||
: AppColors.primary0,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isHovering ? textColor : AppColors.borderBlue,
|
||||
color: isHovering ? accent : AppColors.borderBlue,
|
||||
width: isHovering ? 2 : 1,
|
||||
),
|
||||
),
|
||||
@@ -235,26 +178,25 @@ class _BoardColumn extends StatelessWidget {
|
||||
height: 34,
|
||||
margin: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
color: accent.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
title,
|
||||
_columnHeader(column),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor,
|
||||
color: accent,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: tasks.length,
|
||||
itemCount: column.cards.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _TaskCard(task: tasks[index]);
|
||||
return _TaskCard(card: column.cards[index], accent: accent);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -264,21 +206,25 @@ class _BoardColumn extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _columnHeader(BoardColumn column) {
|
||||
final name = column.name ?? '';
|
||||
final limit = column.limit;
|
||||
if (limit == null || limit <= 0) return name;
|
||||
return '$name (${column.cards.length}/$limit)';
|
||||
}
|
||||
}
|
||||
|
||||
class _TaskCard extends StatelessWidget {
|
||||
final BoardData task;
|
||||
final BoardCard card;
|
||||
final Color accent;
|
||||
|
||||
const _TaskCard({required this.task});
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
const _TaskCard({required this.card, required this.accent});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Draggable<BoardData>(
|
||||
data: task,
|
||||
return Draggable<BoardCard>(
|
||||
data: card,
|
||||
feedback: Material(
|
||||
elevation: 8,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -319,91 +265,72 @@ class _TaskCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildCardContent(BuildContext context) {
|
||||
final due = card.dueDate;
|
||||
final labels = card.labels;
|
||||
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,
|
||||
if (due != null)
|
||||
Container(
|
||||
height: 24.0,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.0.w()),
|
||||
decoration: BoxDecoration(
|
||||
color: accent.withValues(alpha: 0.15),
|
||||
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,
|
||||
Text(
|
||||
timeConvertor(due),
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey80,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (due != null) const SizedBox(height: 12),
|
||||
Text(
|
||||
task.description!,
|
||||
style: TextStyle(fontSize: 14, color: AppColors.grey70),
|
||||
card.title ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
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),
|
||||
if (card.priority != null) _PriorityChip(priority: card.priority!),
|
||||
if (labels.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [for (final label in labels) _LabelChip(label: label)],
|
||||
),
|
||||
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!,
|
||||
card.tenderId ?? '',
|
||||
style: TextStyle(fontSize: 12, color: AppColors.grey60),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -411,3 +338,83 @@ class _TaskCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PriorityChip extends StatelessWidget {
|
||||
final String priority;
|
||||
const _PriorityChip({required this.priority});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (bg, fg, dot) = _styleFor(priority);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(color: dot, shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
priority,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: fg,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
(Color bg, Color fg, Color dot) _styleFor(String priority) {
|
||||
switch (priority.toLowerCase()) {
|
||||
case 'urgent':
|
||||
case 'high':
|
||||
return (AppColors.red0, AppColors.red20, AppColors.error);
|
||||
case 'low':
|
||||
return (AppColors.primary20, AppColors.textBlue, AppColors.textBlue);
|
||||
case 'medium':
|
||||
default:
|
||||
return (AppColors.orange10, AppColors.orange, AppColors.orange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _LabelChip extends StatelessWidget {
|
||||
final String label;
|
||||
const _LabelChip({required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary20,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 11, color: AppColors.textBlue),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Color? _parseHexColor(String? hex) {
|
||||
if (hex == null) return null;
|
||||
var value = hex.trim();
|
||||
if (value.startsWith('#')) value = value.substring(1);
|
||||
if (value.length == 6) value = 'FF$value';
|
||||
if (value.length != 8) return null;
|
||||
final parsed = int.tryParse(value, radix: 16);
|
||||
if (parsed == null) return null;
|
||||
return Color(parsed);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,9 @@ class BoardStrings {
|
||||
BoardStrings._();
|
||||
|
||||
static const String boardTitle = 'My Board';
|
||||
static const String toDo = 'To Do';
|
||||
static const String inProgress = 'In Progress';
|
||||
static const String done = 'Done';
|
||||
static const String emptyListText = 'No tasks found';
|
||||
static const String deadlineLabel = 'Deadline';
|
||||
static const String readyLabel = 'Ready';
|
||||
static const String incompleteLabel = 'Incomplete';
|
||||
static String taskMovedMessage(String oldStatus, String newStatus) =>
|
||||
'Task moved from $oldStatus to $newStatus';
|
||||
static const String moveFailed = 'Could not move card';
|
||||
static String cardMovedMessage(String fromColumn, String toColumn) =>
|
||||
'Card moved from $fromColumn to $toColumn';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user