From 4579fae4b23174012b4dd63f7b2d0a27f57b17cc Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Sun, 13 Oct 2019 20:39:58 +0200 Subject: [PATCH] Implemented basic time recording w/ frontend --- Admin/Install/Navigation.install.json | 19 +++- Admin/Install/db.json | 7 +- Admin/Routes/Web/Backend.php | 11 +++ Controller/BackendController.php | 28 ++++++ Models/PermissionState.php | 3 +- Models/Session.php | 89 +++++++++++++++++- Models/SessionElement.php | 38 +++++--- Models/SessionElementMapper.php | 15 ++- Models/SessionMapper.php | 39 ++++++++ Theme/Backend/Lang/en.lang.php | 7 ++ Theme/Backend/dashboard.tpl.php | 70 +++----------- Theme/Backend/private-dashboard.tpl.php | 120 ++++++++++++++++++++++++ 12 files changed, 367 insertions(+), 79 deletions(-) create mode 100644 Theme/Backend/private-dashboard.tpl.php diff --git a/Admin/Install/Navigation.install.json b/Admin/Install/Navigation.install.json index 18420db..00913bb 100644 --- a/Admin/Install/Navigation.install.json +++ b/Admin/Install/Navigation.install.json @@ -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, diff --git a/Admin/Install/db.json b/Admin/Install/db.json index 023babb..91d4623 100644 --- a/Admin/Install/db.json +++ b/Admin/Install/db.json @@ -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 }, diff --git a/Admin/Routes/Web/Backend.php b/Admin/Routes/Web/Backend.php index da4056f..0439129 100644 --- a/Admin/Routes/Web/Backend.php +++ b/Admin/Routes/Web/Backend.php @@ -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, + ], + ], + ], ]; diff --git a/Controller/BackendController.php b/Controller/BackendController.php index 2ef3de4..0e95c2d 100644 --- a/Controller/BackendController.php +++ b/Controller/BackendController.php @@ -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; } } diff --git a/Models/PermissionState.php b/Models/PermissionState.php index 2130942..5e031ea 100644 --- a/Models/PermissionState.php +++ b/Models/PermissionState.php @@ -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; } diff --git a/Models/Session.php b/Models/Session.php index ae31d1b..ad9ee6f 100644 --- a/Models/Session.php +++ b/Models/Session.php @@ -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; } /** diff --git a/Models/SessionElement.php b/Models/SessionElement.php index 966a051..e7a4739 100644 --- a/Models/SessionElement.php +++ b/Models/SessionElement.php @@ -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, ]; diff --git a/Models/SessionElementMapper.php b/Models/SessionElementMapper.php index 6133481..a38564b 100644 --- a/Models/SessionElementMapper.php +++ b/Models/SessionElementMapper.php @@ -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> + * @since 1.0.0 + */ + protected static array $belongsTo = [ + 'session' => [ + 'mapper' => SessionMapper::class, + 'src' => 'hr_timerecording_session_element_session', + ], + ]; + /** * Primary table. * diff --git a/Models/SessionMapper.php b/Models/SessionMapper.php index a7c59f6..71f309e 100644 --- a/Models/SessionMapper.php +++ b/Models/SessionMapper.php @@ -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); + } } diff --git a/Theme/Backend/Lang/en.lang.php b/Theme/Backend/Lang/en.lang.php index 65f5b63..fb00b86 100644 --- a/Theme/Backend/Lang/en.lang.php +++ b/Theme/Backend/Lang/en.lang.php @@ -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', diff --git a/Theme/Backend/dashboard.tpl.php b/Theme/Backend/dashboard.tpl.php index ec0a487..84b4214 100644 --- a/Theme/Backend/dashboard.tpl.php +++ b/Theme/Backend/dashboard.tpl.php @@ -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(); ?>
-
-
-
-
- -
-
- -
-
- -
- -
-
-
-
-
- -
-
-

Vaction

-
- -
Used Vacation -
Last Vacation -
Next Vacation -
-
-
-
- -
+
+ + +
getHtml('Recordings') ?>
getHtml('Date'); ?> + Status + Employee getHtml('Start') ?> getHtml('Break') ?> getHtml('End') ?> @@ -77,17 +37,17 @@ echo $this->getData('nav')->render(); ?>
getStart()->format('Y-m-d'); ?> + Status Here + getEmployee()->getProfile()->getAccount()->getName1(); ?>, getEmployee()->getProfile()->getAccount()->getName2(); ?> + getStart()->format('H:i:s'); ?> + getBreak() / 3600); ?>h getBreak() / 60) % 60); ?>m + getEnd() !== null ? $session->getEnd()->format('H:i') : ''; ?> + getBusy() / 3600); ?>h getBusy() / 60) % 60); ?>m +
- -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 \ No newline at end of file diff --git a/Theme/Backend/private-dashboard.tpl.php b/Theme/Backend/private-dashboard.tpl.php new file mode 100644 index 0000000..54bbc22 --- /dev/null +++ b/Theme/Backend/private-dashboard.tpl.php @@ -0,0 +1,120 @@ +getData('sessions'); + +echo $this->getData('nav')->render(); ?> + +
+
+
+
+
+ +
+
+ +
+
+ +
+ +
+
+
+
+
+ +
+
+

Work

+
+ +
This month +
Last month +
This year +
+
+
+
+ +
+
+

Vaction

+
+ +
Used Vacation +
Last Vacation +
Next Vacation +
+
+
+
+
+ +
+
+
+ + + + + + + + +
getHtml('Recordings') ?>
getHtml('Date'); ?> + Status + getHtml('Start') ?> + getHtml('Break') ?> + getHtml('End') ?> + getHtml('Total') ?> +
+
getStart()->format('Y-m-d'); ?> - getHtml('D' . $session->getStart()->format('w')); ?> + Status Here + getStart()->format('H:i'); ?> + getBreak() / 3600); ?>h getBreak() / 60) % 60); ?>m + getEnd() !== null ? $session->getEnd()->format('H:i') : ''; ?> + getBusy() / 3600); ?>h getBusy() / 60) % 60); ?>m + +
+
+
+
+ +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 \ No newline at end of file