diff --git a/controllers/TasksApiController.php b/controllers/TasksApiController.php index 5a1f83f9..0c8370af 100644 --- a/controllers/TasksApiController.php +++ b/controllers/TasksApiController.php @@ -18,4 +18,23 @@ class TasksApiController extends BaseApiController { return $this->ApiResponse($this->TasksService->GetCurrent()); } + + public function MarkTaskAsCompleted(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) + { + $doneTime = date('Y-m-d H:i:s'); + if (isset($request->getQueryParams()['done_time']) && !empty($request->getQueryParams()['done_time']) && IsIsoDateTime($request->getQueryParams()['done_time'])) + { + $doneTime = $request->getQueryParams()['done_time']; + } + + try + { + $this->TasksService->MarkTaskAsCompleted($args['taskId'], $doneTime); + return $this->VoidApiActionResponse($response); + } + catch (\Exception $ex) + { + return $this->VoidApiActionResponse($response, false, 400, $ex->getMessage()); + } + } } diff --git a/controllers/TasksController.php b/controllers/TasksController.php index 24999da5..e928b118 100644 --- a/controllers/TasksController.php +++ b/controllers/TasksController.php @@ -16,10 +16,20 @@ class TasksController extends BaseController public function Overview(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) { + if (isset($request->getQueryParams()['include_done'])) + { + $tasks = $this->Database->tasks()->orderBy('name'); + } + else + { + $tasks = $this->TasksService->GetCurrent(); + } + return $this->AppContainer->view->render($response, 'tasks', [ - 'tasks' => $this->Database->tasks()->orderBy('name'), + 'tasks' => $tasks, 'nextXDays' => 5, - 'taskCategories' => $this->Database->task_categories()->orderBy('name') + 'taskCategories' => $this->Database->task_categories()->orderBy('name'), + 'users' => $this->Database->users() ]); } @@ -29,7 +39,8 @@ class TasksController extends BaseController { return $this->AppContainer->view->render($response, 'taskform', [ 'mode' => 'create', - 'taskCategories' => $this->Database->task_categories()->orderBy('name') + 'taskCategories' => $this->Database->task_categories()->orderBy('name'), + 'users' => $this->Database->users()->orderBy('username') ]); } else @@ -37,7 +48,8 @@ class TasksController extends BaseController return $this->AppContainer->view->render($response, 'taskform', [ 'task' => $this->Database->tasks($args['taskId']), 'mode' => 'edit', - 'taskCategories' => $this->Database->task_categories()->orderBy('name') + 'taskCategories' => $this->Database->task_categories()->orderBy('name'), + 'users' => $this->Database->users()->orderBy('username') ]); } } diff --git a/grocy.openapi.json b/grocy.openapi.json index 65ce2f0d..7053969e 100644 --- a/grocy.openapi.json +++ b/grocy.openapi.json @@ -1301,6 +1301,56 @@ } } } + }, + "/tasks/mark-task-as-completed/{taskId}": { + "get": { + "description": "Marks the given task as completed", + "tags": [ + "Tasks" + ], + "parameters": [ + { + "in": "path", + "name": "taskId", + "required": true, + "description": "A valid task id", + "schema": { + "type": "integer" + } + }, + { + "in": "query", + "name": "done_time", + "required": false, + "description": "The time of when the task was completed, when omitted, the current time is used", + "schema": { + "type": "date-time" + } + } + ], + "responses": { + "200": { + "description": "A VoidApiActionResponse object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VoidApiActionResponse" + } + } + } + }, + "400": { + "description": "A VoidApiActionResponse object (possible errors are: Not existing task)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExampleVoidApiActionResponse" + } + } + } + } + } + } } }, "components": { @@ -1933,10 +1983,26 @@ "name": { "type": "string" }, - "due": { + "description": { + "type": "string" + }, + "due_date": { "type": "string", "format": "date-time" }, + "done": { + "type": "integer" + }, + "done_timestamp": { + "type": "string", + "format": "date-time" + }, + "category_id": { + "type": "integer" + }, + "assigned_to_user_id": { + "type": "integer" + }, "row_created_timestamp": { "type": "string", "format": "date-time" diff --git a/localization/de.php b/localization/de.php index 6f2c6a5f..d64d1248 100644 --- a/localization/de.php +++ b/localization/de.php @@ -216,6 +216,25 @@ return array( 'Click to show technical details' => 'Klick um technische Details anzuzeigen', 'Error while saving, probably this item already exists' => 'Fehler beim Speichern, möglicherweise existiert das Element bereits', 'Error details' => 'Fehlerdetails', + 'Tasks' => 'Aufgaben', + 'Show done tasks' => 'Erledigte Aufgaben anzeigen', + 'Task' => 'Aufgabe', + 'Due' => 'Fällig', + 'Assigned to' => 'Zugewiesen an', + 'Mark task "#1" as completed' => 'Aufgabe "#1" als erledigt markieren', + 'Uncategorized' => 'Nicht kategorisiert', + 'Task categories' => 'Aufgabenkategorien', + 'Create task' => 'Aufgabe erstellen', + 'A due date is required' => 'Ein Fälligkeitsdatum ist erforderlich', + 'Category' => 'Kategorie', + 'Edit task' => 'Aufgabe bearbeiten', + 'Are you sure to delete task "#1"?' => 'Aufgabe "#1" wirklich löschen?', + '#1 task is due to be done within the next #2 days' => '#1 Aufgabe steht in den nächsten #2 Tagen an', + '#1 tasks are due to be done within the next #2 days' => '#1 Aufgaben stehen in den nächsten #2 Tagen an', + '#1 task is overdue to be done' => '#1 Aufgabe ist überfällig', + '#1 tasks are overdue to be done' => '#1 Aufgaben sind überfällig', + 'Edit task category' => 'Aufgabenkategorie bearbeiten', + 'Create task category' => 'Aufgabenkategorie erstellen', //Constants 'manually' => 'Manuell', @@ -285,5 +304,11 @@ return array( 'Grams' => 'Gramm', 'Flour' => 'Mehl', 'Pancakes' => 'Pfannkuchen', - 'Sugar' => 'Zucker' + 'Sugar' => 'Zucker', + 'Home' => 'Zuhause', + 'Life' => 'Leben', + 'Projects' => 'Projekte', + 'Repair the garage door' => 'Garagentor reparieren', + 'Fork and improve grocy' => 'grocy forken und verbessern', + 'Find a solution for what to do when I forget the door keys' => 'Eine Lösung für "Haustürschlüssel vergessen" finden' ); diff --git a/migrations/0036.sql b/migrations/0036.sql index 0485a635..7e11119e 100644 --- a/migrations/0036.sql +++ b/migrations/0036.sql @@ -4,8 +4,9 @@ CREATE TABLE tasks ( description TEXT, due_date DATETIME, done TINYINT NOT NULL DEFAULT 0 CHECK(done IN (0, 1)), - done_date DATETIME, + done_timestamp DATETIME, category_id INTEGER, + assigned_to_user_id INTEGER, row_created_timestamp DATETIME DEFAULT (datetime('now', 'localtime')) ); @@ -20,5 +21,4 @@ CREATE VIEW tasks_current AS SELECT * FROM tasks -WHERE due_date IS NULL - OR (due_date IS NOT NULL AND due_date > datetime('now', 'localtime')); +WHERE done = 0; diff --git a/public/css/grocy.css b/public/css/grocy.css index 0e0acff4..5b5cb2c0 100644 --- a/public/css/grocy.css +++ b/public/css/grocy.css @@ -67,6 +67,14 @@ a.discrete-link:focus { color: inherit; } +.text-strike-through { + text-decoration: line-through; +} + +button.disabled { + pointer-events: none; +} + /* Hide the default up/down arrow buttons for number inputs because we use our own buttons in numberpicker */ input[type='number'] { -moz-appearance: textfield; diff --git a/public/viewjs/components/datetimepicker.js b/public/viewjs/components/datetimepicker.js index 762d82f1..99fd4fef 100644 --- a/public/viewjs/components/datetimepicker.js +++ b/public/viewjs/components/datetimepicker.js @@ -29,6 +29,10 @@ if (Grocy.Components.DateTimePicker.GetInputElement().data('init-with-now') === { startDate = moment().format(Grocy.Components.DateTimePicker.GetInputElement().data('format')); } +if (Grocy.Components.DateTimePicker.GetInputElement().data('init-value').length > 0) +{ + startDate = moment(Grocy.Components.DateTimePicker.GetInputElement().data('init-value')).format(Grocy.Components.DateTimePicker.GetInputElement().data('format')); +} var limitDate = moment('2999-12-31 23:59:59'); if (Grocy.Components.DateTimePicker.GetInputElement().data('limit-end-to-now') === true) diff --git a/public/viewjs/taskform.js b/public/viewjs/taskform.js index 547f4987..a01da5a8 100644 --- a/public/viewjs/taskform.js +++ b/public/viewjs/taskform.js @@ -2,9 +2,14 @@ { e.preventDefault(); + var jsonData = $('#task-form').serializeJSON(); + jsonData.assigned_to_user_id = jsonData.user_id; + delete jsonData.user_id; + jsonData.due_date = Grocy.Components.DateTimePicker.GetValue(); + if (Grocy.EditMode === 'create') { - Grocy.Api.Post('add-object/tasks', $('#task-form').serializeJSON(), + Grocy.Api.Post('add-object/tasks', jsonData, function(result) { window.location.href = U('/tasks'); @@ -17,7 +22,7 @@ } else { - Grocy.Api.Post('edit-object/tasks/' + Grocy.EditObjectId, $('#task-form').serializeJSON(), + Grocy.Api.Post('edit-object/tasks/' + Grocy.EditObjectId, jsonData, function(result) { window.location.href = U('/tasks'); diff --git a/public/viewjs/tasks.js b/public/viewjs/tasks.js index 7315041c..6126d182 100644 --- a/public/viewjs/tasks.js +++ b/public/viewjs/tasks.js @@ -35,9 +35,23 @@ $(document).on('click', '.do-task-button', function(e) var taskName = $(e.currentTarget).attr('data-task-name'); var doneTime = moment().format('YYYY-MM-DD HH:mm:ss'); - Grocy.Api.Get('tasks/mark-task-done/' + taskId + '?tracked_time=' + doneTime, + Grocy.Api.Get('tasks/mark-task-as-completed/' + taskId + '?done_time=' + doneTime, function() { + if (!$("#show-done-tasks").is(":checked")) + { + $('#task-' + taskId + '-row').fadeOut(500, function () + { + $(this).remove(); + }); + } + else + { + $('#task-' + taskId + '-row').addClass("text-muted"); + $('#task-' + taskId + '-name').addClass("text-strike-through"); + $('.do-task-button[data-task-id="' + taskId + '"]').addClass("disabled"); + } + toastr.success(L('Marked task #1 as completed on #2', taskName, doneTime)); RefreshContextualTimeago(); RefreshStatistics(); @@ -88,6 +102,23 @@ $(document).on('click', '.delete-task-button', function (e) }); }); +$("#show-done-tasks").change(function() +{ + if (this.checked) + { + window.location.href = U('/tasks?include_done'); + } + else + { + window.location.href = U('/tasks'); + } +}); + +if (GetUriParam('include_done')) +{ + $("#show-done-tasks").prop('checked', true); +} + function RefreshStatistics() { var nextXDays = $("#info-due-tasks").data("next-x-days"); diff --git a/routes.php b/routes.php index d6636b84..3e88129b 100644 --- a/routes.php +++ b/routes.php @@ -111,6 +111,7 @@ $app->group('/api', function() // Tasks $this->get('/tasks/get-current', '\Grocy\Controllers\TasksApiController:Current'); + $this->get('/tasks/mark-task-as-completed/{taskId}', '\Grocy\Controllers\TasksApiController:MarkTaskAsCompleted'); })->add(new ApiKeyAuthMiddleware($appContainer, $appContainer->LoginControllerInstance->GetSessionCookieName(), $appContainer->ApiKeyHeaderName)) ->add(JsonMiddleware::class) ->add(new CorsMiddleware([ diff --git a/services/DemoDataGeneratorService.php b/services/DemoDataGeneratorService.php index e8bffbde..59328f18 100644 --- a/services/DemoDataGeneratorService.php +++ b/services/DemoDataGeneratorService.php @@ -91,12 +91,12 @@ class DemoDataGeneratorService extends BaseService INSERT INTO task_categories (name) VALUES ('{$localizationService->Localize('Life')}'); --2 INSERT INTO task_categories (name) VALUES ('{$localizationService->Localize('Projects')}'); --3 - INSERT INTO tasks (name, category_id, due_date) VALUES ('{$localizationService->Localize('Repair the garage door')}', 1, date(datetime('now', 'localtime'), '+14 day')); - INSERT INTO tasks (name, category_id, due_date) VALUES ('{$localizationService->Localize('Task2')}', 1, date(datetime('now', 'localtime'), '+14 day')); - INSERT INTO tasks (name, category_id, due_date) VALUES ('{$localizationService->Localize('Task3')}', 2, date(datetime('now', 'localtime'), '-1 day')); - INSERT INTO tasks (name, category_id, due_date) VALUES ('{$localizationService->Localize('Task4')}', 2, date(datetime('now', 'localtime'), '-1 day')); - INSERT INTO tasks (name, due_date) VALUES ('{$localizationService->Localize('Task5')}', date(datetime('now', 'localtime'), '+3 day')); - INSERT INTO tasks (name, due_date) VALUES ('{$localizationService->Localize('Task6')}', date(datetime('now', 'localtime'), '+4 day')); + INSERT INTO tasks (name, category_id, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Repair the garage door')}', 1, date(datetime('now', 'localtime'), '+14 day'), 1); + INSERT INTO tasks (name, category_id, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Fork and improve grocy')}', 3, date(datetime('now', 'localtime'), '+30 day'), 1); + INSERT INTO tasks (name, category_id, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Task')}1', 2, date(datetime('now', 'localtime'), '-1 day'), 1); + INSERT INTO tasks (name, category_id, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Task')}2', 2, date(datetime('now', 'localtime'), '-1 day'), 1); + INSERT INTO tasks (name, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Find a solution for what to do when I forget the door keys')}', date(datetime('now', 'localtime'), '+3 day'), 1); + INSERT INTO tasks (name, due_date, assigned_to_user_id) VALUES ('{$localizationService->Localize('Task')}3', date(datetime('now', 'localtime'), '+4 day'), 1); INSERT INTO migrations (migration) VALUES (-1); "; diff --git a/services/TasksService.php b/services/TasksService.php index 070f8a49..a9262fdd 100644 --- a/services/TasksService.php +++ b/services/TasksService.php @@ -10,6 +10,22 @@ class TasksService extends BaseService return $this->DatabaseService->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ); } + public function MarkTaskAsCompleted($taskId, $doneTime) + { + if (!$this->TaskExists($taskId)) + { + throw new \Exception('Task does not exist'); + } + + $taskRow = $this->Database->tasks()->where('id = :1', $taskId)->fetch(); + $taskRow->update(array( + 'done' => 1, + 'done_timestamp' => $doneTime + )); + + return true; + } + private function TaskExists($taskId) { $taskRow = $this->Database->tasks()->where('id = :1', $taskId)->fetch(); diff --git a/views/components/datetimepicker.blade.php b/views/components/datetimepicker.blade.php index e5308d2d..c1df105e 100644 --- a/views/components/datetimepicker.blade.php +++ b/views/components/datetimepicker.blade.php @@ -3,6 +3,7 @@ @endpush @php if(!isset($isRequired)) { $isRequired = true; } @endphp +@php if(!isset($initialValue)) { $initialValue = ''; } @endphp
@@ -11,6 +12,7 @@ diff --git a/views/components/userpicker.blade.php b/views/components/userpicker.blade.php index ca5a9ed5..2a1ad9e7 100644 --- a/views/components/userpicker.blade.php +++ b/views/components/userpicker.blade.php @@ -4,6 +4,7 @@ @php if(empty($prefillByUsername)) { $prefillByUsername = ''; } @endphp @php if(empty($prefillByUserId)) { $prefillByUserId = ''; } @endphp +@php if(!isset($nextInputSelector)) { $nextInputSelector = ''; } @endphp
diff --git a/views/taskform.blade.php b/views/taskform.blade.php index e1c019df..fd507e66 100644 --- a/views/taskform.blade.php +++ b/views/taskform.blade.php @@ -48,7 +48,7 @@ 'limitEndToNow' => false, 'limitStartToNow' => false, 'invalidFeedback' => $L('A due date is required'), - 'nextInputSelector' => '', + 'nextInputSelector' => 'category_id', 'additionalCssClasses' => 'date-only-datetimepicker', 'isRequired' => false )) @@ -63,6 +63,19 @@
+ @php + $initUserId = GROCY_USER_ID; + if ($mode == 'edit') + { + $initUserId = $task->assigned_to_user_id; + } + @endphp + @include('components.userpicker', array( + 'label' => 'Assigned to', + 'users' => $users, + 'prefillByUserId' => $initUserId + )) + diff --git a/views/tasks.blade.php b/views/tasks.blade.php index 6cbcb83f..7bad8d06 100644 --- a/views/tasks.blade.php +++ b/views/tasks.blade.php @@ -33,6 +33,14 @@
+
+
+ + +
+
@@ -41,16 +49,17 @@ # - {{ $L('Aufgabe') }} + {{ $L('Task') }} {{ $L('Due') }} Hidden category + {{ $L('Assigned to') }} @foreach($tasks as $task) - + - name) }}" + name) }}" data-task-id="{{ $task->id }}" data-task-name="{{ $task->name }}"> @@ -64,7 +73,7 @@ - + {{ $task->name }} @@ -72,7 +81,10 @@ - @if($task->category_id !== null) {{ FindObjectInArrayByPropertyValue($taskCategories, 'id', $task->category_id)->name }} @else {{ $L('Uncategorized') }}@endif + @if($task->category_id != null) {{ FindObjectInArrayByPropertyValue($taskCategories, 'id', $task->category_id)->name }} @else {{ $L('Uncategorized') }}@endif + + + @if($task->assigned_to_user_id != null) {{ GetUserDisplayName(FindObjectInArrayByPropertyValue($users, 'id', $task->assigned_to_user_id)) }} @endif @endforeach