Added possibility to track who did a habit (this implements and closes #21)

This commit is contained in:
Bernd Bestel 2018-07-24 20:45:14 +02:00
parent bcbdf58376
commit 249b01d7a8
No known key found for this signature in database
GPG Key ID: 71BD34C0D4891300
16 changed files with 139 additions and 14 deletions

View File

@ -9,6 +9,7 @@ use \Grocy\Controllers\LoginController;
if (file_exists(__DIR__ . '/embedded.txt')) if (file_exists(__DIR__ . '/embedded.txt'))
{ {
define('GROCY_DATAPATH', file_get_contents(__DIR__ . '/embedded.txt')); define('GROCY_DATAPATH', file_get_contents(__DIR__ . '/embedded.txt'));
define('GROCY_USER_ID', 1);
} }
else else
{ {

View File

@ -22,9 +22,15 @@ class HabitsApiController extends BaseApiController
$trackedTime = $request->getQueryParams()['tracked_time']; $trackedTime = $request->getQueryParams()['tracked_time'];
} }
$doneBy = GROCY_USER_ID;
if (isset($request->getQueryParams()['done_by']) && !empty($request->getQueryParams()['done_by']))
{
$doneBy = $request->getQueryParams()['done_by'];
}
try try
{ {
$this->HabitsService->TrackHabit($args['habitId'], $trackedTime); $this->HabitsService->TrackHabit($args['habitId'], $trackedTime, $doneBy);
return $this->VoidApiActionResponse($response); return $this->VoidApiActionResponse($response);
} }
catch (\Exception $ex) catch (\Exception $ex)

View File

@ -38,7 +38,8 @@ class HabitsController extends BaseController
public function TrackHabitExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args) public function TrackHabitExecution(\Slim\Http\Request $request, \Slim\Http\Response $response, array $args)
{ {
return $this->AppContainer->view->render($response, 'habittracking', [ return $this->AppContainer->view->render($response, 'habittracking', [
'habits' => $this->Database->habits()->orderBy('name') 'habits' => $this->Database->habits()->orderBy('name'),
'users' => $this->Database->users()->orderBy('username')
]); ]);
} }

View File

@ -21,7 +21,7 @@ class UsersApiController extends BaseApiController
try try
{ {
$this->UsersService->CreateUser($requestBody['username'], $requestBody['first_name'], $requestBody['last_name'], $requestBody['password']); $this->UsersService->CreateUser($requestBody['username'], $requestBody['first_name'], $requestBody['last_name'], $requestBody['password']);
return $this->ApiResponse(array('success' => $success)); return $this->ApiResponse(array('success' => true));
} }
catch (\Exception $ex) catch (\Exception $ex)
{ {
@ -33,7 +33,7 @@ class UsersApiController extends BaseApiController
{ {
try try
{ {
$this->UsersService->DeleteUser($args['userId']); $success = $this->UsersService->DeleteUser($args['userId']);
return $this->ApiResponse(array('success' => $success)); return $this->ApiResponse(array('success' => $success));
} }
catch (\Exception $ex) catch (\Exception $ex)
@ -49,7 +49,7 @@ class UsersApiController extends BaseApiController
try try
{ {
$this->UsersService->EditUser($args['userId'], $requestBody['username'], $requestBody['first_name'], $requestBody['last_name'], $requestBody['password']); $this->UsersService->EditUser($args['userId'], $requestBody['username'], $requestBody['first_name'], $requestBody['last_name'], $requestBody['password']);
return $this->ApiResponse(array('success' => $success)); return $this->ApiResponse(array('success' => true));
} }
catch (\Exception $ex) catch (\Exception $ex)
{ {

View File

@ -144,3 +144,27 @@ function Setting(string $name, $value)
} }
} }
} }
function GetUserDisplayName($user)
{
$displayName = '';
if (empty($user->first_name) && !empty($user->last_name))
{
$displayName = $user->last_name;
}
elseif (empty($user->last_name) && !empty($user->first_name))
{
$displayName = $user->first_name;
}
elseif (!empty($user->last_name) && !empty($user->first_name))
{
$displayName = $user->first_name . ' ' . $user->last_name;
}
else
{
$displayName = $user->username;
}
return $displayName;
}

View File

@ -180,6 +180,7 @@ return array(
'Confirm password' => 'Passwort bestätigen', 'Confirm password' => 'Passwort bestätigen',
'Passwords do not match' => 'Passwörter stimmen nicht überein', 'Passwords do not match' => 'Passwörter stimmen nicht überein',
'Change password' => 'Passwort ändern', 'Change password' => 'Passwort ändern',
'Done by' => 'Ausgeführt von',
//Constants //Constants
'manually' => 'Manuell', 'manually' => 'Manuell',

2
migrations/0028.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE habits_log
ADD done_by_user_id

View File

@ -1,6 +1,6 @@
Grocy.Components.ProductPicker = { }; Grocy.Components.ProductPicker = { };
Grocy.Components.ProductPicker.GetPicker = function () Grocy.Components.ProductPicker.GetPicker = function()
{ {
return $('#product_id'); return $('#product_id');
} }
@ -43,7 +43,7 @@ Grocy.Components.ProductPicker.HideCustomError = function()
$("#custom-productpicker-error").addClass("d-none"); $("#custom-productpicker-error").addClass("d-none");
} }
$('.combobox').combobox({ $('.product-combobox').combobox({
appendId: '_text_input', appendId: '_text_input',
bsVersion: '4' bsVersion: '4'
}); });

View File

@ -0,0 +1,62 @@
Grocy.Components.UserPicker = { };
Grocy.Components.UserPicker.GetPicker = function()
{
return $('#user_id');
}
Grocy.Components.UserPicker.GetInputElement = function()
{
return $('#user_id_text_input');
}
Grocy.Components.UserPicker.GetValue = function()
{
return $('#user_id').val();
}
Grocy.Components.UserPicker.SetValue = function(value)
{
Grocy.Components.UserPicker.GetInputElement().val(value);
Grocy.Components.UserPicker.GetInputElement().trigger('change');
}
$('.user-combobox').combobox({
appendId: '_text_input',
bsVersion: '4'
});
var prefillUser = Grocy.Components.UserPicker.GetPicker().parent().data('prefill-by-username').toString();
if (typeof prefillUser !== "undefined")
{
var possibleOptionElement = $("#user_id option[data-additional-searchdata*='" + prefillUser + "']").first();
if (possibleOptionElement.length === 0)
{
possibleOptionElement = $("#user_id option:contains('" + prefillUser + "')").first();
}
if (possibleOptionElement.length > 0)
{
$('#user_id').val(possibleOptionElement.val());
$('#user_id').data('combobox').refresh();
$('#user_id').trigger('change');
var nextInputElement = $(Grocy.Components.UserPicker.GetPicker().parent().data('next-input-selector').toString());
nextInputElement.focus();
}
}
var prefillUserId = Grocy.Components.UserPicker.GetPicker().parent().data('prefill-by-user-id').toString();
if (typeof prefillUserId !== "undefined")
{
var possibleOptionElement = $("#user_id option[value='" + prefillUserId + "']").first();
if (possibleOptionElement.length > 0)
{
$('#user_id').val(possibleOptionElement.val());
$('#user_id').data('combobox').refresh();
$('#user_id').trigger('change');
var nextInputElement = $(Grocy.Components.UserPicker.GetPicker().parent().data('next-input-selector').toString());
nextInputElement.focus();
}
}

View File

@ -7,7 +7,7 @@
Grocy.Api.Get('habits/get-habit-details/' + jsonForm.habit_id, Grocy.Api.Get('habits/get-habit-details/' + jsonForm.habit_id,
function (habitDetails) function (habitDetails)
{ {
Grocy.Api.Get('habits/track-habit-execution/' + jsonForm.habit_id + '?tracked_time=' + Grocy.Components.DateTimePicker.GetValue(), Grocy.Api.Get('habits/track-habit-execution/' + jsonForm.habit_id + '?tracked_time=' + Grocy.Components.DateTimePicker.GetValue() + "&done_by=" + Grocy.Components.UserPicker.GetValue(),
function(result) function(result)
{ {
toastr.success(L('Tracked execution of habit #1 on #2', habitDetails.habit.name, Grocy.Components.DateTimePicker.GetValue())); toastr.success(L('Tracked execution of habit #1 on #2', habitDetails.habit.name, Grocy.Components.DateTimePicker.GetValue()));

View File

@ -52,16 +52,23 @@ class HabitsService extends BaseService
); );
} }
public function TrackHabit(int $habitId, string $trackedTime) public function TrackHabit(int $habitId, string $trackedTime, $doneBy = GROCY_USER_ID)
{ {
if (!$this->HabitExists($habitId)) if (!$this->HabitExists($habitId))
{ {
throw new \Exception('Habit does not exist'); throw new \Exception('Habit does not exist');
} }
$userRow = $this->Database->users()->where('id = :1', $doneBy)->fetch();
if ($userRow === null)
{
throw new \Exception('User does not exist');
}
$logRow = $this->Database->habits_log()->createRow(array( $logRow = $this->Database->habits_log()->createRow(array(
'habit_id' => $habitId, 'habit_id' => $habitId,
'tracked_time' => $trackedTime 'tracked_time' => $trackedTime,
'done_by_user_id' => $doneBy
)); ));
$logRow->save(); $logRow->save();

View File

@ -7,7 +7,7 @@
<div class="form-group" data-next-input-selector="{{ $nextInputSelector }}" data-disallow-add-product-workflows="{{ BoolToString($disallowAddProductWorkflows) }}" data-prefill-by-name="{{ $prefillByName }}"> <div class="form-group" data-next-input-selector="{{ $nextInputSelector }}" data-disallow-add-product-workflows="{{ BoolToString($disallowAddProductWorkflows) }}" data-prefill-by-name="{{ $prefillByName }}">
<label for="product_id">{{ $L('Product') }} <i class="fas fa-barcode"></i><span id="barcode-lookup-disabled-hint" class="small text-muted d-none"> {{ $L('Barcode lookup is disabled') }}</span></label> <label for="product_id">{{ $L('Product') }} <i class="fas fa-barcode"></i><span id="barcode-lookup-disabled-hint" class="small text-muted d-none"> {{ $L('Barcode lookup is disabled') }}</span></label>
<select class="form-control combobox" id="product_id" name="product_id" required> <select class="form-control product-combobox" id="product_id" name="product_id" required>
<option value=""></option> <option value=""></option>
@foreach($products as $product) @foreach($products as $product)
<option data-additional-searchdata="{{ $product->barcode }}" value="{{ $product->id }}">{{ $product->name }}</option> <option data-additional-searchdata="{{ $product->barcode }}" value="{{ $product->id }}">{{ $product->name }}</option>

View File

@ -0,0 +1,16 @@
@push('componentScripts')
<script src="{{ $U('/viewjs/components/userpicker.js', true) }}?v={{ $version }}"></script>
@endpush
@php if(empty($prefillByUsername)) { $prefillByUsername = ''; } @endphp
@php if(empty($prefillByUserId)) { $prefillByUserId = ''; } @endphp
<div class="form-group" data-next-input-selector="{{ $nextInputSelector }}" data-prefill-by-username="{{ $prefillByUsername }}" data-prefill-by-user-id="{{ $prefillByUserId }}">
<label for="user_id">{{ $L($label) }}</label>
<select class="form-control user-combobox" id="user_id" name="user_id">
<option value=""></option>
@foreach($users as $user)
<option data-additional-searchdata="{{ $user->username }}" value="{{ $user->id }}">{{ GetUserDisplayName($user) }}</option>
@endforeach
</select>
</div>

View File

@ -32,6 +32,13 @@
'invalidFeedback' => $L('This can only be before now') 'invalidFeedback' => $L('This can only be before now')
)) ))
@include('components.userpicker', array(
'label' => 'Done by',
'users' => $users,
'nextInputSelector' => '#user_id',
'prefillByUserId' => GROCY_USER_ID
))
<button id="save-habittracking-button" type="submit" class="btn btn-success">{{ $L('OK') }}</button> <button id="save-habittracking-button" type="submit" class="btn btn-success">{{ $L('OK') }}</button>
</form> </form>

View File

@ -180,10 +180,8 @@
<a class="nav-link dropdown-toggle discrete-link" href="#" data-toggle="dropdown"><i class="fas fa-wrench"></i> <span class="d-inline d-lg-none">{{ $L('Settings') }}</span></a> <a class="nav-link dropdown-toggle discrete-link" href="#" data-toggle="dropdown"><i class="fas fa-wrench"></i> <span class="d-inline d-lg-none">{{ $L('Settings') }}</span></a>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
@if($isEmbeddedInstallation === false)
<a class="dropdown-item discrete-link" href="{{ $U('/users') }}"><i class="fas fa-users"></i>&nbsp;{{ $L('Manage users') }}</a> <a class="dropdown-item discrete-link" href="{{ $U('/users') }}"><i class="fas fa-users"></i>&nbsp;{{ $L('Manage users') }}</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
@endif
<a class="dropdown-item discrete-link" href="{{ $U('/manageapikeys') }}"><i class="fas fa-handshake"></i>&nbsp;{{ $L('Manage API keys') }}</a> <a class="dropdown-item discrete-link" href="{{ $U('/manageapikeys') }}"><i class="fas fa-handshake"></i>&nbsp;{{ $L('Manage API keys') }}</a>
<a class="dropdown-item discrete-link" target="_blank" href="{{ $U('/api') }}"><i class="fas fa-book"></i>&nbsp;{{ $L('REST API & data model documentation') }}</a> <a class="dropdown-item discrete-link" target="_blank" href="{{ $U('/api') }}"><i class="fas fa-book"></i>&nbsp;{{ $L('REST API & data model documentation') }}</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>

View File

@ -41,7 +41,7 @@
<a class="btn btn-info btn-sm" href="{{ $U('/user/') }}{{ $user->id }}"> <a class="btn btn-info btn-sm" href="{{ $U('/user/') }}{{ $user->id }}">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
<a class="btn btn-danger btn-sm user-delete-button @if($user->id === GROCY_USER_ID){{ disabled }}@endif" href="#" data-user-id="{{ $user->id }}" data-user-username="{{ $user->username }}"> <a class="btn btn-danger btn-sm user-delete-button @if($user->id == GROCY_USER_ID) disabled @endif" href="#" data-user-id="{{ $user->id }}" data-user-username="{{ $user->username }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
</td> </td>