From 02057988dcc6c2c7a248d62a195b0cb68c0e4b2f Mon Sep 17 00:00:00 2001 From: AmirReza Jamali Date: Tue, 26 May 2026 10:41:34 +0330 Subject: [PATCH] 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. --- lib/core/enums/task_status.dart | 29 - lib/core/extensions/board_data_extension.dart | 12 - lib/data/repositories/board_repositories.dart | 14 +- lib/data/services/board_service.dart | 104 ++- .../model/board_response/board_response.dart | 247 +++++++- .../board_response.freezed.dart | 596 ------------------ .../board_response/board_response.g.dart | 51 -- lib/view_models/board_view_model.dart | 114 ++-- lib/views/board/pages/board_screen.dart | 377 +++++------ lib/views/board/strings/board_strings.dart | 10 +- 10 files changed, 542 insertions(+), 1012 deletions(-) delete mode 100644 lib/core/enums/task_status.dart delete mode 100644 lib/core/extensions/board_data_extension.dart delete mode 100644 lib/data/services/model/board_response/board_response.freezed.dart delete mode 100644 lib/data/services/model/board_response/board_response.g.dart diff --git a/lib/core/enums/task_status.dart b/lib/core/enums/task_status.dart deleted file mode 100644 index 5298c23..0000000 --- a/lib/core/enums/task_status.dart +++ /dev/null @@ -1,29 +0,0 @@ -enum TaskStatus { toDo, inProgress, done } - -extension TaskStatusExtension on TaskStatus { - // تبدیل enum به string - String get value => name; - - // تبدیل string به enum - static TaskStatus fromString(String? status) { - if (status == null) { - return TaskStatus.toDo; - } - - return TaskStatus.values.firstWhere( - (e) => e.name == status, - orElse: () => TaskStatus.toDo, - ); - } - - String get displayNameEn { - switch (this) { - case TaskStatus.toDo: - return 'To Do'; - case TaskStatus.inProgress: - return 'In Progress'; - case TaskStatus.done: - return 'Done'; - } - } -} diff --git a/lib/core/extensions/board_data_extension.dart b/lib/core/extensions/board_data_extension.dart deleted file mode 100644 index 31039de..0000000 --- a/lib/core/extensions/board_data_extension.dart +++ /dev/null @@ -1,12 +0,0 @@ -import '../../data/services/model/board_response/board_response.dart'; -import '../enums/task_status.dart'; - -extension BoardDataExtension on BoardData { - // تبدیل status string به TaskStatus enum - TaskStatus get statusEnum => TaskStatusExtension.fromString(status); - - // کپی BoardData با status جدید - BoardData copyWithStatus(TaskStatus newStatus) { - return copyWith(status: newStatus.value); - } -} diff --git a/lib/data/repositories/board_repositories.dart b/lib/data/repositories/board_repositories.dart index 703ef5b..6f2cca1 100644 --- a/lib/data/repositories/board_repositories.dart +++ b/lib/data/repositories/board_repositories.dart @@ -7,7 +7,17 @@ class BoardRepository { BoardRepository({required BoardService boardService}) : _boardService = boardService; - Future> getBoard() async { - return _boardService.getBoard(); + Future> getBoard() => _boardService.getBoard(); + + Future> moveCard({ + required String cardId, + required String columnId, + required int newOrder, + }) { + return _boardService.moveCard( + cardId: cardId, + columnId: columnId, + newOrder: newOrder, + ); } } diff --git a/lib/data/services/board_service.dart b/lib/data/services/board_service.dart index 7d2f579..5343c55 100644 --- a/lib/data/services/board_service.dart +++ b/lib/data/services/board_service.dart @@ -1,5 +1,6 @@ -import '../../../core/network/network_manager.dart'; -import '../../core/config/app_config.dart'; +import 'dart:convert'; + +import '../../core/network/network_manager.dart'; import '../../core/utils/result.dart'; import 'model/board_response/board_response.dart'; @@ -9,72 +10,45 @@ class BoardService { final NetworkManager _networkManager; + static const _basePath = '/admin/v1/kanban/kanban'; + Future> getBoard() async { - final uri = '${AppConfig.apiBaseUrl}/api/v1/board'; - await Future.delayed(const Duration(seconds: 2)); + return _networkManager.makeRequest( + '$_basePath/boards/default', + (json) => BoardResponse.fromJson(json), + method: 'GET', + ); + } - // final result = await _networkManager.makeRequest( - // uri, - // (json) => BoardResponse.fromJson(json), - // method: 'GET', - // ); - // return result; - - return Result.ok( - BoardResponse( - success: true, - message: 'Board data fetched successfully', - data: tasks, - error: null, - ), + Future> moveCard({ + required String cardId, + required String columnId, + required int newOrder, + }) async { + final body = jsonEncode({ + 'card_id': cardId, + 'column_id': columnId, + 'new_order': newOrder, + }); + return _networkManager.makeRequest( + '$_basePath/cards/move', + (json) => MoveCardResponse.fromJson(json), + method: 'PUT', + data: body, ); } } -List tasks = [ - BoardData( - id: '1', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', - deadline: DateTime(2025, 6, 15), - assignedTo: 'Pratyush Tewari', - isReady: false, - status: 'toDo', - ), - BoardData( - id: '2', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', - deadline: DateTime(2025, 6, 15), - assignedTo: 'Pratyush Tewari', - isReady: true, - status: 'toDo', - ), - BoardData( - id: '3', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', - deadline: DateTime(2025, 6, 15), - assignedTo: 'Pratyush Tewari', - isReady: false, - status: 'inProgress', - ), - BoardData( - id: '4', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', - deadline: DateTime(2025, 6, 15), - assignedTo: 'Pratyush Tewari', - isReady: true, - status: 'inProgress', - ), - BoardData( - id: '5', - description: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', - deadline: DateTime(2025, 6, 15), - assignedTo: 'Pratyush Tewari', - isReady: false, - status: 'done', - ), -]; +class MoveCardResponse { + MoveCardResponse({required this.success, required this.message}); + + final bool? success; + final String? message; + + factory MoveCardResponse.fromJson(Map json) { + return MoveCardResponse( + success: json['success'] as bool?, + message: json['message'] as String?, + ); + } +} diff --git a/lib/data/services/model/board_response/board_response.dart b/lib/data/services/model/board_response/board_response.dart index 8bf6c6b..1b0f140 100644 --- a/lib/data/services/model/board_response/board_response.dart +++ b/lib/data/services/model/board_response/board_response.dart @@ -1,34 +1,229 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - +import '../../../../core/utils/date_utils.dart'; import '../error/error_model.dart'; -part 'board_response.freezed.dart'; -part 'board_response.g.dart'; +class BoardResponse { + BoardResponse({ + required this.success, + required this.message, + required this.data, + required this.error, + }); -@freezed -abstract class BoardResponse with _$BoardResponse { - const factory BoardResponse({ - required bool? success, - required String? message, - required List? data, - required ErrorModel? error, - }) = _BoardResponse; + final bool? success; + final String? message; + final BoardDetailData? data; + final ErrorModel? error; - factory BoardResponse.fromJson(Map json) => - _$BoardResponseFromJson(json); + factory BoardResponse.fromJson(Map json) { + final dataJson = json['data']; + final errorJson = json['error']; + return BoardResponse( + success: json['success'] as bool?, + message: json['message'] as String?, + data: + dataJson is Map + ? BoardDetailData.fromJson(dataJson) + : null, + error: + errorJson is Map + ? ErrorModel.fromJson(errorJson) + : null, + ); + } } -@freezed -abstract class BoardData with _$BoardData { - const factory BoardData({ - required String? id, - required String? description, - required DateTime? deadline, - required String? assignedTo, - required bool? isReady, - required String? status, - }) = _BoardData; +class BoardDetailData { + BoardDetailData({required this.board}); - factory BoardData.fromJson(Map json) => - _$BoardDataFromJson(json); + final Board? board; + + factory BoardDetailData.fromJson(Map json) { + final boardJson = json['board']; + return BoardDetailData( + board: + boardJson is Map ? Board.fromJson(boardJson) : null, + ); + } +} + +class Board { + Board({ + required this.id, + required this.name, + required this.description, + required this.columns, + required this.isDefault, + required this.userId, + required this.createdAt, + required this.updatedAt, + }); + + final String? id; + final String? name; + final String? description; + final List columns; + final bool? isDefault; + final String? userId; + final int? createdAt; + final int? updatedAt; + + factory Board.fromJson(Map json) { + final columnsJson = json['columns']; + return Board( + id: json['id'] as String?, + name: json['name'] as String?, + description: json['description'] as String?, + columns: + columnsJson is List + ? columnsJson + .whereType>() + .map(BoardColumn.fromJson) + .toList() + : [], + isDefault: json['is_default'] as bool?, + userId: json['user_id'] as String?, + createdAt: unixTimestampFromJson(json['created_at']), + updatedAt: unixTimestampFromJson(json['updated_at']), + ); + } + + Board copyWith({List? columns}) { + return Board( + id: id, + name: name, + description: description, + columns: columns ?? this.columns, + isDefault: isDefault, + userId: userId, + createdAt: createdAt, + updatedAt: updatedAt, + ); + } +} + +class BoardColumn { + BoardColumn({ + required this.id, + required this.boardId, + required this.name, + required this.order, + required this.color, + required this.limit, + required this.cards, + required this.cardCount, + required this.isOverLimit, + required this.createdAt, + required this.updatedAt, + }); + + final String? id; + final String? boardId; + final String? name; + final int? order; + final String? color; + final int? limit; + final List cards; + final int? cardCount; + final bool? isOverLimit; + final int? createdAt; + final int? updatedAt; + + factory BoardColumn.fromJson(Map json) { + final cardsJson = json['cards']; + return BoardColumn( + id: json['id'] as String?, + boardId: json['board_id'] as String?, + name: json['name'] as String?, + order: (json['order'] as num?)?.toInt(), + color: json['color'] as String?, + limit: (json['limit'] as num?)?.toInt(), + cards: + cardsJson is List + ? cardsJson + .whereType>() + .map(BoardCard.fromJson) + .toList() + : [], + cardCount: (json['card_count'] as num?)?.toInt(), + isOverLimit: json['is_over_limit'] as bool?, + createdAt: unixTimestampFromJson(json['created_at']), + updatedAt: unixTimestampFromJson(json['updated_at']), + ); + } + + BoardColumn copyWith({List? cards, int? cardCount}) { + return BoardColumn( + id: id, + boardId: boardId, + name: name, + order: order, + color: color, + limit: limit, + cards: cards ?? this.cards, + cardCount: cardCount ?? this.cardCount, + isOverLimit: isOverLimit, + createdAt: createdAt, + updatedAt: updatedAt, + ); + } +} + +class BoardCard { + BoardCard({ + required this.id, + required this.columnId, + required this.tenderId, + required this.title, + required this.order, + required this.priority, + required this.labels, + required this.dueDate, + required this.createdAt, + required this.updatedAt, + }); + + final String? id; + final String? columnId; + final String? tenderId; + final String? title; + final int? order; + final String? priority; + final List labels; + final int? dueDate; + final int? createdAt; + final int? updatedAt; + + factory BoardCard.fromJson(Map json) { + final labelsJson = json['labels']; + return BoardCard( + id: json['id'] as String?, + columnId: json['column_id'] as String?, + tenderId: json['tender_id'] as String?, + title: json['title'] as String?, + order: (json['order'] as num?)?.toInt(), + priority: json['priority'] as String?, + labels: + labelsJson is List + ? labelsJson.whereType().toList() + : [], + dueDate: unixTimestampFromJson(json['due_date']), + createdAt: unixTimestampFromJson(json['created_at']), + updatedAt: unixTimestampFromJson(json['updated_at']), + ); + } + + BoardCard copyWith({String? columnId, int? order}) { + return BoardCard( + id: id, + columnId: columnId ?? this.columnId, + tenderId: tenderId, + title: title, + order: order ?? this.order, + priority: priority, + labels: labels, + dueDate: dueDate, + createdAt: createdAt, + updatedAt: updatedAt, + ); + } } diff --git a/lib/data/services/model/board_response/board_response.freezed.dart b/lib/data/services/model/board_response/board_response.freezed.dart deleted file mode 100644 index bc12e33..0000000 --- a/lib/data/services/model/board_response/board_response.freezed.dart +++ /dev/null @@ -1,596 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'board_response.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; - -/// @nodoc -mixin _$BoardResponse { - - bool? get success; String? get message; List? get data; ErrorModel? get error; -/// Create a copy of BoardResponse -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$BoardResponseCopyWith get copyWith => _$BoardResponseCopyWithImpl(this as BoardResponse, _$identity); - - /// Serializes this BoardResponse to a JSON map. - Map toJson(); - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is BoardResponse&&(identical(other.success, success) || other.success == success)&&(identical(other.message, message) || other.message == message)&&const DeepCollectionEquality().equals(other.data, data)&&(identical(other.error, error) || other.error == error)); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => Object.hash(runtimeType,success,message,const DeepCollectionEquality().hash(data),error); - -@override -String toString() { - return 'BoardResponse(success: $success, message: $message, data: $data, error: $error)'; -} - - -} - -/// @nodoc -abstract mixin class $BoardResponseCopyWith<$Res> { - factory $BoardResponseCopyWith(BoardResponse value, $Res Function(BoardResponse) _then) = _$BoardResponseCopyWithImpl; -@useResult -$Res call({ - bool? success, String? message, List? data, ErrorModel? error -}); - - -$ErrorModelCopyWith<$Res>? get error; - -} -/// @nodoc -class _$BoardResponseCopyWithImpl<$Res> - implements $BoardResponseCopyWith<$Res> { - _$BoardResponseCopyWithImpl(this._self, this._then); - - final BoardResponse _self; - final $Res Function(BoardResponse) _then; - -/// Create a copy of BoardResponse -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? success = freezed,Object? message = freezed,Object? data = freezed,Object? error = freezed,}) { - return _then(_self.copyWith( -success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable -as bool?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable -as String?,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable -as List?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable -as ErrorModel?, - )); -} -/// Create a copy of BoardResponse -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$ErrorModelCopyWith<$Res>? get error { - if (_self.error == null) { - return null; - } - - return $ErrorModelCopyWith<$Res>(_self.error!, (value) { - return _then(_self.copyWith(error: value)); - }); -} -} - - -/// Adds pattern-matching-related methods to [BoardResponse]. -extension BoardResponsePatterns on BoardResponse { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap(TResult Function( _BoardResponse value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _BoardResponse() when $default != null: -return $default(_that);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map(TResult Function( _BoardResponse value) $default,){ -final _that = this; -switch (_that) { -case _BoardResponse(): -return $default(_that);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull(TResult? Function( _BoardResponse value)? $default,){ -final _that = this; -switch (_that) { -case _BoardResponse() when $default != null: -return $default(_that);case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen(TResult Function( bool? success, String? message, List? data, ErrorModel? error)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _BoardResponse() when $default != null: -return $default(_that.success,_that.message,_that.data,_that.error);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when(TResult Function( bool? success, String? message, List? data, ErrorModel? error) $default,) {final _that = this; -switch (_that) { -case _BoardResponse(): -return $default(_that.success,_that.message,_that.data,_that.error);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool? success, String? message, List? data, ErrorModel? error)? $default,) {final _that = this; -switch (_that) { -case _BoardResponse() when $default != null: -return $default(_that.success,_that.message,_that.data,_that.error);case _: - return null; - -} -} - -} - -/// @nodoc -@JsonSerializable() - -class _BoardResponse implements BoardResponse { - const _BoardResponse({required this.success, required this.message, required final List? data, required this.error}): _data = data; - factory _BoardResponse.fromJson(Map json) => _$BoardResponseFromJson(json); - -@override final bool? success; -@override final String? message; - final List? _data; -@override List? get data { - final value = _data; - if (value == null) return null; - if (_data is EqualUnmodifiableListView) return _data; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(value); -} - -@override final ErrorModel? error; - -/// Create a copy of BoardResponse -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$BoardResponseCopyWith<_BoardResponse> get copyWith => __$BoardResponseCopyWithImpl<_BoardResponse>(this, _$identity); - -@override -Map toJson() { - return _$BoardResponseToJson(this, ); -} - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _BoardResponse&&(identical(other.success, success) || other.success == success)&&(identical(other.message, message) || other.message == message)&&const DeepCollectionEquality().equals(other._data, _data)&&(identical(other.error, error) || other.error == error)); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => Object.hash(runtimeType,success,message,const DeepCollectionEquality().hash(_data),error); - -@override -String toString() { - return 'BoardResponse(success: $success, message: $message, data: $data, error: $error)'; -} - - -} - -/// @nodoc -abstract mixin class _$BoardResponseCopyWith<$Res> implements $BoardResponseCopyWith<$Res> { - factory _$BoardResponseCopyWith(_BoardResponse value, $Res Function(_BoardResponse) _then) = __$BoardResponseCopyWithImpl; -@override @useResult -$Res call({ - bool? success, String? message, List? data, ErrorModel? error -}); - - -@override $ErrorModelCopyWith<$Res>? get error; - -} -/// @nodoc -class __$BoardResponseCopyWithImpl<$Res> - implements _$BoardResponseCopyWith<$Res> { - __$BoardResponseCopyWithImpl(this._self, this._then); - - final _BoardResponse _self; - final $Res Function(_BoardResponse) _then; - -/// Create a copy of BoardResponse -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? success = freezed,Object? message = freezed,Object? data = freezed,Object? error = freezed,}) { - return _then(_BoardResponse( -success: freezed == success ? _self.success : success // ignore: cast_nullable_to_non_nullable -as bool?,message: freezed == message ? _self.message : message // ignore: cast_nullable_to_non_nullable -as String?,data: freezed == data ? _self._data : data // ignore: cast_nullable_to_non_nullable -as List?,error: freezed == error ? _self.error : error // ignore: cast_nullable_to_non_nullable -as ErrorModel?, - )); -} - -/// Create a copy of BoardResponse -/// with the given fields replaced by the non-null parameter values. -@override -@pragma('vm:prefer-inline') -$ErrorModelCopyWith<$Res>? get error { - if (_self.error == null) { - return null; - } - - return $ErrorModelCopyWith<$Res>(_self.error!, (value) { - return _then(_self.copyWith(error: value)); - }); -} -} - - -/// @nodoc -mixin _$BoardData { - - String? get id; String? get description; DateTime? get deadline; String? get assignedTo; bool? get isReady; String? get status; -/// Create a copy of BoardData -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$BoardDataCopyWith get copyWith => _$BoardDataCopyWithImpl(this as BoardData, _$identity); - - /// Serializes this BoardData to a JSON map. - Map toJson(); - - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is BoardData&&(identical(other.id, id) || other.id == id)&&(identical(other.description, description) || other.description == description)&&(identical(other.deadline, deadline) || other.deadline == deadline)&&(identical(other.assignedTo, assignedTo) || other.assignedTo == assignedTo)&&(identical(other.isReady, isReady) || other.isReady == isReady)&&(identical(other.status, status) || other.status == status)); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => Object.hash(runtimeType,id,description,deadline,assignedTo,isReady,status); - -@override -String toString() { - return 'BoardData(id: $id, description: $description, deadline: $deadline, assignedTo: $assignedTo, isReady: $isReady, status: $status)'; -} - - -} - -/// @nodoc -abstract mixin class $BoardDataCopyWith<$Res> { - factory $BoardDataCopyWith(BoardData value, $Res Function(BoardData) _then) = _$BoardDataCopyWithImpl; -@useResult -$Res call({ - String? id, String? description, DateTime? deadline, String? assignedTo, bool? isReady, String? status -}); - - - - -} -/// @nodoc -class _$BoardDataCopyWithImpl<$Res> - implements $BoardDataCopyWith<$Res> { - _$BoardDataCopyWithImpl(this._self, this._then); - - final BoardData _self; - final $Res Function(BoardData) _then; - -/// Create a copy of BoardData -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = freezed,Object? description = freezed,Object? deadline = freezed,Object? assignedTo = freezed,Object? isReady = freezed,Object? status = freezed,}) { - return _then(_self.copyWith( -id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable -as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable -as String?,deadline: freezed == deadline ? _self.deadline : deadline // ignore: cast_nullable_to_non_nullable -as DateTime?,assignedTo: freezed == assignedTo ? _self.assignedTo : assignedTo // ignore: cast_nullable_to_non_nullable -as String?,isReady: freezed == isReady ? _self.isReady : isReady // ignore: cast_nullable_to_non_nullable -as bool?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable -as String?, - )); -} - -} - - -/// Adds pattern-matching-related methods to [BoardData]. -extension BoardDataPatterns on BoardData { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap(TResult Function( _BoardData value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _BoardData() when $default != null: -return $default(_that);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map(TResult Function( _BoardData value) $default,){ -final _that = this; -switch (_that) { -case _BoardData(): -return $default(_that);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull(TResult? Function( _BoardData value)? $default,){ -final _that = this; -switch (_that) { -case _BoardData() when $default != null: -return $default(_that);case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen(TResult Function( String? id, String? description, DateTime? deadline, String? assignedTo, bool? isReady, String? status)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _BoardData() when $default != null: -return $default(_that.id,_that.description,_that.deadline,_that.assignedTo,_that.isReady,_that.status);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when(TResult Function( String? id, String? description, DateTime? deadline, String? assignedTo, bool? isReady, String? status) $default,) {final _that = this; -switch (_that) { -case _BoardData(): -return $default(_that.id,_that.description,_that.deadline,_that.assignedTo,_that.isReady,_that.status);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? id, String? description, DateTime? deadline, String? assignedTo, bool? isReady, String? status)? $default,) {final _that = this; -switch (_that) { -case _BoardData() when $default != null: -return $default(_that.id,_that.description,_that.deadline,_that.assignedTo,_that.isReady,_that.status);case _: - return null; - -} -} - -} - -/// @nodoc -@JsonSerializable() - -class _BoardData implements BoardData { - const _BoardData({required this.id, required this.description, required this.deadline, required this.assignedTo, required this.isReady, required this.status}); - factory _BoardData.fromJson(Map json) => _$BoardDataFromJson(json); - -@override final String? id; -@override final String? description; -@override final DateTime? deadline; -@override final String? assignedTo; -@override final bool? isReady; -@override final String? status; - -/// Create a copy of BoardData -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$BoardDataCopyWith<_BoardData> get copyWith => __$BoardDataCopyWithImpl<_BoardData>(this, _$identity); - -@override -Map toJson() { - return _$BoardDataToJson(this, ); -} - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _BoardData&&(identical(other.id, id) || other.id == id)&&(identical(other.description, description) || other.description == description)&&(identical(other.deadline, deadline) || other.deadline == deadline)&&(identical(other.assignedTo, assignedTo) || other.assignedTo == assignedTo)&&(identical(other.isReady, isReady) || other.isReady == isReady)&&(identical(other.status, status) || other.status == status)); -} - -@JsonKey(includeFromJson: false, includeToJson: false) -@override -int get hashCode => Object.hash(runtimeType,id,description,deadline,assignedTo,isReady,status); - -@override -String toString() { - return 'BoardData(id: $id, description: $description, deadline: $deadline, assignedTo: $assignedTo, isReady: $isReady, status: $status)'; -} - - -} - -/// @nodoc -abstract mixin class _$BoardDataCopyWith<$Res> implements $BoardDataCopyWith<$Res> { - factory _$BoardDataCopyWith(_BoardData value, $Res Function(_BoardData) _then) = __$BoardDataCopyWithImpl; -@override @useResult -$Res call({ - String? id, String? description, DateTime? deadline, String? assignedTo, bool? isReady, String? status -}); - - - - -} -/// @nodoc -class __$BoardDataCopyWithImpl<$Res> - implements _$BoardDataCopyWith<$Res> { - __$BoardDataCopyWithImpl(this._self, this._then); - - final _BoardData _self; - final $Res Function(_BoardData) _then; - -/// Create a copy of BoardData -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = freezed,Object? description = freezed,Object? deadline = freezed,Object? assignedTo = freezed,Object? isReady = freezed,Object? status = freezed,}) { - return _then(_BoardData( -id: freezed == id ? _self.id : id // ignore: cast_nullable_to_non_nullable -as String?,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable -as String?,deadline: freezed == deadline ? _self.deadline : deadline // ignore: cast_nullable_to_non_nullable -as DateTime?,assignedTo: freezed == assignedTo ? _self.assignedTo : assignedTo // ignore: cast_nullable_to_non_nullable -as String?,isReady: freezed == isReady ? _self.isReady : isReady // ignore: cast_nullable_to_non_nullable -as bool?,status: freezed == status ? _self.status : status // ignore: cast_nullable_to_non_nullable -as String?, - )); -} - - -} - -// dart format on diff --git a/lib/data/services/model/board_response/board_response.g.dart b/lib/data/services/model/board_response/board_response.g.dart deleted file mode 100644 index 38983f4..0000000 --- a/lib/data/services/model/board_response/board_response.g.dart +++ /dev/null @@ -1,51 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'board_response.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -_BoardResponse _$BoardResponseFromJson(Map json) => - _BoardResponse( - success: json['success'] as bool?, - message: json['message'] as String?, - data: - (json['data'] as List?) - ?.map((e) => BoardData.fromJson(e as Map)) - .toList(), - error: - json['error'] == null - ? null - : ErrorModel.fromJson(json['error'] as Map), - ); - -Map _$BoardResponseToJson(_BoardResponse instance) => - { - 'success': instance.success, - 'message': instance.message, - 'data': instance.data, - 'error': instance.error, - }; - -_BoardData _$BoardDataFromJson(Map json) => _BoardData( - id: json['id'] as String?, - description: json['description'] as String?, - deadline: - json['deadline'] == null - ? null - : DateTime.parse(json['deadline'] as String), - assignedTo: json['assignedTo'] as String?, - isReady: json['isReady'] as bool?, - status: json['status'] as String?, -); - -Map _$BoardDataToJson(_BoardData instance) => - { - 'id': instance.id, - 'description': instance.description, - 'deadline': instance.deadline?.toIso8601String(), - 'assignedTo': instance.assignedTo, - 'isReady': instance.isReady, - 'status': instance.status, - }; diff --git a/lib/view_models/board_view_model.dart b/lib/view_models/board_view_model.dart index 6b3f407..c568cf1 100644 --- a/lib/view_models/board_view_model.dart +++ b/lib/view_models/board_view_model.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import '../core/enums/task_status.dart'; import '../core/utils/result.dart'; import '../data/repositories/board_repositories.dart'; +import '../data/services/board_service.dart'; import '../data/services/model/board_response/board_response.dart'; class BoardViewModel with ChangeNotifier { @@ -12,23 +12,27 @@ class BoardViewModel with ChangeNotifier { bool _isLoading = false; String? _errorMessage; - List _boardData = []; + Board? _board; bool get isLoading => _isLoading; String? get errorMessage => _errorMessage; - List get boardData => _boardData; - bool get hasData => _boardData.isNotEmpty; + Board? get board => _board; + List get columns => _board?.columns ?? const []; + bool get hasData => _board != null && columns.isNotEmpty; Future getBoard() async { _isLoading = true; + _errorMessage = null; notifyListeners(); final result = await _boardRepository.getBoard(); switch (result) { case Ok(): - // تبدیل به لیست قابل تغییر - _boardData = List.from(result.value.data ?? []); + _board = result.value.data?.board; + if (_board == null) { + _errorMessage = result.value.error?.message ?? 'No board data'; + } break; case Error(): _errorMessage = result.error.toString(); @@ -38,40 +42,72 @@ class BoardViewModel with ChangeNotifier { notifyListeners(); } - // update task status - void updateTaskStatus(String taskId, String newStatus) { - final taskIndex = _boardData.indexWhere((task) => task.id == taskId); - if (taskIndex != -1) { - _boardData[taskIndex] = _boardData[taskIndex].copyWith(status: newStatus); + /// Look up the column a given card currently lives in. + BoardColumn? columnForCard(String cardId) { + for (final column in columns) { + if (column.cards.any((c) => c.id == cardId)) return column; + } + return null; + } + + /// Move a card to [targetColumnId] at the end of that column. + /// Optimistically updates local state, then calls the API; rolls back on + /// failure. + Future> moveCard({ + required BoardCard card, + required BoardColumn targetColumn, + }) async { + final cardId = card.id; + final targetColumnId = targetColumn.id; + if (cardId == null || targetColumnId == null || _board == null) { + return Result.error(Exception('Invalid card or column')); + } + + final sourceColumn = columnForCard(cardId); + if (sourceColumn == null) { + return Result.error(Exception('Card not found in any column')); + } + if (sourceColumn.id == targetColumnId) { + return Result.ok(MoveCardResponse(success: true, message: 'No change')); + } + + final previousColumns = _board!.columns; + final newOrder = targetColumn.cards.length; + + _board = _board!.copyWith( + columns: + previousColumns.map((column) { + if (column.id == sourceColumn.id) { + final remaining = + column.cards.where((c) => c.id != cardId).toList(); + return column.copyWith( + cards: remaining, + cardCount: remaining.length, + ); + } + if (column.id == targetColumnId) { + final updated = List.from(column.cards) + ..add(card.copyWith(columnId: targetColumnId, order: newOrder)); + return column.copyWith( + cards: updated, + cardCount: updated.length, + ); + } + return column; + }).toList(), + ); + notifyListeners(); + + final result = await _boardRepository.moveCard( + cardId: cardId, + columnId: targetColumnId, + newOrder: newOrder, + ); + + if (result is Error) { + _board = _board!.copyWith(columns: previousColumns); notifyListeners(); } - } - - bool isValidMove(TaskStatus currentStatus, TaskStatus targetStatus) { - if (currentStatus == TaskStatus.toDo) { - return targetStatus == TaskStatus.inProgress; - } else if (currentStatus == TaskStatus.inProgress) { - return targetStatus == TaskStatus.toDo || targetStatus == TaskStatus.done; - } else if (currentStatus == TaskStatus.done) { - return targetStatus == TaskStatus.inProgress; - } - return false; - } - - List getTasksByStatus(TaskStatus status) { - return boardData.where((task) => task.status == status.value).toList(); - } - - void _updateTaskStatus(BoardData task, TaskStatus newStatus) { - if (task.status == newStatus.value) { - return; - } - - final oldStatus = task.status; - - // update task status in ViewModel - updateTaskStatus(task.id!, newStatus.value); - - // _showStatusChangeMessage(context, task, oldStatus, newStatus); + return result; } } diff --git a/lib/views/board/pages/board_screen.dart b/lib/views/board/pages/board_screen.dart index 32cbdfe..07df1d4 100644 --- a/lib/views/board/pages/board_screen.dart +++ b/lib/views/board/pages/board_screen.dart @@ -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 { }); } - void _updateTaskStatus(BoardData task, TaskStatus newStatus) { - if (task.statusEnum == newStatus) { - return; - } + Future _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) { + AppToast.success( + context, + BoardStrings.cardMovedMessage( + source.name ?? '', + target.name ?? '', + ), + ); + } else if (result is Error) { + AppToast.error(context, BoardStrings.moveFailed); + } } @override @@ -73,9 +63,7 @@ class _BoardScreenState extends State { backgroundColor: AppColors.backgroundColor, body: Column( children: [ - const DesktopNavigationWidget( - currentIndex: 2, // Tenders index - ), + const DesktopNavigationWidget(currentIndex: 2), Consumer( builder: (context, viewModel, child) { if (viewModel.isLoading) { @@ -92,12 +80,16 @@ class _BoardScreenState extends State { 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 { 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 { 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 { } } -class _BoardColumn extends StatelessWidget { - final String title; - final Color color; - final Color textColor; - final List tasks; - final TaskStatus status; - final Function(BoardData, TaskStatus) onTaskDropped; - final bool Function(TaskStatus, TaskStatus) isValidMove; +class _BoardColumnView extends StatelessWidget { + final BoardColumn column; + final Future 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( - onWillAcceptWithDetails: (details) { - return isValidMove(details.data.statusEnum, status); - }, - onAcceptWithDetails: (details) { - onTaskDropped(details.data, status); - }, + final accent = _parseHexColor(column.color) ?? AppColors.textBlue; + return DragTarget( + 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( - data: task, + return Draggable( + 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); +} diff --git a/lib/views/board/strings/board_strings.dart b/lib/views/board/strings/board_strings.dart index f53a263..5a56774 100644 --- a/lib/views/board/strings/board_strings.dart +++ b/lib/views/board/strings/board_strings.dart @@ -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'; }