1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-14 23:22:52 +01:00
invoiceninja/resources/views/tasks/kanban.blade.php

570 lines
20 KiB
PHP
Raw Normal View History

2017-12-17 18:54:54 +01:00
@extends('header')
@section('head')
@parent
<style type="text/css">
2017-12-19 09:30:42 +01:00
2017-12-17 18:54:54 +01:00
.kanban {
overflow-x: auto;
white-space: nowrap;
2017-12-17 21:41:36 +01:00
min-height: 540px;
2017-12-17 18:54:54 +01:00
}
2017-12-18 12:05:34 +01:00
.kanban input {
width: 100%;
}
2017-12-17 18:54:54 +01:00
.kanban-column {
2017-12-17 23:25:30 +01:00
background-color: #E9E9E9;
2017-12-17 18:54:54 +01:00
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;
}
2017-12-17 19:59:10 +01:00
.kanban-column-row {
2017-12-18 22:28:07 +01:00
margin-bottom: -12px;
2017-12-17 19:59:10 +01:00
}
2017-12-17 21:10:54 +01:00
.kanban-column-row .fa-circle {
float:right;
padding-top: 10px;
padding-right: 8px;
}
2017-12-18 22:28:07 +01:00
.kanban-column-row .panel {
word-break: break-all;
}
2017-12-17 18:54:54 +01:00
.kanban-column-row .view div {
padding: 8px;
}
.kanban-column textarea {
2017-12-17 19:59:10 +01:00
resize: vertical;
2017-12-17 18:54:54 +01:00
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;
}
2017-12-17 21:10:54 +01:00
.project-group0 { color: #000000; }
.project-group1 { color: #1c9f77; }
.project-group2 { color: #d95d02; }
.project-group3 { color: #716cb1; }
.project-group4 { color: #e62a8b; }
.project-group5 { color: #5fa213; }
.project-group6 { color: #e6aa04; }
.project-group7 { color: #a87821; }
.project-group8 { color: #676767; }
2017-12-17 18:54:54 +01:00
</style>
@stop
2017-12-17 23:25:30 +01:00
@section('top-right')
<div class="form-group">
<input type="text" placeholder="{{ trans('texts.filter') }}" data-bind="value: filter, valueUpdate: 'afterkeydown'"
class="form-control" style="background-color: #FFFFFF !important"/>
</div>
@stop
2017-12-17 18:54:54 +01:00
@section('content')
<script type="text/javascript">
2017-12-17 19:59:10 +01:00
2017-12-17 18:54:54 +01:00
var statuses = {!! $statuses !!};
2017-12-17 19:59:10 +01:00
var tasks = {!! $tasks !!};
2017-12-17 21:10:54 +01:00
var projects = {!! $projects !!};
var clients = {!! $clients !!};
var projectMap = {};
var clientMap = {};
2017-12-18 12:05:34 +01:00
var statusMap = {};
2017-12-17 19:59:10 +01:00
2017-12-17 18:54:54 +01:00
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();
2017-12-17 23:25:30 +01:00
self.is_adding_status = ko.observable(false);
self.new_status = ko.observable('');
self.filter = ko.observable('');
2017-12-18 22:28:07 +01:00
self.is_sending_request = ko.observable(false);
2017-12-17 23:25:30 +01:00
2017-12-17 18:54:54 +01:00
for (var i=0; i<statuses.length; i++) {
2017-12-17 19:59:10 +01:00
var status = statuses[i];
var statusModel = new StatusModel(status);
2017-12-18 12:05:34 +01:00
statusMap[status.public_id] = statusModel;
2017-12-17 19:59:10 +01:00
self.statuses.push(statusModel);
2017-12-17 18:54:54 +01:00
}
2017-12-17 21:10:54 +01:00
for (var i=0; i<projects.length; i++) {
var project = projects[i];
projectMap[project.public_id] = new ProjectModel(project);
}
for (var i=0; i<clients.length; i++) {
var client = clients[i];
2017-12-17 23:25:30 +01:00
clientMap[client.public_id] = new ClientModel(client);
2017-12-17 21:10:54 +01:00
}
2017-12-17 19:59:10 +01:00
for (var i=0; i<tasks.length; i++) {
var task = tasks[i];
var taskModel = new TaskModel(task);
2017-12-19 09:14:26 +01:00
var statusModel = false;
2017-12-18 12:05:34 +01:00
if (task.task_status) {
var statusModel = statusMap[task.task_status.public_id];
}
2017-12-19 09:14:26 +01:00
if (! statusModel) {
statusModel = self.statuses()[0];
}
if (statusModel) {
statusModel.tasks.push(taskModel);
}
2017-12-17 19:59:10 +01:00
}
2017-12-19 09:27:28 +01:00
self.startNewStatus = function() {
2017-12-17 23:25:30 +01:00
self.is_adding_status(true);
2017-12-17 23:36:55 +01:00
$('.kanban-column-last .kanban-column-row.editing textarea').focus();
2017-12-17 23:25:30 +01:00
}
2017-12-19 09:27:28 +01:00
self.cancelNewStatus = function() {
2017-12-17 23:25:30 +01:00
self.is_adding_status(false);
}
2017-12-19 09:27:28 +01:00
self.saveNewStatus = function() {
2017-12-17 23:25:30 +01:00
var statusModel = new StatusModel({
2017-12-19 12:25:13 +01:00
name: self.new_status(),
sort_order: self.statuses().length,
2017-12-17 23:25:30 +01:00
})
2017-12-19 09:14:26 +01:00
var url = '{{ url('/task_statuses') }}';
2017-12-19 12:25:13 +01:00
var data = statusModel.toData();
2017-12-19 09:14:26 +01:00
self.ajax('post', url, data, function(response) {
statusModel.public_id(response.public_id);
self.statuses.push(statusModel);
self.is_adding_status(false);
})
}
self.ajax = function(method, url, data, callback) {
model.is_sending_request(true);
$.ajax({
type: method,
url: url,
data: data,
dataType: 'json',
accepts: {
json: 'application/json'
},
success: function(response) {
callback(response);
},
error: function(error) {
console.log('error');
console.log(error);
},
}).always(function() {
setTimeout(function() {
model.is_sending_request(false);
}, 1000);
});
2017-12-17 23:25:30 +01:00
}
2017-12-17 18:54:54 +01:00
self.onDragged = function() {
}
}
function StatusModel(data) {
var self = this;
self.name = ko.observable();
2017-12-19 12:25:13 +01:00
self.sort_order = ko.observable();
2017-12-18 22:28:07 +01:00
self.public_id = ko.observable();
2017-12-17 18:54:54 +01:00
self.is_editing_status = ko.observable(false);
2017-12-17 19:59:10 +01:00
self.is_header_hovered = ko.observable(false);
2017-12-17 18:54:54 +01:00
self.tasks = ko.observableArray();
self.new_task = new TaskModel();
2017-12-19 12:25:13 +01:00
self.toData = function() {
return 'name=' + encodeURIComponent(self.name()) +
'&sort_order=' + self.sort_order();
}
2017-12-17 19:59:10 +01:00
self.onHeaderMouseOver = function() {
self.is_header_hovered(true);
}
self.onHeaderMouseOut = function() {
self.is_header_hovered(false);
}
2017-12-19 09:27:28 +01:00
self.startEditStatus = function() {
2017-12-17 18:54:54 +01:00
self.is_editing_status(true);
}
2017-12-19 09:27:28 +01:00
self.saveEditStatus = function() {
var url = '{{ url('/task_statuses') }}/' + self.public_id();
var data = 'name=' + encodeURIComponent(self.name());
model.ajax('put', url, data, function(response) {
self.is_editing_status(false);
})
}
self.cancelEditStatus = function() {
2017-12-17 18:54:54 +01:00
self.is_editing_status(false);
}
self.onDragged = function() {
}
self.archiveStatus = function() {
2017-12-17 21:41:36 +01:00
sweetConfirm(function() {
2017-12-19 09:14:26 +01:00
var url = '{{ url('/task_statuses') }}/' + self.public_id();
model.ajax('delete', url, null, function(response) {
model.statuses.remove(self);
})
2017-12-17 21:41:36 +01:00
}, "{{ trans('texts.archive_status')}}");
2017-12-17 18:54:54 +01:00
}
self.cancelNewTask = function() {
if (self.new_task.is_blank()) {
self.new_task.description('');
}
2017-12-19 09:30:42 +01:00
self.new_task.is_editing_task(false);
2017-12-17 18:54:54 +01:00
}
2017-12-17 19:59:10 +01:00
self.saveNewTask = function() {
2017-12-18 22:28:07 +01:00
var task = self.new_task;
var description = (task.description() || '').trim();
2017-12-18 12:16:16 +01:00
if (! description) {
return false;
}
2017-12-17 18:54:54 +01:00
var task = new TaskModel({
2017-12-18 22:28:07 +01:00
description: description,
task_status_id: self.public_id(),
task_status_sort_order: self.tasks.length,
2017-12-17 18:54:54 +01:00
})
2017-12-18 22:28:07 +01:00
2017-12-19 09:14:26 +01:00
var url = '{{ url('/tasks') }}';
var data = task.toData();
model.ajax('post', url, data, function(response) {
task.public_id(response.public_id);
self.tasks.push(task);
self.new_task.reset();
})
2017-12-17 18:54:54 +01:00
}
if (data) {
ko.mapping.fromJS(data, {}, this);
}
}
function TaskModel(data) {
var self = this;
2017-12-17 21:41:36 +01:00
self.public_id = ko.observable(0);
2017-12-17 18:54:54 +01:00
self.description = ko.observable('');
2017-12-17 19:59:10 +01:00
self.description.orig = ko.observable('');
2017-12-17 18:54:54 +01:00
self.is_blank = ko.observable(false);
self.is_editing_task = ko.observable(false);
2017-12-17 21:10:54 +01:00
self.project = ko.observable();
2017-12-17 23:25:30 +01:00
self.client = ko.observable();
2017-12-18 22:28:07 +01:00
self.task_status_id = ko.observable();
self.task_status_sort_order = ko.observable();
2017-12-17 21:10:54 +01:00
self.projectColor = ko.computed(function() {
if (! self.project()) {
return '';
}
var projectId = self.project().public_id();
var colorNum = (projectId-1) % 8;
return 'project-group' + (colorNum+1);
})
2017-12-17 18:54:54 +01:00
2017-12-19 09:30:42 +01:00
self.startEditTask = function() {
2017-12-17 19:59:10 +01:00
self.description.orig(self.description());
2017-12-17 18:54:54 +01:00
self.is_editing_task(true);
$('.kanban-column-row.editing textarea').focus();
}
self.onDragged = function() {
}
2017-12-18 22:28:07 +01:00
self.toData = function() {
return 'description=' + encodeURIComponent(self.description()) +
'&task_status_id=' + self.task_status_id() +
'&task_status_sort_order=' + self.task_status_sort_order();
}
2017-12-17 23:25:30 +01:00
self.matchesFilter = function(filter) {
if (filter) {
filter = filter.toLowerCase();
var parts = filter.split(' ');
for (var i=0; i<parts.length; i++) {
var part = parts[i];
var isMatch = false;
if (self.description()) {
if (self.description().toLowerCase().indexOf(part) >= 0) {
isMatch = true;
}
}
if (self.project()) {
var projectName = self.project().name();
if (projectName && projectName.toLowerCase().indexOf(part) >= 0) {
isMatch = true;
}
}
if (self.client()) {
var clientName = self.client().displayName();
if (clientName && clientName.toLowerCase().indexOf(part) >= 0) {
isMatch = true;
}
}
if (! isMatch) {
return false;
}
}
}
return true;
}
2017-12-17 18:54:54 +01:00
self.cancelEditTask = function() {
2017-12-17 19:59:10 +01:00
if (self.is_blank()) {
self.description('');
} else {
self.description(self.description.orig());
2017-12-17 18:54:54 +01:00
}
2017-12-17 19:59:10 +01:00
2017-12-19 09:30:42 +01:00
self.is_editing_task(false);
2017-12-17 18:54:54 +01:00
}
2017-12-17 21:41:36 +01:00
self.saveEditTask = function() {
2017-12-18 22:35:43 +01:00
var description = (self.description() || '').trim();
if (! description) {
return false;
}
2017-12-19 09:14:26 +01:00
var url = '{{ url('/tasks') }}/' + self.public_id();
var data = self.toData();
model.ajax('put', url, data, function(response) {
2017-12-19 09:30:42 +01:00
self.is_editing_task(false);
2017-12-19 09:14:26 +01:00
});
2017-12-17 18:54:54 +01:00
}
2017-12-17 21:41:36 +01:00
self.viewTask = function() {
//console.log();
2017-12-17 23:25:30 +01:00
window.open('{{ url('/tasks') }}/' + self.public_id() + '/edit', 'task');
2017-12-17 21:41:36 +01:00
}
2017-12-17 18:54:54 +01:00
self.reset = function() {
2017-12-19 09:30:42 +01:00
self.is_editing_task(false);
2017-12-17 18:54:54 +01:00
self.is_blank(true);
2017-12-19 09:30:42 +01:00
self.description('');
2017-12-17 18:54:54 +01:00
}
2017-12-17 21:10:54 +01:00
self.mapping = {
'project': {
create: function(options) {
return projectMap[options.data.public_id];
}
2017-12-17 23:25:30 +01:00
},
'client': {
create: function(options) {
return clientMap[options.data.public_id];
}
2017-12-17 21:10:54 +01:00
}
}
2017-12-17 18:54:54 +01:00
if (data) {
2017-12-17 21:10:54 +01:00
ko.mapping.fromJS(data, self.mapping, this);
2017-12-17 18:54:54 +01:00
} else {
//self.description('{{ trans('texts.add_task') }}...');
self.is_blank(true);
}
2017-12-17 21:10:54 +01:00
}
2017-12-17 18:54:54 +01:00
2017-12-17 21:10:54 +01:00
function ProjectModel(data) {
var self = this;
self.name = ko.observable();
if (data) {
ko.mapping.fromJS(data, {}, this);
}
2017-12-17 18:54:54 +01:00
}
2017-12-17 23:25:30 +01:00
function ClientModel(data) {
var self = this;
self.name = ko.observable();
self.displayName = ko.computed(function() {
return self.name();
})
if (data) {
ko.mapping.fromJS(data, {}, this);
}
}
2017-12-17 18:54:54 +01:00
$(function() {
2017-12-18 22:28:07 +01:00
toastr.options.timeOut = 3000;
toastr.options.positionClass = 'toast-bottom-right';
2017-12-17 18:54:54 +01:00
window.model = new ViewModel();
ko.applyBindings(model);
2017-12-18 22:28:07 +01:00
2017-12-17 23:28:11 +01:00
$('.kanban').show();
2017-12-17 18:54:54 +01:00
});
</script>
2017-12-17 23:28:11 +01:00
<div class="kanban" style="display: none">
2017-12-17 23:25:30 +01:00
<div data-bind="sortable: { data: statuses, as: 'status', afterMove: onDragged, allowDrop: true, connectClass: 'connect-column' }" style="float:left">
2017-12-17 18:54:54 +01:00
<div class="well kanban-column">
2017-12-17 19:59:10 +01:00
<div class="kanban-column-header" data-bind="css: { editing: is_editing_status }, event: { mouseover: onHeaderMouseOver, mouseout: onHeaderMouseOut }">
2017-12-19 09:27:28 +01:00
<div class="pull-left" data-bind="event: { click: startEditStatus }">
2017-12-17 18:54:54 +01:00
<div class="view" data-bind="text: name"></div>
2017-12-19 09:27:28 +01:00
<input class="edit" type="text" data-bind="value: name, valueUpdate: 'afterkeydown', hasfocus: is_editing_status, selected: is_editing_status,
event: { blur: cancelEditStatus }, enterkey: saveEditStatus"/>
2017-12-17 18:54:54 +01:00
</div>
2017-12-17 23:25:30 +01:00
<div class="pull-right" data-bind="click: archiveStatus, visible: is_header_hovered">
2017-12-17 18:54:54 +01:00
<i class="fa fa-times" title="{{ trans('texts.archive') }}"></i>
</div><br/>
</div>
2017-12-18 12:34:25 +01:00
<div data-bind="sortable: { data: tasks, as: 'task', afterMove: onDragged, allowDrop: true, connectClass: 'connect-row' }" style="min-height:8px">
2017-12-17 23:25:30 +01:00
<div class="kanban-column-row" data-bind="css: { editing: is_editing_task }, visible: task.matchesFilter($root.filter())">
2017-12-19 09:30:42 +01:00
<div data-bind="event: { click: startEditTask }">
2017-12-17 23:25:30 +01:00
<div class="view panel">
2017-12-17 21:10:54 +01:00
<i class="fa fa-circle" data-bind="visible: project, css: projectColor"></i>
2017-12-17 18:54:54 +01:00
<div data-bind="text: description"></div>
</div>
</div>
<div class="edit">
2017-12-17 19:59:10 +01:00
<textarea data-bind="value: description, valueUpdate: 'afterkeydown', enterkey: saveEditTask"></textarea>
2017-12-17 18:54:54 +01:00
<div class="pull-right">
<button type='button' class='btn btn-default btn-sm' data-bind="click: cancelEditTask">
{{ trans('texts.cancel') }}
</button>
2017-12-17 21:41:36 +01:00
<button type='button' class='btn btn-primary btn-sm' data-bind="click: viewTask">
{{ trans('texts.view') }}
</button>
2017-12-17 18:54:54 +01:00
<button type='button' class='btn btn-success btn-sm' data-bind="click: saveEditTask">
{{ trans('texts.save') }}
</button>
</div>
2017-12-18 12:34:25 +01:00
<div class="clearfix" style="padding-bottom:20px"></div>
2017-12-17 18:54:54 +01:00
</div>
</div>
</div>
2017-12-17 23:36:55 +01:00
<div class="kanban-column-row" data-bind="css: { editing: new_task.is_editing_task }, with: new_task">
2017-12-19 09:30:42 +01:00
<div data-bind="event: { click: startEditTask }" style="padding-bottom:6px">
2017-12-17 19:59:10 +01:00
<a href="#" class="view text-muted" style="font-size:13px" data-bind="visible: is_blank">
2017-12-17 18:54:54 +01:00
{{ trans('texts.new_task') }}...
</a>
</div>
<div class="edit">
2017-12-17 19:59:10 +01:00
<textarea data-bind="value: description, valueUpdate: 'afterkeydown', enterkey: $parent.saveNewTask"></textarea>
2017-12-17 18:54:54 +01:00
<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>
2017-12-17 23:25:30 +01:00
2017-12-17 23:36:55 +01:00
<div class="kanban-column kanban-column-last well">
2017-12-17 23:25:30 +01:00
<div class="kanban-column-row" data-bind="css: { editing: is_adding_status }">
2017-12-19 09:27:28 +01:00
<div class="view" data-bind="event: { click: startNewStatus }" style="padding-bottom: 8px;">
2017-12-18 12:05:34 +01:00
<a href="#" class="text-muted" style="font-size:13px">
2017-12-17 23:25:30 +01:00
{{ trans('texts.new_status') }}...
</a>
</div>
<div class="edit">
2017-12-18 12:05:34 +01:00
<input data-bind="value: new_status, valueUpdate: 'afterkeydown',
2017-12-19 09:27:28 +01:00
hasfocus: is_adding_status, selected: is_adding_status, enterkey: saveNewStatus"></textarea>
2017-12-18 12:05:34 +01:00
<div class="pull-right" style="padding-top:6px">
2017-12-19 09:27:28 +01:00
<button type='button' class='btn btn-default btn-sm' data-bind="click: cancelNewStatus">
2017-12-17 23:25:30 +01:00
{{ trans('texts.cancel') }}
</button>
2017-12-19 09:27:28 +01:00
<button type='button' class='btn btn-success btn-sm' data-bind="click: saveNewStatus">
2017-12-17 23:25:30 +01:00
{{ trans('texts.save') }}
</button>
</div>
</div>
</div>
</div>
2017-12-17 18:54:54 +01:00
</div>
@stop