mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-10 05:02:36 +01:00
Working on task kanban
This commit is contained in:
parent
3b6438459e
commit
dd7756d4ca
@ -23,6 +23,7 @@ if (! defined('APP_NAME')) {
|
|||||||
define('ENTITY_CREDIT', 'credit');
|
define('ENTITY_CREDIT', 'credit');
|
||||||
define('ENTITY_QUOTE', 'quote');
|
define('ENTITY_QUOTE', 'quote');
|
||||||
define('ENTITY_TASK', 'task');
|
define('ENTITY_TASK', 'task');
|
||||||
|
define('ENTITY_TASK_STATUS', 'task_status');
|
||||||
define('ENTITY_ACCOUNT_GATEWAY', 'account_gateway');
|
define('ENTITY_ACCOUNT_GATEWAY', 'account_gateway');
|
||||||
define('ENTITY_USER', 'user');
|
define('ENTITY_USER', 'user');
|
||||||
define('ENTITY_TOKEN', 'token');
|
define('ENTITY_TOKEN', 'token');
|
||||||
|
42
app/Http/Controllers/TaskKanbanController.php
Normal file
42
app/Http/Controllers/TaskKanbanController.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\TaskStatus;
|
||||||
|
|
||||||
|
class TaskKanbanController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return \Illuminate\Contracts\View\View
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
// check initial statuses exist
|
||||||
|
$stauses = TaskStatus::scope()->get();
|
||||||
|
|
||||||
|
if (! $stauses->count()) {
|
||||||
|
$stauses = [];
|
||||||
|
$defaults = [
|
||||||
|
'backlog',
|
||||||
|
'ready_to_do',
|
||||||
|
'in_progress',
|
||||||
|
'done',
|
||||||
|
];
|
||||||
|
for ($i=0; $i<count($defaults); $i++) {
|
||||||
|
$status = TaskStatus::createNew();
|
||||||
|
$status->name = trans('texts.' . $defaults[$i]);
|
||||||
|
$status->sort_order = $i;
|
||||||
|
$status->save();
|
||||||
|
$stauses[] = $status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'title' => trans('texts.kanban'),
|
||||||
|
'statuses' => $stauses,
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('tasks.kanban', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
30
app/Models/TaskStatus.php
Normal file
30
app/Models/TaskStatus.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class PaymentTerm.
|
||||||
|
*/
|
||||||
|
class TaskStatus extends EntityModel
|
||||||
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $timestamps = true;
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $dates = ['deleted_at'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getEntityType()
|
||||||
|
{
|
||||||
|
return ENTITY_TASK_STATUS;
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,33 @@ class AddRemember2faToken extends Migration
|
|||||||
Schema::table('users', function ($table) {
|
Schema::table('users', function ($table) {
|
||||||
$table->string('remember_2fa_token', 100)->nullable();
|
$table->string('remember_2fa_token', 100)->nullable();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Schema::dropIfExists('task_statuses');
|
||||||
|
Schema::create('task_statuses', function ($table) {
|
||||||
|
$table->increments('id');
|
||||||
|
$table->unsignedInteger('user_id');
|
||||||
|
$table->unsignedInteger('account_id')->index();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
|
||||||
|
$table->string('name')->nullable();
|
||||||
|
$table->smallInteger('sort_order')->default(0);
|
||||||
|
|
||||||
|
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
|
||||||
|
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->unsignedInteger('public_id')->index();
|
||||||
|
$table->unique(['account_id', 'public_id']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('tasks', function ($table) {
|
||||||
|
$table->unsignedInteger('task_status_id')->nullable();
|
||||||
|
$table->smallInteger('task_status_sort_order')->default(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('tasks', function ($table) {
|
||||||
|
$table->foreign('task_status_id')->references('id')->on('task_statuses')->onDelete('cascade');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,5 +55,16 @@ class AddRemember2faToken extends Migration
|
|||||||
Schema::table('users', function ($table) {
|
Schema::table('users', function ($table) {
|
||||||
$table->dropColumn('remember_2fa_token');
|
$table->dropColumn('remember_2fa_token');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Schema::table('tasks', function ($table) {
|
||||||
|
$table->dropForeign('tasks_task_status_id_foreign');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('tasks', function ($table) {
|
||||||
|
$table->dropColumn('task_status_id');
|
||||||
|
$table->dropColumn('task_status_sort_order');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::dropIfExists('task_statuses');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2613,6 +2613,11 @@ $LANG = array(
|
|||||||
'do_not_trust' => 'Do not remember this device',
|
'do_not_trust' => 'Do not remember this device',
|
||||||
'trust_for_30_days' => 'Trust for 30 days',
|
'trust_for_30_days' => 'Trust for 30 days',
|
||||||
'trust_forever' => 'Trust forever',
|
'trust_forever' => 'Trust forever',
|
||||||
|
'kanban' => 'Kanban',
|
||||||
|
'backlog' => 'Backlog',
|
||||||
|
'ready_to_do' => 'Ready to do',
|
||||||
|
'in_progress' => 'In progress',
|
||||||
|
'add_status' => 'Add status',
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -84,6 +84,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@elseif ($entityType == ENTITY_TASK)
|
@elseif ($entityType == ENTITY_TASK)
|
||||||
|
{!! Button::normal(trans('texts.kanban'))->asLinkTo(url('/tasks/kanban'))->appendIcon(Icon::create('th')) !!}
|
||||||
{!! Button::normal(trans('texts.time_tracker'))->asLinkTo('javascript:openTimeTracker()')->appendIcon(Icon::create('time')) !!}
|
{!! Button::normal(trans('texts.time_tracker'))->asLinkTo('javascript:openTimeTracker()')->appendIcon(Icon::create('time')) !!}
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
303
resources/views/tasks/kanban.blade.php
Normal file
303
resources/views/tasks/kanban.blade.php
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
@extends('header')
|
||||||
|
|
||||||
|
@section('head')
|
||||||
|
@parent
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
.kanban {
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column {
|
||||||
|
padding: 10px;
|
||||||
|
height: 100%;
|
||||||
|
width: 230px;
|
||||||
|
margin-right: 12px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
white-space: normal;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column-header {
|
||||||
|
font-weight: bold;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column-header .pull-left {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column-header .fa-times {
|
||||||
|
color: #888888;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column-header input {
|
||||||
|
width: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column-header .view {
|
||||||
|
padding-top: 3px;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column-row .view div {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column .edit {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column .editing .edit {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column .editing .view {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
@stop
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
var statuses = {!! $statuses !!};
|
||||||
|
ko.bindingHandlers.enterkey = {
|
||||||
|
init: function (element, valueAccessor, allBindings, viewModel) {
|
||||||
|
var callback = valueAccessor();
|
||||||
|
$(element).keypress(function (event) {
|
||||||
|
var keyCode = (event.which ? event.which : event.keyCode);
|
||||||
|
if (keyCode === 13) {
|
||||||
|
callback.call(viewModel);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ko.bindingHandlers.selected = {
|
||||||
|
update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
|
||||||
|
var selected = ko.utils.unwrapObservable(valueAccessor());
|
||||||
|
if (selected) element.select();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function ViewModel() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.statuses = ko.observableArray();
|
||||||
|
for (var i=0; i<statuses.length; i++) {
|
||||||
|
self.statuses.push(new StatusModel(statuses[i]));
|
||||||
|
}
|
||||||
|
self.statuses.push(new StatusModel());
|
||||||
|
|
||||||
|
self.onDragged = function() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusModel(data) {
|
||||||
|
var self = this;
|
||||||
|
self.name = ko.observable();
|
||||||
|
self.is_blank = ko.observable(false);
|
||||||
|
self.is_editing_status = ko.observable(false);
|
||||||
|
self.tasks = ko.observableArray();
|
||||||
|
self.new_task = new TaskModel();
|
||||||
|
|
||||||
|
self.inputValue = ko.computed({
|
||||||
|
read: function () {
|
||||||
|
return self.is_blank() ? '' : self.name();
|
||||||
|
},
|
||||||
|
write: function(value) {
|
||||||
|
self.name(value);
|
||||||
|
if (self.is_blank()) {
|
||||||
|
self.is_blank(false);
|
||||||
|
model.statuses.push(new StatusModel());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.placeholder = ko.computed(function() {
|
||||||
|
return self.is_blank() ? '{{ trans('texts.add_status') }}...' : '';
|
||||||
|
})
|
||||||
|
|
||||||
|
self.startEdit = function() {
|
||||||
|
self.is_editing_status(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.endEdit = function() {
|
||||||
|
self.is_editing_status(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onDragged = function() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
self.archiveStatus = function() {
|
||||||
|
window.model.statuses.remove(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cancelNewTask = function() {
|
||||||
|
if (self.new_task.is_blank()) {
|
||||||
|
self.new_task.description('');
|
||||||
|
}
|
||||||
|
self.new_task.endEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.saveNewTask = function(task) {
|
||||||
|
var task = new TaskModel({
|
||||||
|
description: task.description()
|
||||||
|
})
|
||||||
|
self.tasks.push(task);
|
||||||
|
self.new_task.reset();
|
||||||
|
self.is_blank(false);
|
||||||
|
self.endEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
ko.mapping.fromJS(data, {}, this);
|
||||||
|
self.tasks.push(new TaskModel({description:'testing'}));
|
||||||
|
} else {
|
||||||
|
self.name('{{ trans('texts.add_status') }}...');
|
||||||
|
self.is_blank(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function TaskModel(data) {
|
||||||
|
var self = this;
|
||||||
|
self.description = ko.observable('');
|
||||||
|
self.is_blank = ko.observable(false);
|
||||||
|
self.is_editing_task = ko.observable(false);
|
||||||
|
|
||||||
|
self.startEdit = function() {
|
||||||
|
console.log('start edit');
|
||||||
|
|
||||||
|
self.is_editing_task(true);
|
||||||
|
$('.kanban-column-row.editing textarea').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.endEdit = function() {
|
||||||
|
self.is_editing_task(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onDragged = function() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cancelEditTask = function() {
|
||||||
|
/*
|
||||||
|
if (self.new_task.is_blank()) {
|
||||||
|
self.new_task.description('');
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
self.endEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.saveEditTask = function(task) {
|
||||||
|
self.endEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.reset = function() {
|
||||||
|
self.endEdit();
|
||||||
|
self.description('');
|
||||||
|
self.is_blank(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
ko.mapping.fromJS(data, {}, this);
|
||||||
|
} else {
|
||||||
|
//self.description('{{ trans('texts.add_task') }}...');
|
||||||
|
self.is_blank(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
window.model = new ViewModel();
|
||||||
|
ko.applyBindings(model);
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- <div data-bind="text: ko.toJSON(model)"></div> -->
|
||||||
|
<script id="itemTmpl" type="text/html">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="kanban">
|
||||||
|
<div data-bind="sortable: { data: statuses, as: 'status', afterMove: onDragged, allowDrop: true, connectClass: 'connect-column' }">
|
||||||
|
<div class="well kanban-column">
|
||||||
|
|
||||||
|
<div class="kanban-column-header" data-bind="css: { editing: is_editing_status }">
|
||||||
|
<div class="pull-left" data-bind="event: { click: startEdit }">
|
||||||
|
<div class="view" data-bind="text: name"></div>
|
||||||
|
<input class="edit" type="text"
|
||||||
|
data-bind="value: inputValue, hasfocus: is_editing_status, selected: is_editing_status,
|
||||||
|
placeholder: placeholder, event: { blur: endEdit }, enterkey: endEdit"/>
|
||||||
|
</div>
|
||||||
|
<div class="pull-right" data-bind="click: archiveStatus, visible: ! is_blank()">
|
||||||
|
<i class="fa fa-times" title="{{ trans('texts.archive') }}"></i>
|
||||||
|
</div><br/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-bind="sortable: { data: tasks, as: 'task', afterMove: onDragged, allowDrop: true, connectClass: 'connect-row' }">
|
||||||
|
<div class="kanban-column-row" data-bind="css: { editing: is_editing_task }">
|
||||||
|
<div data-bind="event: { click: startEdit }">
|
||||||
|
<div class="view panel" data-bind="visible: ! is_blank()">
|
||||||
|
<div data-bind="text: description"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="edit">
|
||||||
|
<textarea data-bind="value: description"></textarea>
|
||||||
|
<div class="pull-right">
|
||||||
|
<button type='button' class='btn btn-default btn-sm' data-bind="click: cancelEditTask">
|
||||||
|
{{ trans('texts.cancel') }}
|
||||||
|
</button>
|
||||||
|
<button type='button' class='btn btn-success btn-sm' data-bind="click: saveEditTask">
|
||||||
|
{{ trans('texts.save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kanban-column-row" data-bind="css: { editing: new_task.is_editing_task }, with: new_task">
|
||||||
|
<div data-bind="event: { click: startEdit }">
|
||||||
|
<div class="view panel" data-bind="visible: ! is_blank()">
|
||||||
|
<div data-bind="text: description"></div>
|
||||||
|
</div>
|
||||||
|
<a href="#" class="view text-muted" data-bind="visible: is_blank">
|
||||||
|
{{ trans('texts.new_task') }}...
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="edit">
|
||||||
|
<textarea data-bind="value: description"></textarea>
|
||||||
|
<div class="pull-right">
|
||||||
|
<button type='button' class='btn btn-default btn-sm' data-bind="click: $parent.cancelNewTask">
|
||||||
|
{{ trans('texts.cancel') }}
|
||||||
|
</button>
|
||||||
|
<button type='button' class='btn btn-success btn-sm' data-bind="click: $parent.saveNewTask">
|
||||||
|
{{ trans('texts.save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@stop
|
@ -143,6 +143,7 @@ Route::group(['middleware' => ['lookup:user', 'auth:user']], function () {
|
|||||||
Route::get('clients/statement/{client_id}/{status_id?}/{start_date?}/{end_date?}', 'ClientController@statement');
|
Route::get('clients/statement/{client_id}/{status_id?}/{start_date?}/{end_date?}', 'ClientController@statement');
|
||||||
|
|
||||||
Route::get('time_tracker', 'TimeTrackerController@index');
|
Route::get('time_tracker', 'TimeTrackerController@index');
|
||||||
|
Route::get('tasks/kanban', 'TaskKanbanController@index');
|
||||||
Route::resource('tasks', 'TaskController');
|
Route::resource('tasks', 'TaskController');
|
||||||
Route::get('api/tasks/{client_id?}', 'TaskController@getDatatable');
|
Route::get('api/tasks/{client_id?}', 'TaskController@getDatatable');
|
||||||
Route::get('tasks/create/{client_id?}/{project_id?}', 'TaskController@create');
|
Route::get('tasks/create/{client_id?}/{project_id?}', 'TaskController@create');
|
||||||
|
Loading…
Reference in New Issue
Block a user