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'; }