diff --git a/lib/ui/task/kanban/kanban_card.dart b/lib/ui/task/kanban/kanban_card.dart new file mode 100644 index 000000000..337ee4c55 --- /dev/null +++ b/lib/ui/task/kanban/kanban_card.dart @@ -0,0 +1,244 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/ui/app/buttons/app_text_button.dart'; +import 'package:invoiceninja_flutter/ui/app/live_text.dart'; +import 'package:invoiceninja_flutter/ui/task/kanban/kanban_view.dart'; +import 'package:invoiceninja_flutter/utils/app_context.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; +import 'package:invoiceninja_flutter/utils/colors.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +class KanbanTaskCard extends StatefulWidget { + const KanbanTaskCard({ + @required this.task, + @required this.onSavePressed, + @required this.onCancelPressed, + @required this.isSaving, + @required this.isDragging, + }); + final TaskEntity task; + final Function(Completer, String) onSavePressed; + final Function() onCancelPressed; + final bool isSaving; + final bool isDragging; + + @override + _KanbanTaskCardState createState() => _KanbanTaskCardState(); +} + +class _KanbanTaskCardState extends State { + bool _isEditing = false; + bool _isHovered = false; + String _description = ''; + + @override + void initState() { + super.initState(); + + final task = widget.task; + _description = task.description; + _isEditing = task.isNew; + } + + @override + Widget build(BuildContext context) { + final localization = AppLocalization.of(context); + final store = StoreProvider.of(context); + final state = store.state; + final task = widget.task; + final project = state.projectState.get(task.projectId); + final client = state.clientState.get(task.clientId); + + var color = Colors.grey; + if (task.projectId.isNotEmpty) { + final projectIndex = state.projectState.list.indexOf(task.projectId); + color = getColorByIndex(projectIndex); + } + + final isDragging = + context.findAncestorStateOfType().isDragging; + + if (_isEditing && !widget.isDragging) { + return Card( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + children: [ + DecoratedFormField( + autofocus: true, + initialValue: _description, + minLines: 3, + maxLines: 10, + onChanged: (value) => _description = value, + ), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AppTextButton( + onPressed: () { + setState(() { + _isEditing = false; + if (task.isNew) { + widget.onCancelPressed(); + } + }); + }, + label: localization.cancel, + ), + Padding( + padding: const EdgeInsets.only(left: 8), + child: ElevatedButton( + onPressed: () { + final completer = snackBarCompleter( + context, localization.updatedTask); + completer.future.then((value) { + setState(() { + _isEditing = false; + }); + }); + widget.onSavePressed(completer, _description.trim()); + }, + child: Text(localization.save), + ), + ), + ], + ) + ], + ), + ), + ); + } + + return MouseRegion( + onEnter: (event) => setState(() => _isHovered = true), + onExit: (event) => setState(() => _isHovered = false), + child: InkWell( + child: Opacity( + opacity: widget.isSaving ? .5 : 1, + child: Card( + color: Theme.of(context).backgroundColor, + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(task.description, maxLines: 3), + SizedBox(height: 8), + if (_isHovered && !isDragging) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: InkWell( + child: Center( + child: Text( + localization.viewTask, + style: TextStyle(fontSize: 12), + )), + onTap: task.isNew + ? null + : () { + if (state.taskUIState.selectedId == + task.id) { + viewEntityById( + appContext: context.getAppContext(), + entityId: '', + entityType: EntityType.task, + showError: false); + } else { + viewEntity( + appContext: context.getAppContext(), + entity: task); + } + }, + ), + ), + Expanded( + child: InkWell( + onTap: () { + handleEntityAction( + context.getAppContext(), + task, + task.isRunning + ? EntityAction.stop + : EntityAction.start); + }, + child: Center( + child: Text( + task.isRunning + ? localization.stopTask + : task.getTaskTimes().isEmpty + ? localization.startTask + : localization.resumeTask, + style: TextStyle(fontSize: 12)), + ), + ), + ), + ], + ) + else + Row( + children: [ + LiveText( + () { + return formatDuration(task.calculateDuration()) + + (client.isOld + ? ' • ' + client.displayName + : '') + + (project.isOld ? ' • ' + project.name : ''); + }, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w300, + ), + ), + Spacer(), + if (task.documents.isNotEmpty) + Padding( + padding: const EdgeInsets.only(left: 8), + child: Icon( + MdiIcons.paperclip, + size: 16, + ), + ), + if (task.isRunning) + Padding( + padding: const EdgeInsets.only(left: 8), + child: Icon( + Icons.play_arrow, + size: 16, + color: state.accentColor, + ), + ), + SizedBox( + width: 8, + ), + Icon( + MdiIcons.briefcaseOutline, + color: color, + size: 16, + ), + ], + ), + ], + ), + ), + ), + ), + onTap: () { + setState(() { + _isEditing = true; + }); + }, + ), + ); + } +} diff --git a/lib/ui/task/kanban/kanban_status.dart b/lib/ui/task/kanban/kanban_status.dart new file mode 100644 index 000000000..ab862bb9f --- /dev/null +++ b/lib/ui/task/kanban/kanban_status.dart @@ -0,0 +1,123 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_redux/flutter_redux.dart'; +import 'package:invoiceninja_flutter/redux/app/app_state.dart'; +import 'package:invoiceninja_flutter/ui/app/buttons/app_text_button.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +class KanbanStatusCard extends StatefulWidget { + const KanbanStatusCard({ + @required this.status, + @required this.onSavePressed, + @required this.onCancelPressed, + @required this.isSaving, + }); + final TaskStatusEntity status; + final Function(Completer, String) onSavePressed; + final Function() onCancelPressed; + final bool isSaving; + + @override + _KanbanStatusCardState createState() => _KanbanStatusCardState(); +} + +class _KanbanStatusCardState extends State { + bool _isEditing = false; + String _name = ''; + + @override + void initState() { + super.initState(); + + final status = widget.status; + _name = status.name; + } + + void _onSavePressed() { + final localization = AppLocalization.of(context); + final completer = snackBarCompleter( + context, localization.updatedTaskStatus); + completer.future.then((value) { + setState(() { + _isEditing = false; + }); + }); + + widget.onSavePressed(completer, _name.trim()); + } + + @override + Widget build(BuildContext context) { + final localization = AppLocalization.of(context); + final status = widget.status; + final state = StoreProvider.of(context).state; + final color = state.prefState.enableDarkMode + ? Theme.of(context).cardColor + : Colors.grey.shade300; + + if (_isEditing) { + return Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + DecoratedFormField( + autofocus: true, + initialValue: _name, + minLines: 1, + maxLines: 1, + onChanged: (value) => _name = value, + onSavePressed: (context) => _onSavePressed(), + ), + SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AppTextButton( + onPressed: () { + setState(() { + _isEditing = false; + if (widget.status.isNew) { + widget.onCancelPressed(); + } + }); + }, + label: localization.cancel, + ), + Padding( + padding: const EdgeInsets.only(left: 8), + child: ElevatedButton( + child: Text(localization.save), + onPressed: _onSavePressed, + ), + ), + ], + ) + ], + ), + ); + } + + return InkWell( + child: Padding( + padding: const EdgeInsets.all(8), + child: Opacity( + opacity: widget.isSaving ? .5 : 1, + child: Text( + status.isNew ? localization.unassigned : status.name, + style: TextStyle(fontWeight: FontWeight.w600), + ), + ), + ), + onTap: status.isNew + ? null + : () { + setState(() { + _isEditing = true; + }); + }, + ); + } +} diff --git a/lib/ui/task/kanban/kanban_view.dart b/lib/ui/task/kanban/kanban_view.dart new file mode 100644 index 000000000..55bedadc4 --- /dev/null +++ b/lib/ui/task/kanban/kanban_view.dart @@ -0,0 +1,278 @@ +import 'package:boardview/board_item.dart'; +import 'package:boardview/board_list.dart'; +import 'package:boardview/boardview.dart'; +import 'package:boardview/boardview_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:invoiceninja_flutter/ui/task/kanban/kanban_card.dart'; +import 'package:invoiceninja_flutter/ui/task/kanban/kanban_status.dart'; +import 'package:invoiceninja_flutter/ui/task/kanban/kanban_view_vm.dart'; +import 'package:invoiceninja_flutter/data/models/models.dart'; +import 'package:invoiceninja_flutter/utils/completers.dart'; +import 'package:invoiceninja_flutter/utils/formatting.dart'; +import 'package:invoiceninja_flutter/utils/localization.dart'; + +class KanbanView extends StatefulWidget { + const KanbanView({ + Key key, + @required this.viewModel, + }) : super(key: key); + + final KanbanVM viewModel; + + @override + KanbanViewState createState() => KanbanViewState(); +} + +class KanbanViewState extends State { + final _boardViewController = new BoardViewController(); + + List _statuses; + Map> _tasks; + bool isDragging = false; + + @override + void initState() { + super.initState(); + _initBoard(); + } + + void _initBoard() { + print('## INIT BOARD'); + final viewModel = widget.viewModel; + final state = viewModel.state; + + _statuses = state.taskStatusState.list + .where((statusId) => state.taskStatusState.get(statusId).isActive) + .toList(); + + _statuses.sort((statusIdA, statusIdB) { + final statusA = state.taskStatusState.get(statusIdA); + final statusB = state.taskStatusState.get(statusIdB); + if (statusA.statusOrder == statusB.statusOrder) { + return statusB.updatedAt.compareTo(statusA.updatedAt); + } else { + return (statusA.statusOrder ?? 99999) + .compareTo(statusB.statusOrder ?? 99999); + } + }); + + _statuses = ['', ..._statuses]; + + _tasks = {}; + viewModel.taskList.forEach((taskId) { + final task = state.taskState.map[taskId]; + final status = state.taskStatusState.get(task.statusId); + final statusId = status.isNew ? '' : status.id; + if (!_tasks.containsKey(statusId)) { + _tasks[statusId] = []; + } + _tasks[statusId].add(task.id); + }); + + _tasks.forEach((key, value) { + _tasks[key].sort((taskIdA, taskIdB) { + final taskA = state.taskState.get(taskIdA); + final taskB = state.taskState.get(taskIdB); + if (taskA.statusOrder == taskB.statusOrder) { + return taskB.updatedAt.compareTo(taskA.updatedAt); + } else { + return (taskA.statusOrder ?? 99999) + .compareTo(taskB.statusOrder ?? 99999); + } + }); + }); + } + + void _onBoardChanged() { + final localization = AppLocalization.of(context); + final completer = + snackBarCompleter(context, localization.updatedTaskStatus); + completer.future.catchError((Object error) { + _initBoard(); + }); + + // remove 'unassigned' status + final statusIds = + _statuses.where((statusId) => statusId.isNotEmpty).toList(); + + widget.viewModel.onBoardChanged(completer, statusIds, _tasks); + } + + @override + Widget build(BuildContext context) { + final state = widget.viewModel.state; + final color = state.prefState.enableDarkMode + ? Theme.of(context).cardColor + : Colors.grey.shade300; + + final boardList = _statuses.map((statusId) { + final status = state.taskStatusState.get(statusId); + final hasNewTask = + _tasks[statusId]?.any((taskId) => parseDouble(taskId) < 0) ?? false; + final hasCorectOrder = statusId.isEmpty || + status.statusOrder == _statuses.indexOf(status.id) - 1; + + return BoardList( + draggable: status.isOld && hasCorectOrder, + backgroundColor: color, + headerBackgroundColor: color, + onDropList: (endIndex, startIndex) { + if (endIndex == startIndex) { + return; + } + + setState(() { + final status = _statuses[startIndex]; + _statuses.removeAt(startIndex); + _statuses = [ + ..._statuses.sublist(0, endIndex), + status, + ..._statuses.sublist(endIndex), + ]; + }); + + _onBoardChanged(); + }, + header: [ + Expanded( + child: KanbanStatusCard( + status: status, + isSaving: !hasCorectOrder, + onSavePressed: (completer, name) { + final statusOrder = _statuses.indexOf(statusId); + widget.viewModel.onSaveStatusPressed( + completer, statusId, name, statusOrder); + }, + onCancelPressed: () { + if (status.isNew) { + _statuses.remove(status.id); + } + }, + ), + ), + ], + footer: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 8, top: 2, bottom: 4), + child: hasNewTask + ? SizedBox() + : TextButton( + child: Text(AppLocalization.of(context).newTask), + onPressed: () { + final task = TaskEntity(state: widget.viewModel.state) + .rebuild((b) => b..statusId = status.id); + setState(() { + if (!_tasks.containsKey(status.id)) { + _tasks[status.id] = []; + } + _tasks[status.id].add(task.id); + }); + }, + ), + ), + ), + items: (_tasks[status.id] ?? []) + .map((taskId) => widget.viewModel.state.taskState.get(taskId)) + .map( + (task) { + final isVisible = + widget.viewModel.filteredTaskList.contains(task.id) || + task.isNew; + return BoardItem( + draggable: task.isOld, + item: !isVisible + ? SizedBox() + : KanbanTaskCard( + task: task, + isDragging: isDragging, + isSaving: (task.statusOrder != + _tasks[status.id].indexOf(task.id)) || + task.statusId != statusId, + onSavePressed: (completer, description) { + final statusOrder = _tasks[status.id].indexOf(task.id); + widget.viewModel.onSaveTaskPressed( + completer, + task.id, + status.id, + description, + statusOrder, + ); + }, + onCancelPressed: () { + if (task.isNew) { + setState(() { + _tasks[status.id].remove(task.id); + }); + } + }, + ), + onStartDragItem: (listIndex, itemIndex, state) { + print('## START DRAG'); + setState(() => isDragging = true); + }, + /* + onDragItem: (oldListIndex, oldItemIndex, newListIndex, + newItemIndex, state) { + setState(() => _isDragging = true); + }, + */ + onDropItem: ( + int listIndex, + int itemIndex, + int oldListIndex, + int oldItemIndex, + BoardItemState state, + ) { + print('## STOP DRAG'); + setState(() => isDragging = false); + + if (listIndex == oldListIndex && itemIndex == oldItemIndex) { + return; + } + + final oldStatusId = _statuses[oldListIndex]; + final newStatusId = _statuses[listIndex]; + final taskId = _tasks[status.id][oldItemIndex]; + + setState(() { + if (_tasks.containsKey(oldStatusId) && + _tasks[oldStatusId].contains(taskId)) { + _tasks[oldStatusId].remove(taskId); + } + + if (!_tasks.containsKey(newStatusId)) { + _tasks[newStatusId] = []; + } + + _tasks[newStatusId] = [ + ..._tasks[newStatusId].sublist(0, itemIndex), + taskId, + ..._tasks[newStatusId].sublist(itemIndex), + ]; + }); + + _onBoardChanged(); + }, + ); + }, + ).toList(), + ); + }).toList(); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Stack( + alignment: Alignment.topCenter, + children: [ + BoardView( + boardViewController: _boardViewController, + lists: boardList, + dragDelay: 1, + ), + if (state.isLoading || state.isSaving) LinearProgressIndicator(), + ], + ), + ); + } +} diff --git a/lib/ui/task/kanban_view_vm.dart b/lib/ui/task/kanban/kanban_view_vm.dart similarity index 98% rename from lib/ui/task/kanban_view_vm.dart rename to lib/ui/task/kanban/kanban_view_vm.dart index 22c4c15e1..488908ebf 100644 --- a/lib/ui/task/kanban_view_vm.dart +++ b/lib/ui/task/kanban/kanban_view_vm.dart @@ -1,5 +1,4 @@ import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; @@ -9,7 +8,7 @@ import 'package:invoiceninja_flutter/redux/auth/auth_reducer.dart'; import 'package:invoiceninja_flutter/redux/task/task_actions.dart'; import 'package:invoiceninja_flutter/redux/task/task_selectors.dart'; import 'package:invoiceninja_flutter/redux/task_status/task_status_actions.dart'; -import 'package:invoiceninja_flutter/ui/task/kanban_view.dart'; +import 'package:invoiceninja_flutter/ui/task/kanban/kanban_view.dart'; import 'package:invoiceninja_flutter/utils/completers.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:redux/redux.dart'; diff --git a/lib/ui/task/kanban_view.dart b/lib/ui/task/kanban_view.dart deleted file mode 100644 index 5b3d23eb2..000000000 --- a/lib/ui/task/kanban_view.dart +++ /dev/null @@ -1,628 +0,0 @@ -import 'dart:async'; -import 'package:boardview/board_item.dart'; -import 'package:boardview/board_list.dart'; -import 'package:boardview/boardview.dart'; -import 'package:boardview/boardview_controller.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; -import 'package:invoiceninja_flutter/redux/app/app_state.dart'; -import 'package:invoiceninja_flutter/ui/app/buttons/app_text_button.dart'; -import 'package:invoiceninja_flutter/ui/app/live_text.dart'; -import 'package:invoiceninja_flutter/utils/app_context.dart'; -import 'package:invoiceninja_flutter/data/models/models.dart'; -import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; -import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; -import 'package:invoiceninja_flutter/ui/task/kanban_view_vm.dart'; -import 'package:invoiceninja_flutter/utils/colors.dart'; -import 'package:invoiceninja_flutter/utils/completers.dart'; -import 'package:invoiceninja_flutter/utils/formatting.dart'; -import 'package:invoiceninja_flutter/utils/localization.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; - -class KanbanView extends StatefulWidget { - const KanbanView({ - Key key, - @required this.viewModel, - }) : super(key: key); - - final KanbanVM viewModel; - - @override - _KanbanViewState createState() => _KanbanViewState(); -} - -class _KanbanViewState extends State { - final _boardViewController = new BoardViewController(); - - List _statuses; - Map> _tasks; - bool isDragging = false; - - @override - void initState() { - super.initState(); - _initBoard(); - } - - void _initBoard() { - print('## INIT BOARD'); - final viewModel = widget.viewModel; - final state = viewModel.state; - - _statuses = state.taskStatusState.list - .where((statusId) => state.taskStatusState.get(statusId).isActive) - .toList(); - - _statuses.sort((statusIdA, statusIdB) { - final statusA = state.taskStatusState.get(statusIdA); - final statusB = state.taskStatusState.get(statusIdB); - if (statusA.statusOrder == statusB.statusOrder) { - return statusB.updatedAt.compareTo(statusA.updatedAt); - } else { - return (statusA.statusOrder ?? 99999) - .compareTo(statusB.statusOrder ?? 99999); - } - }); - - _statuses = ['', ..._statuses]; - - _tasks = {}; - viewModel.taskList.forEach((taskId) { - final task = state.taskState.map[taskId]; - final status = state.taskStatusState.get(task.statusId); - final statusId = status.isNew ? '' : status.id; - if (!_tasks.containsKey(statusId)) { - _tasks[statusId] = []; - } - _tasks[statusId].add(task.id); - }); - - _tasks.forEach((key, value) { - _tasks[key].sort((taskIdA, taskIdB) { - final taskA = state.taskState.get(taskIdA); - final taskB = state.taskState.get(taskIdB); - if (taskA.statusOrder == taskB.statusOrder) { - return taskB.updatedAt.compareTo(taskA.updatedAt); - } else { - return (taskA.statusOrder ?? 99999) - .compareTo(taskB.statusOrder ?? 99999); - } - }); - }); - } - - void _onBoardChanged() { - final localization = AppLocalization.of(context); - final completer = - snackBarCompleter(context, localization.updatedTaskStatus); - completer.future.catchError((Object error) { - _initBoard(); - }); - - // remove 'unassigned' status - final statusIds = - _statuses.where((statusId) => statusId.isNotEmpty).toList(); - - widget.viewModel.onBoardChanged(completer, statusIds, _tasks); - } - - @override - Widget build(BuildContext context) { - final state = widget.viewModel.state; - final color = state.prefState.enableDarkMode - ? Theme.of(context).cardColor - : Colors.grey.shade300; - - final boardList = _statuses.map((statusId) { - final status = state.taskStatusState.get(statusId); - final hasNewTask = - _tasks[statusId]?.any((taskId) => parseDouble(taskId) < 0) ?? false; - final hasCorectOrder = statusId.isEmpty || - status.statusOrder == _statuses.indexOf(status.id) - 1; - - return BoardList( - draggable: status.isOld && hasCorectOrder, - backgroundColor: color, - headerBackgroundColor: color, - onDropList: (endIndex, startIndex) { - if (endIndex == startIndex) { - return; - } - - setState(() { - final status = _statuses[startIndex]; - _statuses.removeAt(startIndex); - _statuses = [ - ..._statuses.sublist(0, endIndex), - status, - ..._statuses.sublist(endIndex), - ]; - }); - - _onBoardChanged(); - }, - header: [ - Expanded( - child: _StatusCard( - status: status, - isSaving: !hasCorectOrder, - onSavePressed: (completer, name) { - final statusOrder = _statuses.indexOf(statusId); - widget.viewModel.onSaveStatusPressed( - completer, statusId, name, statusOrder); - }, - onCancelPressed: () { - if (status.isNew) { - _statuses.remove(status.id); - } - }, - ), - ), - ], - footer: Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.only(left: 8, top: 2, bottom: 4), - child: hasNewTask - ? SizedBox() - : TextButton( - child: Text(AppLocalization.of(context).newTask), - onPressed: () { - final task = TaskEntity(state: widget.viewModel.state) - .rebuild((b) => b..statusId = status.id); - setState(() { - if (!_tasks.containsKey(status.id)) { - _tasks[status.id] = []; - } - _tasks[status.id].add(task.id); - }); - }, - ), - ), - ), - items: (_tasks[status.id] ?? []) - .map((taskId) => widget.viewModel.state.taskState.get(taskId)) - .map( - (task) { - final isVisible = - widget.viewModel.filteredTaskList.contains(task.id) || - task.isNew; - return BoardItem( - draggable: task.isOld, - item: !isVisible - ? SizedBox() - : _TaskCard( - task: task, - isDragging: isDragging, - isSaving: (task.statusOrder != - _tasks[status.id].indexOf(task.id)) || - task.statusId != statusId, - onSavePressed: (completer, description) { - final statusOrder = _tasks[status.id].indexOf(task.id); - widget.viewModel.onSaveTaskPressed( - completer, - task.id, - status.id, - description, - statusOrder, - ); - }, - onCancelPressed: () { - if (task.isNew) { - setState(() { - _tasks[status.id].remove(task.id); - }); - } - }, - ), - onStartDragItem: (listIndex, itemIndex, state) { - print('## START DRAG'); - setState(() => isDragging = true); - }, - /* - onDragItem: (oldListIndex, oldItemIndex, newListIndex, - newItemIndex, state) { - setState(() => _isDragging = true); - }, - */ - onDropItem: ( - int listIndex, - int itemIndex, - int oldListIndex, - int oldItemIndex, - BoardItemState state, - ) { - print('## STOP DRAG'); - setState(() => isDragging = false); - - if (listIndex == oldListIndex && itemIndex == oldItemIndex) { - return; - } - - final oldStatusId = _statuses[oldListIndex]; - final newStatusId = _statuses[listIndex]; - final taskId = _tasks[status.id][oldItemIndex]; - - setState(() { - if (_tasks.containsKey(oldStatusId) && - _tasks[oldStatusId].contains(taskId)) { - _tasks[oldStatusId].remove(taskId); - } - - if (!_tasks.containsKey(newStatusId)) { - _tasks[newStatusId] = []; - } - - _tasks[newStatusId] = [ - ..._tasks[newStatusId].sublist(0, itemIndex), - taskId, - ..._tasks[newStatusId].sublist(itemIndex), - ]; - }); - - _onBoardChanged(); - }, - ); - }, - ).toList(), - ); - }).toList(); - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Stack( - alignment: Alignment.topCenter, - children: [ - BoardView( - boardViewController: _boardViewController, - lists: boardList, - dragDelay: 1, - ), - if (state.isLoading || state.isSaving) LinearProgressIndicator(), - ], - ), - ); - } -} - -class _TaskCard extends StatefulWidget { - const _TaskCard({ - @required this.task, - @required this.onSavePressed, - @required this.onCancelPressed, - @required this.isSaving, - @required this.isDragging, - }); - final TaskEntity task; - final Function(Completer, String) onSavePressed; - final Function() onCancelPressed; - final bool isSaving; - final bool isDragging; - - @override - __TaskCardState createState() => __TaskCardState(); -} - -class __TaskCardState extends State<_TaskCard> { - bool _isEditing = false; - bool _isHovered = false; - String _description = ''; - - @override - void initState() { - super.initState(); - - final task = widget.task; - _description = task.description; - _isEditing = task.isNew; - } - - @override - Widget build(BuildContext context) { - final localization = AppLocalization.of(context); - final store = StoreProvider.of(context); - final state = store.state; - final task = widget.task; - final project = state.projectState.get(task.projectId); - final client = state.clientState.get(task.clientId); - - var color = Colors.grey; - if (task.projectId.isNotEmpty) { - final projectIndex = state.projectState.list.indexOf(task.projectId); - color = getColorByIndex(projectIndex); - } - - final isDragging = - context.findAncestorStateOfType<_KanbanViewState>().isDragging; - - if (_isEditing && !widget.isDragging) { - return Card( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column( - children: [ - DecoratedFormField( - autofocus: true, - initialValue: _description, - minLines: 3, - maxLines: 10, - onChanged: (value) => _description = value, - ), - SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - AppTextButton( - onPressed: () { - setState(() { - _isEditing = false; - if (task.isNew) { - widget.onCancelPressed(); - } - }); - }, - label: localization.cancel, - ), - Padding( - padding: const EdgeInsets.only(left: 8), - child: ElevatedButton( - onPressed: () { - final completer = snackBarCompleter( - context, localization.updatedTask); - completer.future.then((value) { - setState(() { - _isEditing = false; - }); - }); - widget.onSavePressed(completer, _description.trim()); - }, - child: Text(localization.save), - ), - ), - ], - ) - ], - ), - ), - ); - } - - return MouseRegion( - onEnter: (event) => setState(() => _isHovered = true), - onExit: (event) => setState(() => _isHovered = false), - child: InkWell( - child: Opacity( - opacity: widget.isSaving ? .5 : 1, - child: Card( - color: Theme.of(context).backgroundColor, - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(task.description, maxLines: 3), - SizedBox(height: 8), - if (_isHovered && !isDragging) - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Expanded( - child: InkWell( - child: Center( - child: Text( - localization.viewTask, - style: TextStyle(fontSize: 12), - )), - onTap: task.isNew - ? null - : () { - if (state.taskUIState.selectedId == - task.id) { - viewEntityById( - appContext: context.getAppContext(), - entityId: '', - entityType: EntityType.task, - showError: false); - } else { - viewEntity( - appContext: context.getAppContext(), - entity: task); - } - }, - ), - ), - Expanded( - child: InkWell( - onTap: () { - handleEntityAction( - context.getAppContext(), - task, - task.isRunning - ? EntityAction.stop - : EntityAction.start); - }, - child: Center( - child: Text( - task.isRunning - ? localization.stopTask - : task.getTaskTimes().isEmpty - ? localization.startTask - : localization.resumeTask, - style: TextStyle(fontSize: 12)), - ), - ), - ), - ], - ) - else - Row( - children: [ - LiveText( - () { - return formatDuration(task.calculateDuration()) + - (client.isOld - ? ' • ' + client.displayName - : '') + - (project.isOld ? ' • ' + project.name : ''); - }, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w300, - ), - ), - Spacer(), - if (task.documents.isNotEmpty) - Padding( - padding: const EdgeInsets.only(left: 8), - child: Icon( - MdiIcons.paperclip, - size: 16, - ), - ), - if (task.isRunning) - Padding( - padding: const EdgeInsets.only(left: 8), - child: Icon( - Icons.play_arrow, - size: 16, - color: state.accentColor, - ), - ), - SizedBox( - width: 8, - ), - Icon( - MdiIcons.briefcaseOutline, - color: color, - size: 16, - ), - ], - ), - ], - ), - ), - ), - ), - onTap: () { - setState(() { - _isEditing = true; - }); - }, - ), - ); - } -} - -class _StatusCard extends StatefulWidget { - const _StatusCard({ - @required this.status, - @required this.onSavePressed, - @required this.onCancelPressed, - @required this.isSaving, - }); - final TaskStatusEntity status; - final Function(Completer, String) onSavePressed; - final Function() onCancelPressed; - final bool isSaving; - - @override - __StatusCardState createState() => __StatusCardState(); -} - -class __StatusCardState extends State<_StatusCard> { - bool _isEditing = false; - String _name = ''; - - @override - void initState() { - super.initState(); - - final status = widget.status; - _name = status.name; - } - - void _onSavePressed() { - final localization = AppLocalization.of(context); - final completer = snackBarCompleter( - context, localization.updatedTaskStatus); - completer.future.then((value) { - setState(() { - _isEditing = false; - }); - }); - - widget.onSavePressed(completer, _name.trim()); - } - - @override - Widget build(BuildContext context) { - final localization = AppLocalization.of(context); - final status = widget.status; - final state = StoreProvider.of(context).state; - final color = state.prefState.enableDarkMode - ? Theme.of(context).cardColor - : Colors.grey.shade300; - - if (_isEditing) { - return Padding( - padding: const EdgeInsets.all(8), - child: Column( - children: [ - DecoratedFormField( - autofocus: true, - initialValue: _name, - minLines: 1, - maxLines: 1, - onChanged: (value) => _name = value, - onSavePressed: (context) => _onSavePressed(), - ), - SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - AppTextButton( - onPressed: () { - setState(() { - _isEditing = false; - if (widget.status.isNew) { - widget.onCancelPressed(); - } - }); - }, - label: localization.cancel, - ), - Padding( - padding: const EdgeInsets.only(left: 8), - child: ElevatedButton( - child: Text(localization.save), - onPressed: _onSavePressed, - ), - ), - ], - ) - ], - ), - ); - } - - return InkWell( - child: Padding( - padding: const EdgeInsets.all(8), - child: Opacity( - opacity: widget.isSaving ? .5 : 1, - child: Text( - status.isNew ? localization.unassigned : status.name, - style: TextStyle(fontWeight: FontWeight.w600), - ), - ), - ), - onTap: status.isNew - ? null - : () { - setState(() { - _isEditing = true; - }); - }, - ); - } -} diff --git a/lib/ui/task/task_screen.dart b/lib/ui/task/task_screen.dart index 57ffbb26b..64ce095b3 100644 --- a/lib/ui/task/task_screen.dart +++ b/lib/ui/task/task_screen.dart @@ -6,7 +6,7 @@ import 'package:invoiceninja_flutter/redux/app/app_actions.dart'; import 'package:invoiceninja_flutter/redux/settings/settings_actions.dart'; import 'package:invoiceninja_flutter/ui/app/list_scaffold.dart'; import 'package:invoiceninja_flutter/ui/app/list_filter.dart'; -import 'package:invoiceninja_flutter/ui/task/kanban_view_vm.dart'; +import 'package:invoiceninja_flutter/ui/task/kanban/kanban_view_vm.dart'; import 'package:invoiceninja_flutter/ui/task/task_presenter.dart'; import 'package:invoiceninja_flutter/ui/task/task_screen_vm.dart'; import 'package:invoiceninja_flutter/utils/icons.dart';