Implemented basic time recording w/ frontend

This commit is contained in:
Dennis Eichhorn 2019-10-13 20:39:58 +02:00
parent 5840080ee3
commit 4579fae4b2
12 changed files with 367 additions and 79 deletions

View File

@ -18,7 +18,22 @@
"pid": "/humanresource/timerecording",
"type": 3,
"subtype": 1,
"name": "List",
"name": "Today",
"uri": "{/prefix}humanresource/timerecording/dashboard?{?}",
"target": "self",
"icon": null,
"order": 1,
"from": "HumanResourceTimeRecording",
"permission": { "permission": 2, "type": null, "element": null },
"parent": 1006301001,
"children": []
},
{
"id": 1006302002,
"pid": "/humanresource/timerecording",
"type": 3,
"subtype": 1,
"name": "Stats",
"uri": "{/prefix}humanresource/timerecording/dashboard?{?}",
"target": "self",
"icon": null,
@ -36,7 +51,7 @@
"type": 2,
"subtype": 1,
"name": "TimeRecording",
"uri": "{/prefix}humanresource/timerecording/dashboard?{?}",
"uri": "{/prefix}private/timerecording/dashboard?{?}",
"target": "self",
"icon": null,
"order": 1,

View File

@ -22,7 +22,8 @@
"hr_timerecording_session_end": {
"name": "hr_timerecording_session_end",
"type": "DATETIME",
"null": false
"null": true,
"default": null
},
"hr_timerecording_session_busy": {
"name": "hr_timerecording_session_busy",
@ -48,8 +49,8 @@
"primary": true,
"autoincrement": true
},
"hr_timerecording_session_element_type": {
"name": "hr_timerecording_session_element_type",
"hr_timerecording_session_element_status": {
"name": "hr_timerecording_session_element_status",
"type": "TINYINT",
"null": false
},

View File

@ -17,4 +17,15 @@ return [
],
],
],
'^.*/private/timerecording/dashboard.*$' => [
[
'dest' => '\Modules\HumanResourceTimeRecording\Controller\BackendController:viewPrivateDashboard',
'verb' => RouteVerb::GET,
'permission' => [
'module' => BackendController::MODULE_NAME,
'type' => PermissionType::READ,
'state' => PermissionState::PRIVATE_DASHBOARD,
],
],
],
];

View File

@ -18,6 +18,7 @@ use phpOMS\Contract\RenderableInterface;
use phpOMS\Message\RequestAbstract;
use phpOMS\Message\ResponseAbstract;
use phpOMS\Views\View;
use Modules\HumanResourceTimeRecording\Models\SessionMapper;
/**
* TimeRecording controller class.
@ -48,6 +49,33 @@ final class BackendController extends Controller
$view->setTemplate('/Modules/HumanResourceTimeRecording/Theme/Backend/dashboard');
$view->addData('nav', $this->app->moduleManager->get('Navigation')->createNavigationMid(1006301001, $request, $response));
$list = SessionMapper::getLastSessionsForDate(new \DateTime('now'));
$view->addData('sessions', $list);
return $view;
}
/**
* Routing end-point for application behaviour.
*
* @param RequestAbstract $request Request
* @param ResponseAbstract $response Response
* @param mixed $data Generic data
*
* @return RenderableInterface
*
* @since 1.0.0
* @codeCoverageIgnore
*/
public function viewPrivateDashboard(RequestAbstract $request, ResponseAbstract $response, $data = null) : RenderableInterface
{
$view = new View($this->app, $request, $response);
$view->setTemplate('/Modules/HumanResourceTimeRecording/Theme/Backend/private-dashboard');
$view->addData('nav', $this->app->moduleManager->get('Navigation')->createNavigationMid(1006303001, $request, $response));
$list = SessionMapper::getNewest(50);
$view->addData('sessions', $list);
return $view;
}
}

View File

@ -26,5 +26,6 @@ use phpOMS\Stdlib\Base\Enum;
*/
abstract class PermissionState extends Enum
{
public const DASHBOARD = 1;
public const DASHBOARD = 1;
public const PRIVATE_DASHBOARD = 2;
}

View File

@ -85,11 +85,14 @@ class Session implements ArrayableInterface, \JsonSerializable
/**
* Constructor.
*
* @param int|Employee $employee Employee
*
* @since 1.0.0
*/
public function __construct()
public function __construct($employee = 0)
{
$this->start = new \DateTime('now');
$this->start = new \DateTime('now');
$this->employee = $employee;
}
/**
@ -104,6 +107,18 @@ class Session implements ArrayableInterface, \JsonSerializable
return $this->id;
}
/**
* Get employee.
*
* @return int|Employee
*
* @since 1.0.0
*/
public function getEmployee()
{
return $this->employee;
}
/**
* Add a session element to the session
*
@ -115,9 +130,75 @@ class Session implements ArrayableInterface, \JsonSerializable
*/
public function addSessionElement($element) : void
{
$this->sessionElement[] = $element;
if ($element->getStatus() === ClockingStatus::START) {
// todo: prevent multiple starts and ends per session?
// todo: if quit element or pause element re-calculate busy time!
$this->start = $element->getDatetime();
}
if ($element->getStatus() === ClockingStatus::END) {
// todo: prevent multiple starts and ends per session?
$this->end = $element->getDatetime();
}
$this->sessionElements[] = $element;
\usort($this->sessionElements, function($a, $b) {
return $a->getDatetime()->getTimestamp() <=> $b->getDatetime()->getTimestamp();
});
$busyTime = 0;
$lastStart = $this->start;
foreach ($this->sessionElements as $e) {
if ($e->getStatus() === ClockingStatus::START) {
continue;
}
if ($e->getStatus() === ClockingStatus::PAUSE || $e->getStatus() === ClockingStatus::END) {
$busyTime += $e->getDatetime()->getTimestamp() - $lastStart->getTimestamp();
}
if ($e->getStatus() === ClockingStatus::CONTINUE) {
$lastStart = $e->getDatetime();
}
}
$this->busy = $busyTime;
}
/**
* Get the total break time of a session
*
* @return int
*
* @since 1.0.0
*/
public function getBreak() : int
{
\usort($this->sessionElements, function($a, $b) {
return $a->getDatetime()->getTimestamp() <=> $b->getDatetime()->getTimestamp();
});
$breakTime = 0;
$lastBreak = $this->start;
foreach ($this->sessionElements as $element) {
if ($element->getStatus() === ClockingStatus::START) {
continue;
}
if ($element->getStatus() === ClockingStatus::PAUSE || $element->getStatus() === ClockingStatus::END) {
$lastBreak = $element->getDatetime();
}
if ($element->getStatus() === ClockingStatus::CONTINUE) {
$breakTime += $element->getDatetime()->getTimestamp() - ($lastBreak->getTimestamp() ?? 0);
}
}
return $breakTime;
}
/**

View File

@ -35,12 +35,12 @@ class SessionElement implements ArrayableInterface, \JsonSerializable
private int $id = 0;
/**
* Session element type.
* Session element status.
*
* @var int
* @since 1.0.0
*/
private int $type = ClockingStatus::START;
private int $status = ClockingStatus::START;
/**
* DateTime
@ -53,10 +53,10 @@ class SessionElement implements ArrayableInterface, \JsonSerializable
/**
* Session id this element belongs to
*
* @var int
* @var int|Session
* @since 1.0.0
*/
private int $session = 0;
private $session = 0;
/**
* Constructor.
@ -66,7 +66,7 @@ class SessionElement implements ArrayableInterface, \JsonSerializable
*
* @since 1.0.0
*/
public function __construct(int $session = 0, \DateTime $dt = null)
public function __construct($session = 0, \DateTime $dt = null)
{
$this->session = $session;
$this->dt = $dt ?? new \DateTime('now');
@ -97,29 +97,41 @@ class SessionElement implements ArrayableInterface, \JsonSerializable
}
/**
* Get the session element type
* Get the session element status
*
* @return int
*
* @since 1.0.0
*/
public function getType() : int
public function getStatus() : int
{
return $this->type;
return $this->status;
}
/**
* Set the session element type
* Set the session element status
*
* @param int $type Session element type
* @param int $status Session element status
*
* @return void
*
* @since 1.0.0
*/
public function setType(int $type) : void
public function setStatus(int $status) : void
{
$this->type = $type;
$this->status = $status;
}
/**
* Get session this element is for
*
* @return int|Session
*
* @since 1.0.0
*/
public function getSession()
{
return $this->session;
}
/**
@ -129,7 +141,7 @@ class SessionElement implements ArrayableInterface, \JsonSerializable
{
return [
'id' => $this->id,
'type' => $this->type,
'status' => $this->status,
'dt' => $this->dt->format('Y-m-d H:i:s'),
'sesseion' => $this->session,
];

View File

@ -35,11 +35,24 @@ final class SessionElementMapper extends DataMapperAbstract
*/
protected static array $columns = [
'hr_timerecording_session_element_id' => ['name' => 'hr_timerecording_session_element_id', 'type' => 'int', 'internal' => 'id'],
'hr_timerecording_session_element_type' => ['name' => 'hr_timerecording_session_element_type', 'type' => 'int', 'internal' => 'type'],
'hr_timerecording_session_element_status' => ['name' => 'hr_timerecording_session_element_status', 'type' => 'int', 'internal' => 'status'],
'hr_timerecording_session_element_dt' => ['name' => 'hr_timerecording_session_element_dt', 'type' => 'DateTime', 'internal' => 'dt'],
'hr_timerecording_session_element_session' => ['name' => 'hr_timerecording_session_element_session', 'type' => 'int', 'internal' => 'session'],
];
/**
* Belongs to.
*
* @var array<string, array<string, string>>
* @since 1.0.0
*/
protected static array $belongsTo = [
'session' => [
'mapper' => SessionMapper::class,
'src' => 'hr_timerecording_session_element_session',
],
];
/**
* Primary table.
*

View File

@ -16,6 +16,8 @@ namespace Modules\HumanResourceTimeRecording\Models;
use Modules\HumanResourceManagement\Models\EmployeeMapper;
use phpOMS\DataStorage\Database\DataMapperAbstract;
use phpOMS\DataStorage\Database\Query\Builder;
use phpOMS\DataStorage\Database\RelationType;
/**
* Mapper class.
@ -86,4 +88,41 @@ final class SessionMapper extends DataMapperAbstract
* @since 1.0.0
*/
protected static string $primaryField = 'hr_timerecording_session_id';
/**
* Created at column
*
* @var string
* @since 1.0.0
*/
protected static string $createdAt = 'hr_timerecording_session_start';
/**
* Get last sessions from all employees
*
* @return array
*
* @todo: consider selecting only active employees
* @todo: consider using a datetime to limit the results to look for
*
* @since 1.0.0
*/
public static function getLastSessionsForDate(\DateTime $dt = null) : array
{
$join = new Builder(self::$db);
$join->prefix(self::$db->getPrefix())
->select(self::$table . '.hr_timerecording_session_employee')
->selectAs('MAX(hr_timerecording_session_start)', 'maxDate')
->from(self::$table)
->groupBy(self::$table . '.hr_timerecording_session_employee');
$query = new Builder(self::$db);
$query->prefix(self::$db->getPrefix())
->select('*')->fromAs(self::$table, 't')
->innerJoin($join, 'tm')
->on('t.hr_timerecording_session_employee', '=', 'tm.hr_timerecording_session_employee')
->andOn('t.hr_timerecording_session_start', '=', 'tm.maxDate');
return self::getAllByQuery($query, RelationType::ALL, 6);
}
}

View File

@ -24,6 +24,13 @@ return ['HumanResourceTimeRecording' => [
'CT3' => 'Vacation',
'CT4' => 'Sick',
'CT5' => 'On the move',
'D0' => 'Sunday',
'D1' => 'Monday',
'D2' => 'Tuesday',
'D3' => 'Wednesday',
'D4' => 'Thursday',
'D5' => 'Friday',
'D6' => 'Saturday',
'Date' => 'Date',
'End' => 'End',
'Recordings' => 'Recordings',

View File

@ -16,60 +16,20 @@ declare(strict_types=1);
use \Modules\HumanResourceTimeRecording\Models\ClockingType;
use \Modules\HumanResourceTimeRecording\Models\ClockingStatus;
$sessions = $this->getData('sessions');
echo $this->getData('nav')->render(); ?>
<div class="row">
<div class="col-md-4 col-xs-12">
<section class="box wf-100">
<div class="inner">
<form id="clocking" method="PUT" action="<?= \phpOMS\Uri\UriFactory::build('{/api}task/element?{?}&csrf={$CSRF}'); ?>">
<table class="layout wf-100" style="table-layout: fixed">
<tr><td><label for="iType"><?= $this->getHtml('Type') ?></label>
<tr><td>
<select id="iType" name="Type">
<option value="<?= ClockingType::OFFICE; ?>"><?= $this->getHtml('CT0') ?>
<option value="<?= ClockingType::REMOTE; ?>"><?= $this->getHtml('CT1') ?>
<option value="<?= ClockingType::HOME; ?>"><?= $this->getHtml('CT2') ?>
<option value="<?= ClockingType::VACATION; ?>"><?= $this->getHtml('CT3') ?>
<option value="<?= ClockingType::SICK; ?>"><?= $this->getHtml('CT4') ?>
<option value="<?= ClockingType::ON_THE_MOVE; ?>"><?= $this->getHtml('CT5') ?>
</select>
<tr><td><label for="iStatus"><?= $this->getHtml('Status') ?></label>
<tr><td>
<select id="iStatus" name="Status">
<option value="<?= ClockingStatus::START; ?>"><?= $this->getHtml('CS0') ?>
<option value="<?= ClockingStatus::PAUSE; ?>"><?= $this->getHtml('CS1') ?>
<option value="<?= ClockingStatus::CONTINUE; ?>"><?= $this->getHtml('CS2') ?>
<option value="<?= ClockingStatus::END; ?>"><?= $this->getHtml('CS3') ?>
</select>
<tr><td>
<input type="submit" id="iclockingButton" name="clockingButton" value="<?= $this->getHtml('Submit', '0', '0'); ?>">
</table>
</form>
</div>
</section>
</div>
<div class="col-md-4 col-xs-12">
<section class="box wf-100">
<header><h1>Vaction</h1></header>
<div class="inner">
<table>
<tr><td>Used Vacation<td>
<tr><td>Last Vacation<td>
<tr><td>Next Vacation<td>
</table>
</div>
</section>
</div>
<div class="col-md-4 col-xs-12">
<div class="col-xs-12">
<div class="box wf-100">
<table id="accountList" class="default">
<caption><?= $this->getHtml('Recordings') ?><i class="fa fa-download floatRight download btn"></i></caption>
<thead>
<tr>
<td><?= $this->getHtml('Date'); ?>
<td>Status
<td>Employee
<td><?= $this->getHtml('Start') ?>
<td><?= $this->getHtml('Break') ?>
<td><?= $this->getHtml('End') ?>
@ -77,17 +37,17 @@ echo $this->getData('nav')->render(); ?>
<tfoot>
<tr><td colspan="5">
<tbody>
<?php foreach ($sessions as $session) : ?>
<tr>
<td><?= $session->getStart()->format('Y-m-d'); ?>
<td><span class="tag">Status Here</span>
<td><?= $session->getEmployee()->getProfile()->getAccount()->getName1(); ?>, <?= $session->getEmployee()->getProfile()->getAccount()->getName2(); ?>
<td><?= $session->getStart()->format('H:i:s'); ?>
<td><?= (int) ($session->getBreak() / 3600); ?>h <?= ((int) ($session->getBreak() / 60) % 60); ?>m
<td><?= $session->getEnd() !== null ? $session->getEnd()->format('H:i') : ''; ?>
<td><?= (int) ($session->getBusy() / 3600); ?>h <?= ((int) ($session->getBusy() / 60) % 60); ?>m
<?php endforeach; ?>
</table>
</div>
</div>
</div>
on response successfull reload!!! or!
on response successfull change ui/change ui and on response not successfull undo changed ui both result in the same
list for month show total of total
list contains segments for week show total of total
every week contains every day show total of total
if you click on a day you get detailed information of that day
show additional section with vacation days

View File

@ -0,0 +1,120 @@
<?php
/**
* Orange Management
*
* PHP Version 7.4
*
* @package HumanResourceTimeRecording
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://orange-management.org
*/
declare(strict_types=1);
use \Modules\HumanResourceTimeRecording\Models\ClockingType;
use \Modules\HumanResourceTimeRecording\Models\ClockingStatus;
$sessions = $this->getData('sessions');
echo $this->getData('nav')->render(); ?>
<div class="row">
<div class="col-md-4 col-xs-12">
<section class="box wf-100">
<div class="inner">
<form id="clocking" method="PUT" action="<?= \phpOMS\Uri\UriFactory::build('{/api}task/element?{?}&csrf={$CSRF}'); ?>">
<table class="layout wf-100" style="table-layout: fixed">
<tr><td><label for="iType"><?= $this->getHtml('Type') ?></label>
<tr><td>
<select id="iType" name="Type">
<option value="<?= ClockingType::OFFICE; ?>"><?= $this->getHtml('CT0') ?>
<option value="<?= ClockingType::REMOTE; ?>"><?= $this->getHtml('CT1') ?>
<option value="<?= ClockingType::HOME; ?>"><?= $this->getHtml('CT2') ?>
<option value="<?= ClockingType::VACATION; ?>"><?= $this->getHtml('CT3') ?>
<option value="<?= ClockingType::SICK; ?>"><?= $this->getHtml('CT4') ?>
<option value="<?= ClockingType::ON_THE_MOVE; ?>"><?= $this->getHtml('CT5') ?>
</select>
<tr><td><label for="iStatus"><?= $this->getHtml('Status') ?></label>
<tr><td>
<select id="iStatus" name="Status">
<option value="<?= ClockingStatus::START; ?>"><?= $this->getHtml('CS0') ?>
<option value="<?= ClockingStatus::PAUSE; ?>"><?= $this->getHtml('CS1') ?>
<option value="<?= ClockingStatus::CONTINUE; ?>"><?= $this->getHtml('CS2') ?>
<option value="<?= ClockingStatus::END; ?>"><?= $this->getHtml('CS3') ?>
</select>
<tr><td>
<input type="submit" id="iclockingButton" name="clockingButton" value="<?= $this->getHtml('Submit', '0', '0'); ?>">
</table>
</form>
</div>
</section>
</div>
<div class="col-md-4 col-xs-12">
<section class="box wf-100">
<header><h1>Work</h1></header>
<div class="inner">
<table>
<tr><td>This month<td>
<tr><td>Last month<td>
<tr><td>This year<td>
</table>
</div>
</section>
</div>
<div class="col-md-4 col-xs-12">
<section class="box wf-100">
<header><h1>Vaction</h1></header>
<div class="inner">
<table>
<tr><td>Used Vacation<td>
<tr><td>Last Vacation<td>
<tr><td>Next Vacation<td>
</table>
</div>
</section>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="box wf-100">
<table id="accountList" class="default">
<caption><?= $this->getHtml('Recordings') ?><i class="fa fa-download floatRight download btn"></i></caption>
<thead>
<tr>
<td><?= $this->getHtml('Date'); ?>
<td>Status
<td><?= $this->getHtml('Start') ?>
<td><?= $this->getHtml('Break') ?>
<td><?= $this->getHtml('End') ?>
<td><?= $this->getHtml('Total') ?>
<tfoot>
<tr><td colspan="5">
<tbody>
<?php foreach ($sessions as $session) : ?>
<tr>
<td><?= $session->getStart()->format('Y-m-d'); ?> - <?= $this->getHtml('D' . $session->getStart()->format('w')); ?>
<td><span class="tag">Status Here</span>
<td><?= $session->getStart()->format('H:i'); ?>
<td><?= (int) ($session->getBreak() / 3600); ?>h <?= ((int) ($session->getBreak() / 60) % 60); ?>m
<td><?= $session->getEnd() !== null ? $session->getEnd()->format('H:i') : ''; ?>
<td><?= (int) ($session->getBusy() / 3600); ?>h <?= ((int) ($session->getBusy() / 60) % 60); ?>m
<?php endforeach; ?>
</table>
</div>
</div>
</div>
on response successfull reload!!! or!
on response successfull change ui/change ui and on response not successfull undo changed ui both result in the same
list for month show total of total
list contains segments for week show total of total
every week contains every day show total of total
if you click on a day you get detailed information of that day
show additional section with vacation days