Compare commits
24
Commits
a93c767142
...
0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d11070ffbb | ||
|
|
5cdc11d9df | ||
|
|
add7895191 | ||
|
|
20fec2dc07 | ||
|
|
123f5eef81 | ||
|
|
0e98a933e1 | ||
|
|
c2676abfa2 | ||
|
|
675c5cb9ae | ||
|
|
c8219d9c88 | ||
|
|
edc7ed5fe6 | ||
|
|
e113a80efc | ||
|
|
397416cd31 | ||
|
|
d0799eaae2 | ||
|
|
7e8ed66cb7 | ||
|
|
93f52af52b | ||
|
|
610e892063 | ||
|
|
7e8d4fbdf4 | ||
|
|
5141fff827 | ||
|
|
e6b9180432 | ||
|
|
b7b72d9044 | ||
|
|
e4cd00caf1 | ||
|
|
9e2e94c116 | ||
|
|
e9b647e8b0 | ||
|
|
5c49997eba |
+8
-1
@@ -78,7 +78,8 @@ fabric.properties
|
|||||||
.LSOverride
|
.LSOverride
|
||||||
|
|
||||||
# Icon must end with two \r
|
# Icon must end with two \r
|
||||||
Icon␍
|
Icon
|
||||||
|
|
||||||
|
|
||||||
# Thumbnails
|
# Thumbnails
|
||||||
._*
|
._*
|
||||||
@@ -99,3 +100,9 @@ Network Trash Folder
|
|||||||
Temporary Items
|
Temporary Items
|
||||||
.apdisk
|
.apdisk
|
||||||
|
|
||||||
|
/api/composer.lock
|
||||||
|
/api/codeception.yml
|
||||||
|
/api/vendor/
|
||||||
|
/api/tests/_*
|
||||||
|
/api/tests/*.suite.yml
|
||||||
|
/api/html/swagger/
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
FROM php:8.0-apache
|
||||||
|
|
||||||
|
WORKDIR /var/www
|
||||||
|
|
||||||
|
RUN a2enmod rewrite
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
paths:
|
||||||
|
tests: tests
|
||||||
|
output: tests/_output
|
||||||
|
data: tests/_data
|
||||||
|
support: tests/_support
|
||||||
|
envs: tests/_envs
|
||||||
|
actor_suffix: Tester
|
||||||
|
extensions:
|
||||||
|
enabled:
|
||||||
|
- Codeception\Extension\RunFailed
|
||||||
|
modules:
|
||||||
|
enabled:
|
||||||
|
- Db:
|
||||||
|
dsn: 'pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb'
|
||||||
|
user: 'bruce'
|
||||||
|
password: 'mypass'
|
||||||
|
dump: tests/_data/dump.sql
|
||||||
|
cleanup: true # reload dump between tests
|
||||||
|
populate: true # load dump before all tests
|
||||||
|
coverage:
|
||||||
|
enabled: true
|
||||||
|
include:
|
||||||
|
- src/*
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "torsten-hettstedt/timekeeping-api",
|
||||||
|
"description": "REST-Api zum F\u00fcllen einer Tabelle f\u00fcr die Arbeitszeiterfassung.",
|
||||||
|
"keywords": [
|
||||||
|
"microframework",
|
||||||
|
"rest",
|
||||||
|
"router",
|
||||||
|
"psr7"
|
||||||
|
],
|
||||||
|
"license": "GPL-2.0-or-later",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Torsten Lücke",
|
||||||
|
"email": "arbeit@torsten-hettstedt.de",
|
||||||
|
"homepage": "http://torsten-hettstedt.de/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"slim/slim": "^4.7.1",
|
||||||
|
"vlucas/phpdotenv": "^4.2",
|
||||||
|
"slim/psr7": "^1.3",
|
||||||
|
"jetbrains/phpstorm-attributes": "^1.0.0",
|
||||||
|
"myclabs/php-enum": "^1.8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^0.12.80",
|
||||||
|
"codeception/codeception": "^4.1.18",
|
||||||
|
"codeception/module-phpbrowser": "^1.0.0",
|
||||||
|
"codeception/module-asserts": "^1.0.0",
|
||||||
|
"codeception/module-db": "^1.1.0"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"TorstenHettstedt\\TimekeepingApi\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"TorstenHettstedt\\TimekeepingApi\\Tests\\Unit\\": "tests/unit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Some hosts may require you to use the `RewriteBase` directive.
|
||||||
|
# Determine the RewriteBase automatically and set it as environment variable.
|
||||||
|
# If you are using Apache aliases to do mass virtual hosting or installed the
|
||||||
|
# project in a subdirectory, the base path will be prepended to allow proper
|
||||||
|
# resolution of the index.php file and to redirect to the correct URI. It will
|
||||||
|
# work in environments without path prefix as well, providing a safe, one-size
|
||||||
|
# fits all solution. But as you do not need it in this case, you can comment
|
||||||
|
# the following 2 lines to eliminate the overhead.
|
||||||
|
RewriteRule ^(.*) - [E=BASE:%1]
|
||||||
|
|
||||||
|
# If the above doesn't work you might need to set the `RewriteBase` directive manually, it should be the
|
||||||
|
# absolute physical path to the directory that contains this htaccess file.
|
||||||
|
# RewriteBase /
|
||||||
|
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^ index.php [QSA,L]
|
||||||
|
</IfModule>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Slim\Factory\AppFactory;
|
||||||
|
|
||||||
|
$app = AppFactory::create();
|
||||||
|
|
||||||
|
$app->get('/', 'TorstenHettstedt\TimekeepingApi\Controller\HelloWorldController:read');
|
||||||
|
|
||||||
|
$app->addErrorMiddleware(true, true, true);
|
||||||
|
|
||||||
|
$app->run();
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.0.3",
|
||||||
|
"info": {
|
||||||
|
"title": "Zeiterfassung App",
|
||||||
|
"contact": {
|
||||||
|
"name": "Torsten Lücke",
|
||||||
|
"url": "http://torsten-hettstedt.de",
|
||||||
|
"email": "arbeit@torsten-hettstedt.de"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "GPL-2.0-or-later"
|
||||||
|
},
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"/working-hours": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Eintragungen Arbeitszeiten"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/ListEntities"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/components/responses/DatabaseException"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"$ref": "#/components/responses/ServerException"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Eintragungen Arbeitszeiten"
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"$ref": "#/components/requestBodies/WorkingHours"
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"$ref": "#/components/responses/ReadEntity"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/components/responses/DatabaseException"
|
||||||
|
},
|
||||||
|
"409": {
|
||||||
|
"$ref": "#/components/responses/RecordAlreadyExistsException"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"$ref": "#/components/responses/ServerException"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/working-hours/{date}" : {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Eintragungen Arbeitszeiten"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/date"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/ListEntities"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/components/responses/DatabaseException"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/components/responses/RecordNotFoundException"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"$ref": "#/components/responses/ServerException"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"tags": [
|
||||||
|
"Eintragungen Arbeitszeiten"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/date"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"$ref": "#/components/requestBodies/WorkingHours"
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/ReadEntity"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/components/responses/DatabaseException"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/components/responses/RecordNotFoundException"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"$ref": "#/components/responses/ServerException"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/views/working-hours/weekly": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Ansichten Arbeitszeiten"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/ListEntitiesView"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/components/responses/DatabaseException"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"$ref": "#/components/responses/ServerException"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/views/working-hours/monthly": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Ansichten Arbeitszeiten"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/ListEntitiesView"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/components/responses/DatabaseException"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"$ref": "#/components/responses/ServerException"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/views/working-hours/yearly": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Ansichten Arbeitszeiten"
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/components/responses/ListEntitiesView"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/components/responses/DatabaseException"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"$ref": "#/components/responses/ServerException"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"WorkingHours": {
|
||||||
|
"description": "Eintrag der Arbeitszeit für einen Tag",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"date": {
|
||||||
|
"description": "Datum eines Arbeitstages",
|
||||||
|
"type": "string",
|
||||||
|
"format": "date"
|
||||||
|
},
|
||||||
|
"workingHours": {
|
||||||
|
"description": "Arbeitszeit des Tages",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"WorkingHoursView": {
|
||||||
|
"type": "object",
|
||||||
|
"readOnly": true,
|
||||||
|
"properties": {
|
||||||
|
"period": {
|
||||||
|
"description": "Angabe des angegebenen Zeitraumes",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"periodDesignation": {
|
||||||
|
"description": "Bezeichnung des Zeitraumes",
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"weekly",
|
||||||
|
"monthly",
|
||||||
|
"yearly"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"totalHours": {
|
||||||
|
"description": "Gesamte Arbeitszeit im Zeitraum",
|
||||||
|
"type": "string",
|
||||||
|
"format": "time"
|
||||||
|
},
|
||||||
|
"workingDays": {
|
||||||
|
"description": "Arbeitstage im Zeitraum",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"overtime": {
|
||||||
|
"description": "Überstunden im Zeitraum",
|
||||||
|
"type": "string",
|
||||||
|
"format": "time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ErrorObject": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"status":{
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message":{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"path":{
|
||||||
|
"type": "string",
|
||||||
|
"format": "path"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requestBodies": {
|
||||||
|
"WorkingHours": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WorkingHours"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"date" : {
|
||||||
|
"description": "Datum eines Eintrages",
|
||||||
|
"name": "date",
|
||||||
|
"in" : "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"ListEntities": {
|
||||||
|
"description": "Liste der Einträge",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/WorkingHours"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ReadEntity": {
|
||||||
|
"description": "Der Eintrag",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/WorkingHours"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ListEntitiesView": {
|
||||||
|
"description": "Liste der Einträge",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/WorkingHoursView"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RecordNotFoundException": {
|
||||||
|
"description": "Der Eintrag wurde nicht gefunden",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorObject"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RecordAlreadyExistsException": {
|
||||||
|
"description": "Für das Datum wurde schon eine Arbeitszeit definiert",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorObject"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DatabaseException": {
|
||||||
|
"description": "Fehler in der Datenbank-Abfrage",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorObject"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ServerException": {
|
||||||
|
"description": "Fehler des Webservers",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ErrorObject"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
parameters:
|
||||||
|
level: 6
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
- tests/unit
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php /** @noinspection PhpUnused */
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Controller;
|
||||||
|
|
||||||
|
use Slim\Psr7\Response;
|
||||||
|
use Slim\Psr7\Request;
|
||||||
|
|
||||||
|
class HelloWorldController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param Response $response
|
||||||
|
* @param mixed[] $args
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*
|
||||||
|
* @noinspection PhpUnusedParameterInspection
|
||||||
|
*/
|
||||||
|
public function read(Request $request, Response $response, array $args): Response
|
||||||
|
{
|
||||||
|
$payload = json_encode('Hallo World');
|
||||||
|
|
||||||
|
$response->getBody()->write($payload);
|
||||||
|
|
||||||
|
return $response
|
||||||
|
->withHeader('Content-Type', 'application/json')
|
||||||
|
->withStatus(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Models;
|
||||||
|
|
||||||
|
|
||||||
|
use JsonSerializable;
|
||||||
|
|
||||||
|
interface ModelInterface extends JsonSerializable
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php /** @noinspection PhpUnusedPrivateFieldInspection */
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Models;
|
||||||
|
|
||||||
|
use MyCLabs\Enum\Enum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum-Klasse für die unterstützten Periode-Definitionen.
|
||||||
|
* Der Wert der Periode ist ein Format-String für {@link https://secure.php.net/manual/en/datetime.format.php}.
|
||||||
|
*
|
||||||
|
* @extends Enum<string>
|
||||||
|
*
|
||||||
|
* @method static PeriodDesignationEnum WEEKLY()
|
||||||
|
* @method static PeriodDesignationEnum MONTHLY()
|
||||||
|
* @method static PeriodDesignationEnum YEARLY()
|
||||||
|
*/
|
||||||
|
class PeriodDesignationEnum extends Enum
|
||||||
|
{
|
||||||
|
private const WEEKLY = 'Y#W';
|
||||||
|
private const MONTHLY = 'Y-m';
|
||||||
|
private const YEARLY = 'Y';
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Models;
|
||||||
|
|
||||||
|
|
||||||
|
use DateInterval;
|
||||||
|
use DateTime;
|
||||||
|
use JetBrains\PhpStorm\ArrayShape;
|
||||||
|
|
||||||
|
class WorkingHours implements ModelInterface
|
||||||
|
{
|
||||||
|
protected ?DateTime $workingDay = null;
|
||||||
|
protected ?DateInterval $workingTime = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return DateTime|null
|
||||||
|
*/
|
||||||
|
public function getWorkingDay(): ?DateTime
|
||||||
|
{
|
||||||
|
return $this->workingDay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param DateTime $workingDay
|
||||||
|
*
|
||||||
|
* @return WorkingHours
|
||||||
|
*/
|
||||||
|
public function setWorkingDay(DateTime $workingDay): WorkingHours
|
||||||
|
{
|
||||||
|
$this->workingDay = $workingDay;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return DateInterval|null
|
||||||
|
*/
|
||||||
|
public function getWorkingTime(): ?DateInterval
|
||||||
|
{
|
||||||
|
return $this->workingTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param DateInterval $workingTime
|
||||||
|
*
|
||||||
|
* @return WorkingHours
|
||||||
|
*/
|
||||||
|
public function setWorkingTime(DateInterval $workingTime): WorkingHours
|
||||||
|
{
|
||||||
|
$this->workingTime = $workingTime;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
#[ArrayShape(['workingDay' => "string", 'workingTime' => "string"])]
|
||||||
|
public function jsonSerialize(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'workingDay' => $this->getWorkingDay()->format('Y-m-d'),
|
||||||
|
'workingTime' => $this->getWorkingTime()->format('%H:%I:%S')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Models;
|
||||||
|
|
||||||
|
|
||||||
|
use DateInterval;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use JetBrains\PhpStorm\ArrayShape;
|
||||||
|
|
||||||
|
class WorkingHoursView implements ModelInterface
|
||||||
|
{
|
||||||
|
protected DateTimeInterface $period;
|
||||||
|
protected PeriodDesignationEnum $periodDesignation;
|
||||||
|
protected int $workingDays;
|
||||||
|
protected DateInterval $totalHours;
|
||||||
|
protected DateInterval $overtime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WorkingHoursView constructor.
|
||||||
|
*
|
||||||
|
* @param DateTimeInterface $period
|
||||||
|
* @param PeriodDesignationEnum $periodDesignation
|
||||||
|
* @param int $workingDays
|
||||||
|
* @param DateInterval $totalHours
|
||||||
|
* @param DateInterval $overtime
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
DateTimeInterface $period,
|
||||||
|
PeriodDesignationEnum $periodDesignation,
|
||||||
|
int $workingDays,
|
||||||
|
DateInterval $totalHours,
|
||||||
|
DateInterval $overtime
|
||||||
|
) {
|
||||||
|
$this->period = $period;
|
||||||
|
$this->periodDesignation = $periodDesignation;
|
||||||
|
$this->workingDays = $workingDays;
|
||||||
|
$this->totalHours = $totalHours;
|
||||||
|
$this->overtime = $overtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return DateTimeInterface
|
||||||
|
*/
|
||||||
|
public function getPeriod(): DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->period;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return PeriodDesignationEnum
|
||||||
|
*/
|
||||||
|
public function getPeriodDesignation(): PeriodDesignationEnum
|
||||||
|
{
|
||||||
|
return $this->periodDesignation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getWorkingDays(): int
|
||||||
|
{
|
||||||
|
return $this->workingDays;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return DateInterval
|
||||||
|
*/
|
||||||
|
public function getTotalHours(): DateInterval
|
||||||
|
{
|
||||||
|
return $this->totalHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return DateInterval
|
||||||
|
*/
|
||||||
|
public function getOvertime(): DateInterval
|
||||||
|
{
|
||||||
|
return $this->overtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
#[ArrayShape(['period' => "string",
|
||||||
|
'periodDesignation' => "string",
|
||||||
|
'totalHours' => "string",
|
||||||
|
'workingDays' => "int",
|
||||||
|
'overtime' => "string"
|
||||||
|
])] public function jsonSerialize(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'period' => $this->getPeriod()->format($this->getPeriodDesignation()->getValue()),
|
||||||
|
'periodDesignation' => strtolower($this->getPeriodDesignation()->getKey()),
|
||||||
|
'workingDays' => $this->getWorkingDays(),
|
||||||
|
'totalHours' => $this->getTotalHours()->format('%H:%I:%S'),
|
||||||
|
'overtime' => $this->getOvertime()->format('%H:%I:%S')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Repositories;
|
||||||
|
|
||||||
|
use DateInterval;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Exception;
|
||||||
|
use PDO;
|
||||||
|
use PDOException;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements RepositoryReaderInterface<WorkingHoursView>
|
||||||
|
*/
|
||||||
|
abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
protected const SQL_SELECT = '';
|
||||||
|
protected const SQL_WHERE = '';
|
||||||
|
|
||||||
|
protected PDO $database;
|
||||||
|
protected PeriodDesignationEnum $periodDesignation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WorkingHoursRepository constructor.
|
||||||
|
*
|
||||||
|
* @param PDO $database
|
||||||
|
*/
|
||||||
|
public function __construct(PDO $database)
|
||||||
|
{
|
||||||
|
$this->database = $database;
|
||||||
|
$this->database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Gibt alle vorhandenen Einträge zurück
|
||||||
|
*
|
||||||
|
* @return WorkingHoursView[]
|
||||||
|
* @throws Exception
|
||||||
|
* @throws PDOException
|
||||||
|
*/
|
||||||
|
public function findAll(): array
|
||||||
|
{
|
||||||
|
$models = [];
|
||||||
|
$stmt = $this->database->prepare(static::SQL_SELECT);
|
||||||
|
$stmt->execute();
|
||||||
|
while ($row = $stmt->fetch()) {
|
||||||
|
$model = new WorkingHoursView(
|
||||||
|
$this->buildDateFromPeriod($row['period']),
|
||||||
|
$this->periodDesignation,
|
||||||
|
(int) $row['workingDays'],
|
||||||
|
$this->buildDateInterval($row['totalHours']),
|
||||||
|
$this->buildDateInterval($row['overtime'])
|
||||||
|
);
|
||||||
|
$models[] = $model;
|
||||||
|
}
|
||||||
|
return $models;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den Eintrag zurück, der durch die ID
|
||||||
|
*
|
||||||
|
* @param mixed $primary_key
|
||||||
|
*
|
||||||
|
* @return WorkingHoursView
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
|
* @throws Exception
|
||||||
|
* @throws PDOException
|
||||||
|
*/
|
||||||
|
public function findByKey(mixed $primary_key): WorkingHoursView
|
||||||
|
{
|
||||||
|
$stmt = $this->database->prepare(static::SQL_SELECT . static::SQL_WHERE);
|
||||||
|
$stmt->bindParam(1, $primary_key);
|
||||||
|
$stmt->execute();
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
if ($row === false) {
|
||||||
|
throw new RepositoryRecordNotFoundException();
|
||||||
|
}
|
||||||
|
return new WorkingHoursView(
|
||||||
|
$this->buildDateFromPeriod($row['period']),
|
||||||
|
$this->periodDesignation,
|
||||||
|
(int) $row['workingDays'],
|
||||||
|
$this->buildDateInterval($row['totalHours']),
|
||||||
|
$this->buildDateInterval($row['overtime'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $time
|
||||||
|
*
|
||||||
|
* @return DateInterval
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function buildDateInterval(string $time): DateInterval
|
||||||
|
{
|
||||||
|
$time_array = explode(':', $time);
|
||||||
|
$hours = (int)$time_array[0];
|
||||||
|
$minutes = (int)$time_array[1];
|
||||||
|
$seconds = (int)$time_array[2];
|
||||||
|
$interval = new DateInterval(sprintf('PT%dH%dM%dS', abs($hours), abs($minutes), abs($seconds)));
|
||||||
|
if (($hours < 0 || ($minutes < 0) || ($seconds < 0))) {
|
||||||
|
$interval->invert = 1;
|
||||||
|
}
|
||||||
|
return $interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $period
|
||||||
|
*
|
||||||
|
* @return DateTimeInterface
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
abstract protected function buildDateFromPeriod(string $period): DateTimeInterface;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Repositories;
|
||||||
|
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class RepositoryException extends Exception
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Repositories;
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryModelAlreadyExists extends RepositoryException
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Repositories;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of \TorstenHettstedt\TimekeepingApi\Models\ModelInterface
|
||||||
|
*/
|
||||||
|
interface RepositoryReaderInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Gibt alle vorhandenen Einträge zurück
|
||||||
|
*
|
||||||
|
* @return T[]
|
||||||
|
*/
|
||||||
|
public function findAll(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den Eintrag zurück, der durch die ID
|
||||||
|
*
|
||||||
|
* @param mixed $primary_key
|
||||||
|
*
|
||||||
|
* @return T
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
|
*/
|
||||||
|
public function findByKey(mixed $primary_key);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Repositories;
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryRecordNotFoundException extends RepositoryException
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Repositories;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of \TorstenHettstedt\TimekeepingApi\Models\ModelInterface
|
||||||
|
*/
|
||||||
|
interface RepositoryWriterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param T $model
|
||||||
|
*
|
||||||
|
* @throws RepositoryModelAlreadyExists
|
||||||
|
*/
|
||||||
|
public function insert(mixed $model): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param T $model
|
||||||
|
*
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
|
*/
|
||||||
|
public function update(mixed $model): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param T $model
|
||||||
|
*
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
|
*/
|
||||||
|
public function delete(mixed $model): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Repositories;
|
||||||
|
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use PDO;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum;
|
||||||
|
|
||||||
|
class WorkingHoursMonthlyViewRepository extends AbstractWorkingHoursViewRepository
|
||||||
|
{
|
||||||
|
|
||||||
|
protected const SQL_SELECT = <<<SQL
|
||||||
|
select
|
||||||
|
"Monat" as "period",
|
||||||
|
"Gesamtarbeitszeit" as "totalHours",
|
||||||
|
"Arbeitstage" as "workingDays",
|
||||||
|
"Überstunden" as "overtime"
|
||||||
|
from "Arbeitszeiten - Monat"
|
||||||
|
SQL;
|
||||||
|
protected const SQL_WHERE = ' where "Monat" = ?';
|
||||||
|
|
||||||
|
protected PDO $database;
|
||||||
|
protected PeriodDesignationEnum $periodDesignation;
|
||||||
|
|
||||||
|
public function __construct(PDO $database)
|
||||||
|
{
|
||||||
|
parent::__construct($database);
|
||||||
|
$this->periodDesignation = PeriodDesignationEnum::MONTHLY();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildDateFromPeriod(string $period): DateTimeInterface
|
||||||
|
{
|
||||||
|
return new DateTime($period . '-01');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Repositories;
|
||||||
|
|
||||||
|
|
||||||
|
use DateInterval;
|
||||||
|
use DateTime;
|
||||||
|
use Exception;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PDO;
|
||||||
|
use PDOException;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\ModelInterface;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @implements RepositoryReaderInterface<WorkingHours>
|
||||||
|
* @implements RepositoryWriterInterface<WorkingHours>
|
||||||
|
*/
|
||||||
|
class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWriterInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
protected PDO $database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WorkingHoursRepository constructor.
|
||||||
|
*
|
||||||
|
* @param PDO $database
|
||||||
|
*/
|
||||||
|
public function __construct(PDO $database)
|
||||||
|
{
|
||||||
|
$this->database = $database;
|
||||||
|
$this->database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Gibt alle vorhandenen Einträge zurück
|
||||||
|
*
|
||||||
|
* @return WorkingHours[]
|
||||||
|
* @throws Exception
|
||||||
|
* @throws PDOException
|
||||||
|
*/
|
||||||
|
public function findAll(): array
|
||||||
|
{
|
||||||
|
$models = [];
|
||||||
|
$stmt = $this->database->prepare('select "Datum" as "workingDay", "Arbeitszeit" as "workingTime" from public."Arbeitszeiten"');
|
||||||
|
$stmt->execute();
|
||||||
|
while ($row = $stmt->fetch()) {
|
||||||
|
$model = new WorkingHours();
|
||||||
|
$model
|
||||||
|
->setWorkingDay(new DateTime($row['workingDay']))
|
||||||
|
->setWorkingTime(new DateInterval('P0000-00-00T' . $row['workingTime']));
|
||||||
|
$models[] = $model;
|
||||||
|
}
|
||||||
|
return $models;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt den Eintrag zurück, der durch die ID
|
||||||
|
*
|
||||||
|
* @param mixed $primary_key
|
||||||
|
*
|
||||||
|
* @return WorkingHours
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
|
* @throws Exception
|
||||||
|
* @throws PDOException
|
||||||
|
*/
|
||||||
|
public function findByKey(mixed $primary_key): WorkingHours
|
||||||
|
{
|
||||||
|
$stmt = $this->database->prepare('select "Datum" as "workingDay", "Arbeitszeit" as "workingTime" from public."Arbeitszeiten" where "Datum" = ?');
|
||||||
|
$stmt->bindParam(1, $primary_key);
|
||||||
|
$stmt->execute();
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
if ($row === false) {
|
||||||
|
throw new RepositoryRecordNotFoundException();
|
||||||
|
}
|
||||||
|
$model = new WorkingHours();
|
||||||
|
$model
|
||||||
|
->setWorkingDay(new DateTime($row['workingDay']))
|
||||||
|
->setWorkingTime(new DateInterval('P0000-00-00T' . $row['workingTime']));
|
||||||
|
return $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param WorkingHours|ModelInterface $model
|
||||||
|
*
|
||||||
|
* @throws RepositoryModelAlreadyExists
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function insert(mixed $model): void
|
||||||
|
{
|
||||||
|
if ($model instanceof WorkingHours) {
|
||||||
|
$workingDay = $model->getWorkingDay()->format('Y-m-d');
|
||||||
|
$workingTime = $model->getWorkingTime()->format('%H:%I:%S');
|
||||||
|
} else {
|
||||||
|
throw new InvalidArgumentException('Es wird ein Modell der Klasse "' . WorkingHours::class . '" verlangt.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->findByKey($workingDay);
|
||||||
|
throw new RepositoryModelAlreadyExists();
|
||||||
|
} /** @noinspection PhpUnusedLocalVariableInspection */
|
||||||
|
catch (RepositoryRecordNotFoundException $exception) {
|
||||||
|
$stmt = $this->database->prepare('insert into public."Arbeitszeiten" ("Datum", "Arbeitszeit") values (?, ?) on conflict do nothing');
|
||||||
|
$stmt->bindParam(1, $workingDay);
|
||||||
|
$stmt->bindParam(2, $workingTime);
|
||||||
|
$stmt->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param WorkingHours|ModelInterface $model
|
||||||
|
*
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
|
*/
|
||||||
|
public function update(mixed $model): void
|
||||||
|
{
|
||||||
|
if ($model instanceof WorkingHours) {
|
||||||
|
$workingDay = $model->getWorkingDay()->format('Y-m-d');
|
||||||
|
$workingTime = $model->getWorkingTime()->format('%H:%I:%S');
|
||||||
|
} else {
|
||||||
|
throw new InvalidArgumentException('Es wird ein Modell der Klasse "' . WorkingHours::class . '" verlangt.');
|
||||||
|
}
|
||||||
|
$stmt = $this->database->prepare('update public."Arbeitszeiten" set "Arbeitszeit" = ? where "Datum" = ?');
|
||||||
|
$stmt->bindParam(1, $workingTime);
|
||||||
|
$stmt->bindParam(2, $workingDay);
|
||||||
|
$stmt->execute();
|
||||||
|
if ($stmt->rowCount() < 1) {
|
||||||
|
throw new RepositoryRecordNotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param WorkingHours|ModelInterface $model
|
||||||
|
*
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
|
*/
|
||||||
|
public function delete(mixed $model): void
|
||||||
|
{
|
||||||
|
if ($model instanceof WorkingHours) {
|
||||||
|
$workingDay = $model->getWorkingDay()->format('Y-m-d');
|
||||||
|
} else {
|
||||||
|
throw new InvalidArgumentException('Es wird ein Modell der Klasse "' . WorkingHours::class . '" verlangt.');
|
||||||
|
}
|
||||||
|
$stmt = $this->database->prepare('delete from public."Arbeitszeiten" where "Datum" = ?');
|
||||||
|
$stmt->bindParam(1, $workingDay);
|
||||||
|
$stmt->execute();
|
||||||
|
if ($stmt->rowCount() < 1) {
|
||||||
|
throw new RepositoryRecordNotFoundException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Repositories;
|
||||||
|
|
||||||
|
use DateInterval;
|
||||||
|
use DateTime;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use PDO;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum;
|
||||||
|
|
||||||
|
class WorkingHoursWeeklyViewRepository extends AbstractWorkingHoursViewRepository
|
||||||
|
{
|
||||||
|
|
||||||
|
protected const SQL_SELECT = <<<SQL
|
||||||
|
select
|
||||||
|
"Woche" as "period",
|
||||||
|
"Gesamtarbeitszeit" as "totalHours",
|
||||||
|
"Arbeitstage" as "workingDays",
|
||||||
|
"Überstunden" as "overtime"
|
||||||
|
from "Arbeitszeiten - Woche"
|
||||||
|
SQL;
|
||||||
|
protected const SQL_WHERE = ' where "Woche" = ?';
|
||||||
|
|
||||||
|
protected PDO $database;
|
||||||
|
protected PeriodDesignationEnum $periodDesignation;
|
||||||
|
|
||||||
|
public function __construct(PDO $database)
|
||||||
|
{
|
||||||
|
parent::__construct($database);
|
||||||
|
$this->periodDesignation = PeriodDesignationEnum::WEEKLY();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildDateFromPeriod(string $period): DateTimeInterface
|
||||||
|
{
|
||||||
|
$time_array = explode('#', $period);
|
||||||
|
$year = (int)$time_array[0];
|
||||||
|
$week = (int)$time_array[1] - 1;
|
||||||
|
$date = new DateTime('first day of January ' . $year);
|
||||||
|
$date->modify('monday this week');
|
||||||
|
$date->add(new DateInterval(sprintf('P%dW', $week)));
|
||||||
|
return $date;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Repositories;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Exception;
|
||||||
|
use PDO;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum;
|
||||||
|
|
||||||
|
class WorkingHoursYearlyViewRepository extends AbstractWorkingHoursViewRepository
|
||||||
|
{
|
||||||
|
|
||||||
|
protected const SQL_SELECT = <<<SQL
|
||||||
|
select
|
||||||
|
"Jahr" as "period",
|
||||||
|
"Gesamtarbeitszeit" as "totalHours",
|
||||||
|
"Arbeitstage" as "workingDays",
|
||||||
|
"Überstunden" as "overtime"
|
||||||
|
from "Arbeitszeiten - Jahr"
|
||||||
|
SQL;
|
||||||
|
protected const SQL_WHERE = ' where "Jahr" = ?';
|
||||||
|
|
||||||
|
protected PDO $database;
|
||||||
|
protected PeriodDesignationEnum $periodDesignation;
|
||||||
|
|
||||||
|
public function __construct(PDO $database)
|
||||||
|
{
|
||||||
|
parent::__construct($database);
|
||||||
|
$this->periodDesignation = PeriodDesignationEnum::YEARLY();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $period
|
||||||
|
*
|
||||||
|
* @return DateTimeInterface
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function buildDateFromPeriod(string $period): DateTimeInterface
|
||||||
|
{
|
||||||
|
return new DateTime($period . '-01-01');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
--
|
||||||
|
-- PostgreSQL database dump
|
||||||
|
--
|
||||||
|
|
||||||
|
SET statement_timeout = 0;
|
||||||
|
SET lock_timeout = 0;
|
||||||
|
SET idle_in_transaction_session_timeout = 0;
|
||||||
|
SET client_encoding = 'UTF8';
|
||||||
|
SET standard_conforming_strings = on;
|
||||||
|
SELECT pg_catalog.set_config('search_path', '', false);
|
||||||
|
SET check_function_bodies = false;
|
||||||
|
SET xmloption = content;
|
||||||
|
SET client_min_messages = warning;
|
||||||
|
SET row_security = off;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS public."Arbeitszeiten";
|
||||||
|
DROP VIEW IF EXISTS public."Arbeitszeiten - Woche";
|
||||||
|
DROP VIEW IF EXISTS public."Arbeitszeiten - Monat";
|
||||||
|
DROP VIEW IF EXISTS public."Arbeitszeiten - Jahr";
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: Arbeitszeiten; Type: TABLE; Schema: public; Owner: torsten
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public."Arbeitszeiten" (
|
||||||
|
"Datum" date not null,
|
||||||
|
"Arbeitszeit" interval(6) null,
|
||||||
|
constraint "Arbeitszeiten_pkey" primary key ("Datum")
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public."Arbeitszeiten" OWNER TO bruce;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: Arbeitszeiten - Jahr; Type: VIEW; Schema: public; Owner: torsten
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW public."Arbeitszeiten - Jahr" AS
|
||||||
|
SELECT (date_part('year'::text, "Arbeitszeiten"."Datum"))::text AS "Jahr",
|
||||||
|
sum("Arbeitszeiten"."Arbeitszeit") AS "Gesamtarbeitszeit",
|
||||||
|
count("Arbeitszeiten"."Datum") AS "Arbeitstage",
|
||||||
|
(sum("Arbeitszeiten"."Arbeitszeit") - ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden"
|
||||||
|
FROM public."Arbeitszeiten"
|
||||||
|
GROUP BY (date_part('year'::text, "Arbeitszeiten"."Datum"))
|
||||||
|
ORDER BY (date_part('year'::text, "Arbeitszeiten"."Datum"));
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public."Arbeitszeiten - Jahr" OWNER TO bruce;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: Arbeitszeiten - Monat; Type: VIEW; Schema: public; Owner: torsten
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW public."Arbeitszeiten - Monat" AS
|
||||||
|
SELECT to_char(date_trunc('month'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone), 'TMYYYY TMMonth'::text) AS "Monat",
|
||||||
|
sum("Arbeitszeiten"."Arbeitszeit") AS "Gesamtarbeitszeit",
|
||||||
|
count("Arbeitszeiten"."Datum") AS "Arbeitstage",
|
||||||
|
(sum("Arbeitszeiten"."Arbeitszeit") - ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden"
|
||||||
|
FROM public."Arbeitszeiten"
|
||||||
|
GROUP BY (date_trunc('month'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone))
|
||||||
|
ORDER BY (date_trunc('month'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone));
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public."Arbeitszeiten - Monat" OWNER TO bruce;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: Arbeitszeiten - Woche; Type: VIEW; Schema: public; Owner: torsten
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW public."Arbeitszeiten - Woche" AS
|
||||||
|
SELECT to_char(date_trunc('week'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone), 'TMYYYY#WW'::text) AS "Woche",
|
||||||
|
sum("Arbeitszeiten"."Arbeitszeit") AS "Gesamtarbeitszeit",
|
||||||
|
count("Arbeitszeiten"."Datum") AS "Arbeitstage",
|
||||||
|
(sum("Arbeitszeiten"."Arbeitszeit") - ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden"
|
||||||
|
FROM public."Arbeitszeiten"
|
||||||
|
GROUP BY (date_trunc('week'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone))
|
||||||
|
ORDER BY (date_trunc('week'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone));
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public."Arbeitszeiten - Woche" OWNER TO bruce;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Data for Name: Arbeitszeiten; Type: TABLE DATA; Schema: public; Owner: torsten
|
||||||
|
--
|
||||||
|
|
||||||
|
COPY public."Arbeitszeiten" ("Datum", "Arbeitszeit") FROM stdin;
|
||||||
|
2020-01-07 08:06:00
|
||||||
|
2020-01-08 08:16:00
|
||||||
|
2020-01-09 07:49:00
|
||||||
|
2020-01-10 07:30:00
|
||||||
|
2020-01-13 08:06:00
|
||||||
|
2020-01-14 08:04:00
|
||||||
|
2020-01-15 08:20:00
|
||||||
|
2020-01-16 07:13:00
|
||||||
|
2020-01-17 07:57:00
|
||||||
|
2020-01-20 07:53:00
|
||||||
|
2020-01-21 07:49:00
|
||||||
|
2020-01-22 07:56:00
|
||||||
|
2020-01-23 07:55:00
|
||||||
|
2020-01-24 07:08:00
|
||||||
|
2020-01-27 08:23:00
|
||||||
|
2020-01-28 07:20:00
|
||||||
|
2020-01-29 08:13:00
|
||||||
|
2020-01-30 08:43:00
|
||||||
|
2020-01-31 07:21:00
|
||||||
|
2020-02-03 07:56:00
|
||||||
|
2020-02-04 08:05:00
|
||||||
|
2020-02-05 07:58:00
|
||||||
|
2020-02-06 08:01:00
|
||||||
|
2020-02-07 07:59:00
|
||||||
|
2020-02-10 07:50:00
|
||||||
|
2020-02-11 08:01:00
|
||||||
|
2020-02-12 08:14:00
|
||||||
|
2020-02-13 08:12:00
|
||||||
|
2020-02-14 07:51:00
|
||||||
|
2020-02-17 07:59:00
|
||||||
|
2020-02-18 08:05:00
|
||||||
|
2020-02-19 07:34:00
|
||||||
|
2020-02-20 07:33:00
|
||||||
|
2020-02-21 07:56:00
|
||||||
|
2020-02-24 08:02:00
|
||||||
|
2020-02-25 08:13:00
|
||||||
|
2020-02-26 08:42:00
|
||||||
|
2020-02-27 07:49:00
|
||||||
|
2020-02-28 08:16:00
|
||||||
|
2020-03-02 08:22:00
|
||||||
|
2020-03-03 08:20:00
|
||||||
|
2020-03-04 08:12:00
|
||||||
|
2020-03-05 08:32:00
|
||||||
|
2020-03-06 04:41:00
|
||||||
|
2020-03-09 07:05:00
|
||||||
|
2020-03-10 07:34:00
|
||||||
|
2020-03-11 07:39:00
|
||||||
|
2020-03-12 07:50:00
|
||||||
|
2020-03-13 08:26:00
|
||||||
|
2020-03-16 07:51:00
|
||||||
|
2020-03-17 07:50:00
|
||||||
|
2020-03-18 07:19:00
|
||||||
|
2020-03-19 07:55:00
|
||||||
|
2020-03-20 05:43:00
|
||||||
|
2020-03-23 08:05:00
|
||||||
|
2020-03-24 08:21:00
|
||||||
|
2020-03-25 08:10:00
|
||||||
|
2020-03-26 10:31:00
|
||||||
|
2020-03-27 04:55:00
|
||||||
|
2020-03-30 08:21:00
|
||||||
|
2020-03-31 08:01:00
|
||||||
|
\.
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Models;
|
||||||
|
|
||||||
|
use Codeception\Test\Unit;
|
||||||
|
use DateInterval;
|
||||||
|
use DateTime;
|
||||||
|
use Error;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
|
||||||
|
|
||||||
|
class WorkingHoursTest extends Unit
|
||||||
|
{
|
||||||
|
|
||||||
|
public function testWorkingHoursWithoutContent(): void
|
||||||
|
{
|
||||||
|
$obj = new WorkingHours();
|
||||||
|
$this->assertNull($obj->getWorkingDay());
|
||||||
|
$this->assertNull($obj->getWorkingTime());
|
||||||
|
|
||||||
|
$this->expectException(Error::class);
|
||||||
|
$obj->jsonSerialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
*/
|
||||||
|
public function workingHoursProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
new DateTime('2020-11-30'),
|
||||||
|
new DateInterval("PT8H"),
|
||||||
|
[
|
||||||
|
'workingDay' => '2020-11-30',
|
||||||
|
'workingTime' => '08:00:00',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
new DateTime('2021-05-03'),
|
||||||
|
new DateInterval("PT7H45M"),
|
||||||
|
[
|
||||||
|
'workingDay' => '2021-05-03',
|
||||||
|
'workingTime' => '07:45:00',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param DateTime $date
|
||||||
|
* @param DateInterval $interval
|
||||||
|
* @param array<string, string> $should_json
|
||||||
|
*
|
||||||
|
* @dataProvider workingHoursProvider
|
||||||
|
*/
|
||||||
|
public function testWorkingHoursWithContent(DateTime $date, DateInterval $interval, array $should_json): void
|
||||||
|
{
|
||||||
|
|
||||||
|
$obj = new WorkingHours();
|
||||||
|
$obj
|
||||||
|
->setWorkingDay($date)
|
||||||
|
->setWorkingTime($interval);
|
||||||
|
|
||||||
|
$this->assertNotNull($obj->getWorkingDay());
|
||||||
|
$this->assertInstanceOf(DateTime::class, $obj->getWorkingDay());
|
||||||
|
$this->assertEquals($date, $obj->getWorkingDay());
|
||||||
|
|
||||||
|
$this->assertNotNull($obj->getWorkingTime());
|
||||||
|
$this->assertInstanceOf(DateInterval::class, $obj->getWorkingTime());
|
||||||
|
$this->assertEquals($interval, $obj->getWorkingTime());
|
||||||
|
|
||||||
|
$json = $obj->jsonSerialize();
|
||||||
|
$this->assertIsArray($json);
|
||||||
|
$this->assertEquals($should_json, $json);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Models;
|
||||||
|
|
||||||
|
use Codeception\Test\Unit;
|
||||||
|
use DateInterval;
|
||||||
|
use DateTime;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView;
|
||||||
|
|
||||||
|
class WorkingHoursViewTest extends Unit
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
*/
|
||||||
|
public function workingHoursProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
new DateTime('2020-11-30'),
|
||||||
|
PeriodDesignationEnum::WEEKLY(),
|
||||||
|
15,
|
||||||
|
new DateInterval("PT32H22M"),
|
||||||
|
new DateInterval("PT22M"),
|
||||||
|
[
|
||||||
|
'period' => '2020#49',
|
||||||
|
'periodDesignation' => 'weekly',
|
||||||
|
'workingDays' => 15,
|
||||||
|
'totalHours' => '32:22:00',
|
||||||
|
'overtime' => '00:22:00'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
new DateTime('2020-11-30'),
|
||||||
|
PeriodDesignationEnum::MONTHLY(),
|
||||||
|
15,
|
||||||
|
new DateInterval("PT32H22M"),
|
||||||
|
new DateInterval("PT22M"),
|
||||||
|
[
|
||||||
|
'period' => '2020-11',
|
||||||
|
'periodDesignation' => 'monthly',
|
||||||
|
'workingDays' => 15,
|
||||||
|
'totalHours' => '32:22:00',
|
||||||
|
'overtime' => '00:22:00'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
new DateTime('2020-11-30'),
|
||||||
|
PeriodDesignationEnum::YEARLY(),
|
||||||
|
15,
|
||||||
|
new DateInterval("PT32H22M"),
|
||||||
|
new DateInterval("PT22M"),
|
||||||
|
[
|
||||||
|
'period' => '2020',
|
||||||
|
'periodDesignation' => 'yearly',
|
||||||
|
'workingDays' => 15,
|
||||||
|
'totalHours' => '32:22:00',
|
||||||
|
'overtime' => '00:22:00'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param DateTimeInterface $period
|
||||||
|
* @param PeriodDesignationEnum $periodDesignation
|
||||||
|
* @param int $workingDays
|
||||||
|
* @param DateInterval $totalHours
|
||||||
|
* @param DateInterval $overtime
|
||||||
|
* @param array<string, string> $should_json
|
||||||
|
*
|
||||||
|
* @dataProvider workingHoursProvider
|
||||||
|
*/
|
||||||
|
public function testWorkingHoursViewWithContent(
|
||||||
|
DateTimeInterface $period,
|
||||||
|
PeriodDesignationEnum $periodDesignation,
|
||||||
|
int $workingDays,
|
||||||
|
DateInterval $totalHours,
|
||||||
|
DateInterval $overtime,
|
||||||
|
array $should_json
|
||||||
|
): void
|
||||||
|
{
|
||||||
|
|
||||||
|
$obj = new WorkingHoursView($period, $periodDesignation, $workingDays, $totalHours, $overtime);
|
||||||
|
|
||||||
|
$this->assertNotNull($obj->getPeriod());
|
||||||
|
$this->assertInstanceOf(DateTimeInterface::class, $obj->getPeriod());
|
||||||
|
$this->assertEquals($period, $obj->getPeriod());
|
||||||
|
|
||||||
|
$this->assertNotNull($obj->getPeriodDesignation());
|
||||||
|
$this->assertInstanceOf(PeriodDesignationEnum::class, $obj->getPeriodDesignation());
|
||||||
|
$this->assertEquals($periodDesignation, $obj->getPeriodDesignation());
|
||||||
|
|
||||||
|
$this->assertNotNull($obj->getWorkingDays());
|
||||||
|
$this->assertIsInt($obj->getWorkingDays());
|
||||||
|
$this->assertEquals($workingDays, $obj->getWorkingDays());
|
||||||
|
|
||||||
|
$this->assertNotNull($obj->getTotalHours());
|
||||||
|
$this->assertInstanceOf(DateInterval::class, $obj->getTotalHours());
|
||||||
|
$this->assertEquals($totalHours, $obj->getTotalHours());
|
||||||
|
|
||||||
|
$this->assertNotNull($obj->getOvertime());
|
||||||
|
$this->assertInstanceOf(DateInterval::class, $obj->getOvertime());
|
||||||
|
$this->assertEquals($overtime, $obj->getOvertime());
|
||||||
|
|
||||||
|
|
||||||
|
$json = $obj->jsonSerialize();
|
||||||
|
$this->assertIsArray($json);
|
||||||
|
$this->assertEquals($should_json, $json);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Repositories;
|
||||||
|
|
||||||
|
use Codeception\Test\Unit;
|
||||||
|
use DateInterval;
|
||||||
|
use DateTime;
|
||||||
|
use Exception;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PDO;
|
||||||
|
use PDOStatement;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\ModelInterface;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryModelAlreadyExists;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursRepository;
|
||||||
|
|
||||||
|
class WorkingHoursRepositoryTest extends Unit
|
||||||
|
{
|
||||||
|
const EXISTING_DATE = '2020-01-28';
|
||||||
|
const EXISTING_INTERVAL = 'PT7H20M';
|
||||||
|
const NEW_DATE = '2020-04-01';
|
||||||
|
const NEW_INTERVAL = 'PT7H59M';
|
||||||
|
const RECORDS_COUNT = 61;
|
||||||
|
|
||||||
|
protected PDO $pdoObject;
|
||||||
|
protected PDOStatement $pdoStatement;
|
||||||
|
|
||||||
|
protected function _before(): void
|
||||||
|
{
|
||||||
|
/** @noinspection SpellCheckingInspection */
|
||||||
|
$this->pdoObject = new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass');
|
||||||
|
parent::_before();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testFindAll(): void
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursRepository($this->pdoObject);
|
||||||
|
$models = $repository->findAll();
|
||||||
|
$this->assertIsArray($models);
|
||||||
|
$this->assertContainsOnly(WorkingHours::class, $models);
|
||||||
|
$this->assertCount(self::RECORDS_COUNT, $models);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
|
*/
|
||||||
|
public function testExistingFindByKey(): void
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursRepository($this->pdoObject);
|
||||||
|
$model = $repository->findByKey(self::EXISTING_DATE);
|
||||||
|
$this->assertInstanceOf(WorkingHours::class, $model);
|
||||||
|
$this->assertEquals(new DateTime(self::EXISTING_DATE), $model->getWorkingDay());
|
||||||
|
$this->assertEquals(new DateInterval(self::EXISTING_INTERVAL), $model->getWorkingTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNotExistingFindByKey(): void
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursRepository($this->pdoObject);
|
||||||
|
$this->expectException(RepositoryRecordNotFoundException::class);
|
||||||
|
$repository->findByKey(self::NEW_DATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testInsertWithInvalidModel(): void
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursRepository($this->pdoObject);
|
||||||
|
$model = $this->makeEmpty(ModelInterface::class);
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$repository->insert($model);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testNewInsert(): void
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursRepository($this->pdoObject);
|
||||||
|
$model = $this->make(WorkingHours::class, [
|
||||||
|
'workingDay' => new DateTime(self::NEW_DATE),
|
||||||
|
'workingTime' => new DateInterval(self::NEW_INTERVAL),
|
||||||
|
]);
|
||||||
|
$repository->insert($model);
|
||||||
|
$model = $repository->findByKey(self::NEW_DATE);
|
||||||
|
$this->assertInstanceOf(WorkingHours::class, $model);
|
||||||
|
$this->assertEquals(new DateTime(self::NEW_DATE), $model->getWorkingDay());
|
||||||
|
$this->assertEquals(new DateInterval(self::NEW_INTERVAL), $model->getWorkingTime());
|
||||||
|
$this->assertCount(self::RECORDS_COUNT + 1, $repository->findAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testExistingInsert(): void
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursRepository($this->pdoObject);
|
||||||
|
$model = $this->make(WorkingHours::class, [
|
||||||
|
'workingDay' => new DateTime(self::EXISTING_DATE),
|
||||||
|
'workingTime' => new DateInterval(self::NEW_INTERVAL),
|
||||||
|
]);
|
||||||
|
$this->expectException(RepositoryModelAlreadyExists::class);
|
||||||
|
$repository->insert($model);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testDeleteWithInvalidModel(): void
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursRepository($this->pdoObject);
|
||||||
|
$model = $this->makeEmpty(ModelInterface::class);
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$repository->delete($model);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testExistsDelete(): void
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursRepository($this->pdoObject);
|
||||||
|
$model = $this->make(WorkingHours::class, [
|
||||||
|
'workingDay' => new DateTime(self::EXISTING_DATE),
|
||||||
|
'workingTime' => new DateInterval(self::EXISTING_INTERVAL),
|
||||||
|
]);
|
||||||
|
$repository->delete($model);
|
||||||
|
$this->expectException(RepositoryRecordNotFoundException::class);
|
||||||
|
$repository->findByKey(self::EXISTING_DATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testNotExistsDelete(): void
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursRepository($this->pdoObject);
|
||||||
|
$model = $this->make(WorkingHours::class, [
|
||||||
|
'workingDay' => new DateTime(self::NEW_DATE),
|
||||||
|
'workingTime' => new DateInterval(self::NEW_INTERVAL),
|
||||||
|
]);
|
||||||
|
$this->expectException(RepositoryRecordNotFoundException::class);
|
||||||
|
$repository->delete($model);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testUpdateWithInvalidModel(): void
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursRepository($this->pdoObject);
|
||||||
|
$model = $this->makeEmpty(ModelInterface::class);
|
||||||
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
$repository->update($model);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testExistsUpdate(): void
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursRepository($this->pdoObject);
|
||||||
|
$model = $this->make(WorkingHours::class, [
|
||||||
|
'workingDay' => new DateTime(self::EXISTING_DATE),
|
||||||
|
'workingTime' => new DateInterval(self::NEW_INTERVAL),
|
||||||
|
]);
|
||||||
|
$repository->update($model);
|
||||||
|
$model_db = $repository->findByKey(self::EXISTING_DATE);
|
||||||
|
$this->assertEquals(new DateInterval(self::NEW_INTERVAL), $model_db->getWorkingTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testNotExistsUpdate(): void
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursRepository($this->pdoObject);
|
||||||
|
$model = $this->make(WorkingHours::class, [
|
||||||
|
'workingDay' => new DateTime(self::NEW_DATE),
|
||||||
|
'workingTime' => new DateInterval(self::NEW_INTERVAL),
|
||||||
|
]);
|
||||||
|
$this->expectException(RepositoryRecordNotFoundException::class);
|
||||||
|
$repository->update($model);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Repositories;
|
||||||
|
|
||||||
|
use Codeception\Test\Unit;
|
||||||
|
use DateTime;
|
||||||
|
use DateTimeZone;
|
||||||
|
use Exception;
|
||||||
|
use PDO;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursWeeklyViewRepository;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursMonthlyViewRepository;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursYearlyViewRepository;
|
||||||
|
|
||||||
|
class WorkingHoursViewRepositoryTest extends Unit
|
||||||
|
{
|
||||||
|
protected PDO $pdoObject;
|
||||||
|
|
||||||
|
protected function _before(): void
|
||||||
|
{
|
||||||
|
/** @noinspection SpellCheckingInspection */
|
||||||
|
$this->pdoObject = new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass');
|
||||||
|
parent::_before();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
*/
|
||||||
|
public function listObjectProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[WorkingHoursYearlyViewRepository::class, 1],
|
||||||
|
[WorkingHoursMonthlyViewRepository::class, 3],
|
||||||
|
[WorkingHoursWeeklyViewRepository::class, 13],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function existingObjectProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[WorkingHoursYearlyViewRepository::class, '2020', 61, new DateTime('2020-01-01', new DateTimeZone('UCT'))],
|
||||||
|
[WorkingHoursMonthlyViewRepository::class, '2020 February', 20, new DateTime('2020-02-01', new DateTimeZone('UCT'))],
|
||||||
|
[WorkingHoursWeeklyViewRepository::class, '2020#03', 5, new DateTime('2020-01-13', new DateTimeZone('UCT'))],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
*/
|
||||||
|
public function notExistingObjectProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[WorkingHoursYearlyViewRepository::class, '2022',],
|
||||||
|
[WorkingHoursMonthlyViewRepository::class, '2020 May',],
|
||||||
|
[WorkingHoursWeeklyViewRepository::class, '2020#30',],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $period_class
|
||||||
|
* @param int $count_records
|
||||||
|
*
|
||||||
|
* @dataProvider listObjectProvider
|
||||||
|
*/
|
||||||
|
public function testFindAll(string $period_class, int $count_records): void
|
||||||
|
{
|
||||||
|
$repository = new $period_class($this->pdoObject);
|
||||||
|
$models = $repository->findAll();
|
||||||
|
$this->assertIsArray($models);
|
||||||
|
$this->assertContainsOnly(WorkingHoursView::class, $models);
|
||||||
|
$this->assertCount($count_records, $models);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @dataProvider existingObjectProvider
|
||||||
|
*
|
||||||
|
* @param string $period_class
|
||||||
|
* @param string $search
|
||||||
|
* @param int $workingDays
|
||||||
|
* @param DateTime $period
|
||||||
|
*/
|
||||||
|
public function testExistingFindByKey(string $period_class, string $search, int $workingDays, DateTime $period): void
|
||||||
|
{
|
||||||
|
$repository = new $period_class($this->pdoObject);
|
||||||
|
$model = $repository->findByKey($search);
|
||||||
|
$this->assertInstanceOf(WorkingHoursView::class, $model);
|
||||||
|
$this->assertEquals($workingDays, $model->getWorkingDays());
|
||||||
|
$this->assertEquals($period->format('Ymd'), $model->getPeriod()->format('Ymd'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $period_class
|
||||||
|
* @param string $search
|
||||||
|
*
|
||||||
|
* @dataProvider notExistingObjectProvider
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function testNotExistingFindByKey(string $period_class, string $search): void
|
||||||
|
{
|
||||||
|
$repository = new $period_class($this->pdoObject);
|
||||||
|
$this->expectException(RepositoryRecordNotFoundException::class);
|
||||||
|
$repository->findByKey($search);
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
volumes:
|
||||||
|
logs:
|
||||||
|
driver: local
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
build: ./api/
|
||||||
|
environment:
|
||||||
|
docker: "true"
|
||||||
|
ports:
|
||||||
|
- 8090:80
|
||||||
|
volumes:
|
||||||
|
- ./api:/var/www
|
||||||
|
- logs:/var/www/logs
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
-- # Definition der Tabelle
|
||||||
|
-- DROP TABLE public."Arbeitszeiten";
|
||||||
|
create table public."Arbeitszeiten"
|
||||||
|
(
|
||||||
|
"Datum" date not null,
|
||||||
|
"Arbeitszeit" interval(6) null,
|
||||||
|
constraint "Arbeitszeiten_pkey" primary key ("Datum")
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- # Definition der Ansichten
|
||||||
|
-- ## Wöchentlich
|
||||||
|
create view "Arbeitszeiten - Woche" ("Woche", "Gesamtarbeitszeit", "Arbeitstage", "Überstunden") as
|
||||||
|
select
|
||||||
|
to_char(date_trunc('week'::text, "Arbeitszeiten"."Datum"::timestamp with time zone),
|
||||||
|
'TMYYYY#WW'::text) as "Woche",
|
||||||
|
sum("Arbeitszeiten"."Arbeitszeit") as "Gesamtarbeitszeit",
|
||||||
|
count("Arbeitszeiten"."Datum") as "Arbeitstage",
|
||||||
|
sum("Arbeitszeiten"."Arbeitszeit") - count("Arbeitszeiten"."Datum") ::double precision * '08:00:00'::interval as "Überstunden"
|
||||||
|
from "Arbeitszeiten"
|
||||||
|
group by (date_trunc('week'::text, "Arbeitszeiten"."Datum"::timestamp with time zone))
|
||||||
|
order by (date_trunc('week'::text, "Arbeitszeiten"."Datum"::timestamp with time zone));
|
||||||
|
|
||||||
|
-- ## Monatlich
|
||||||
|
create view "Arbeitszeiten - Monat" ("Monat", "Gesamtarbeitszeit", "Arbeitstage", "Überstunden") as
|
||||||
|
select
|
||||||
|
to_char(date_trunc('month'::text, "Arbeitszeiten"."Datum"::timestamp with time zone),
|
||||||
|
'TMYYYY TMMonth'::text) as "Monat",
|
||||||
|
sum("Arbeitszeiten"."Arbeitszeit") as "Gesamtarbeitszeit",
|
||||||
|
count("Arbeitszeiten"."Datum") as "Arbeitstage",
|
||||||
|
sum("Arbeitszeiten"."Arbeitszeit") - count("Arbeitszeiten"."Datum") ::double precision * '08:00:00'::interval as "Überstunden"
|
||||||
|
from "Arbeitszeiten"
|
||||||
|
group by (date_trunc('month'::text, "Arbeitszeiten"."Datum"::timestamp with time zone))
|
||||||
|
order by (date_trunc('month'::text, "Arbeitszeiten"."Datum"::timestamp with time zone));
|
||||||
|
|
||||||
|
-- ## Jährlich
|
||||||
|
create view "Arbeitszeiten - Jahr"("Jahr", "Gesamtarbeitszeit", "Arbeitstage", "Überstunden") as
|
||||||
|
select
|
||||||
|
date_part('year'::text, "Arbeitszeiten"."Datum")::text as "Jahr",
|
||||||
|
sum("Arbeitszeiten"."Arbeitszeit") as "Gesamtarbeitszeit",
|
||||||
|
count("Arbeitszeiten"."Datum") as "Arbeitstage",
|
||||||
|
sum("Arbeitszeiten"."Arbeitszeit") - count("Arbeitszeiten"."Datum") ::double precision * '08:00:00'::interval as "Überstunden"
|
||||||
|
from "Arbeitszeiten"
|
||||||
|
group by (date_part('year'::text, "Arbeitszeiten"."Datum"))
|
||||||
|
order by (date_part('year'::text, "Arbeitszeiten"."Datum"));
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
@startuml
|
||||||
|
|
||||||
|
!define DOMAIN (M,#FFAAAA) Domain Model
|
||||||
|
!define VIEW (V,DarkTurquoise) Domain Model - Ansicht
|
||||||
|
|
||||||
|
class Arbeitszeiten <<DOMAIN>> {
|
||||||
|
Datum
|
||||||
|
Arbeitszeit
|
||||||
|
insert()
|
||||||
|
list()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Arbeitszeiten-Ansicht <<VIEW>> {
|
||||||
|
Gesamtarbeitszeit
|
||||||
|
Arbeitstage
|
||||||
|
Überstunden
|
||||||
|
list()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Arbeitszeiten-Woche <<VIEW>> {
|
||||||
|
Woche
|
||||||
|
}
|
||||||
|
|
||||||
|
class Arbeitszeiten-Monat <<VIEW>> {
|
||||||
|
Monat
|
||||||
|
}
|
||||||
|
|
||||||
|
class Arbeitszeiten-Jahr <<VIEW>> {
|
||||||
|
Jahr
|
||||||
|
}
|
||||||
|
|
||||||
|
"Arbeitszeiten-Ansicht" <|-- "Arbeitszeiten-Woche"
|
||||||
|
"Arbeitszeiten-Ansicht" <|-- "Arbeitszeiten-Monat"
|
||||||
|
"Arbeitszeiten-Ansicht" <|-- "Arbeitszeiten-Jahr"
|
||||||
|
|
||||||
|
@enduml
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
@startuml
|
||||||
|
|
||||||
|
!define DOMAIN (M,#FFAAAA) Domain Model
|
||||||
|
!define TABLE (T,DarkTurquoise) Physical Data Model
|
||||||
|
!define VIEW (V,#AAFFAA) Model View
|
||||||
|
!define AUTO Auto Generated
|
||||||
|
!define ID_KEY id: INTEGER <<PK>> <<AUTO>> <<Surrogate>>
|
||||||
|
|
||||||
|
class Arbeitszeiten <<TABLE>> {
|
||||||
|
Datum: DATE
|
||||||
|
Arbeitszeit: INTERVAL
|
||||||
|
}
|
||||||
|
|
||||||
|
class Arbeitszeiten-Ansicht <<VIEW>> {
|
||||||
|
Gesamtarbeitszeit: INTERVAL
|
||||||
|
Arbeitstage: INTEGER
|
||||||
|
Überstunden: INTERVAL
|
||||||
|
list()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Arbeitszeiten-Woche <<VIEW>> {
|
||||||
|
Woche: TEXT
|
||||||
|
}
|
||||||
|
|
||||||
|
class Arbeitszeiten-Monat <<VIEW>> {
|
||||||
|
Monat: TEXT
|
||||||
|
}
|
||||||
|
|
||||||
|
class Arbeitszeiten-Jahr <<VIEW>> {
|
||||||
|
Jahr: TEXT
|
||||||
|
}
|
||||||
|
|
||||||
|
"Arbeitszeiten-Ansicht" <|-- "Arbeitszeiten-Woche"
|
||||||
|
"Arbeitszeiten-Ansicht" <|-- "Arbeitszeiten-Monat"
|
||||||
|
"Arbeitszeiten-Ansicht" <|-- "Arbeitszeiten-Jahr"
|
||||||
|
|
||||||
|
@enduml
|
||||||
Reference in New Issue
Block a user