Merge pull request 'api-erstellen' (#1) from api-erstellen into master
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -80,6 +80,7 @@ fabric.properties
|
|||||||
# 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/.env
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
## Datenbank-Einstellungen
|
||||||
|
DATABASES_HOST=psql.domain.tld
|
||||||
|
DATABASES_NAME=db
|
||||||
|
DATABASES_USER=user
|
||||||
|
DATABASES_PASS=1234
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
FROM php:8.0-apache
|
||||||
|
|
||||||
|
RUN apt update && apt install -y libpq-dev
|
||||||
|
RUN docker-php-ext-install -j$(nproc) pdo pdo_pgsql
|
||||||
|
|
||||||
|
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,46 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"php-di/slim-bridge": "^3.1.0",
|
||||||
|
"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",
|
||||||
|
"codeception/module-rest": "^1.2.8"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"TorstenHettstedt\\TimekeepingApi\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"TorstenHettstedt\\TimekeepingApi\\Tests\\Unit\\": "tests/unit",
|
||||||
|
"TorstenHettstedt\\TimekeepingApi\\Tests\\Api\\": "tests/api",
|
||||||
|
"Helper\\": "tests/_support/Helper"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<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} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^ index.php [QSA,L]
|
||||||
|
</IfModule>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
use DI\Container;
|
||||||
|
use Slim\Factory\AppFactory;
|
||||||
|
use Slim\Routing\RouteCollectorProxy;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursController;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursViewController;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Middleware\ErrorHandler;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Middleware\JsonBodyParserMiddleware;
|
||||||
|
|
||||||
|
$container = new Container();
|
||||||
|
|
||||||
|
$container->set('databases', function () {
|
||||||
|
$dsn = "pgsql:host=${_ENV['DATABASES_HOST']};port=5432;dbname=${_ENV['DATABASES_NAME']}";
|
||||||
|
return new PDO($dsn, $_ENV['DATABASES_USER'], $_ENV['DATABASES_PASS']);
|
||||||
|
});
|
||||||
|
|
||||||
|
AppFactory::setContainer($container);
|
||||||
|
$app = AppFactory::create();
|
||||||
|
$app->add(new JsonBodyParserMiddleware());
|
||||||
|
|
||||||
|
$app->group('/views/working-hours', function (RouteCollectorProxy $group) {
|
||||||
|
$group->get('/weekly', WorkingHoursViewController::class . ':browseWeekly');
|
||||||
|
$group->get('/monthly', WorkingHoursViewController::class . ':browseMonthly');
|
||||||
|
$group->get('/yearly', WorkingHoursViewController::class . ':browseYearly');
|
||||||
|
});
|
||||||
|
|
||||||
|
$app->group('/working-hours', function (RouteCollectorProxy $group) {
|
||||||
|
$group->get('', WorkingHoursController::class . ':browse');
|
||||||
|
$group->post('', WorkingHoursController::class . ':creat');
|
||||||
|
$group->group('/{id:\d\d\d\d-\d\d-\d\d}', function (RouteCollectorProxy $group) {
|
||||||
|
$group->get('', WorkingHoursController::class . ':read');
|
||||||
|
$group->put('', WorkingHoursController::class . ':update');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
|
||||||
|
$errorMiddleware->setDefaultErrorHandler(new ErrorHandler($app));
|
||||||
|
|
||||||
|
$app->run();
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
],
|
||||||
|
"description": "Angabe des Datum im Request-Body werden ignoriert",
|
||||||
|
"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": {
|
||||||
|
"workingDay": {
|
||||||
|
"description": "Datum eines Arbeitstages",
|
||||||
|
"type": "string",
|
||||||
|
"format": "date",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"workingTime": {
|
||||||
|
"description": "Arbeitszeit des Tages",
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "[0-5]\\d:[0-5]\\d:[0-5]\\d",
|
||||||
|
"example": "08:01:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 665 B |
Binary file not shown.
|
After Width: | Height: | Size: 628 B |
@@ -0,0 +1,60 @@
|
|||||||
|
<!-- HTML for static distribution bundle build -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Swagger UI</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="swagger-ui.css" >
|
||||||
|
<link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" />
|
||||||
|
<link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" />
|
||||||
|
<style>
|
||||||
|
html
|
||||||
|
{
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: -moz-scrollbars-vertical;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*:before,
|
||||||
|
*:after
|
||||||
|
{
|
||||||
|
box-sizing: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
body
|
||||||
|
{
|
||||||
|
margin:0;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
|
||||||
|
<script src="swagger-ui-bundle.js"> </script>
|
||||||
|
<script src="swagger-ui-standalone-preset.js"> </script>
|
||||||
|
<script>
|
||||||
|
window.onload = function() {
|
||||||
|
// Begin Swagger UI call region
|
||||||
|
const ui = SwaggerUIBundle({
|
||||||
|
url: "../openapi.json",
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
deepLinking: true,
|
||||||
|
presets: [
|
||||||
|
SwaggerUIBundle.presets.apis,
|
||||||
|
SwaggerUIStandalonePreset
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
SwaggerUIBundle.plugins.DownloadUrl
|
||||||
|
],
|
||||||
|
layout: "StandaloneLayout"
|
||||||
|
})
|
||||||
|
// End Swagger UI call region
|
||||||
|
|
||||||
|
window.ui = ui
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-US">
|
||||||
|
<title>Swagger UI: OAuth2 Redirect</title>
|
||||||
|
<body onload="run()">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
function run () {
|
||||||
|
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
||||||
|
var sentState = oauth2.state;
|
||||||
|
var redirectUrl = oauth2.redirectUrl;
|
||||||
|
var isValid, qp, arr;
|
||||||
|
|
||||||
|
if (/code|token|error/.test(window.location.hash)) {
|
||||||
|
qp = window.location.hash.substring(1);
|
||||||
|
} else {
|
||||||
|
qp = location.search.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
arr = qp.split("&")
|
||||||
|
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})
|
||||||
|
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
||||||
|
function (key, value) {
|
||||||
|
return key === "" ? value : decodeURIComponent(value)
|
||||||
|
}
|
||||||
|
) : {}
|
||||||
|
|
||||||
|
isValid = qp.state === sentState
|
||||||
|
|
||||||
|
if ((
|
||||||
|
oauth2.auth.schema.get("flow") === "accessCode"||
|
||||||
|
oauth2.auth.schema.get("flow") === "authorizationCode"
|
||||||
|
) && !oauth2.auth.code) {
|
||||||
|
if (!isValid) {
|
||||||
|
oauth2.errCb({
|
||||||
|
authId: oauth2.auth.name,
|
||||||
|
source: "auth",
|
||||||
|
level: "warning",
|
||||||
|
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qp.code) {
|
||||||
|
delete oauth2.state;
|
||||||
|
oauth2.auth.code = qp.code;
|
||||||
|
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
||||||
|
} else {
|
||||||
|
let oauthErrorMsg
|
||||||
|
if (qp.error) {
|
||||||
|
oauthErrorMsg = "["+qp.error+"]: " +
|
||||||
|
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
||||||
|
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2.errCb({
|
||||||
|
authId: oauth2.auth.name,
|
||||||
|
source: "auth",
|
||||||
|
level: "error",
|
||||||
|
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
||||||
|
}
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
|
|||||||
|
parameters:
|
||||||
|
level: 6
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
- tests/unit
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Controller;
|
||||||
|
|
||||||
|
use JsonSerializable;
|
||||||
|
use PDO;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Slim\Psr7\Response;
|
||||||
|
|
||||||
|
abstract class AbstractController
|
||||||
|
{
|
||||||
|
|
||||||
|
protected PDO $databases;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WorkingHoursController constructor.
|
||||||
|
*
|
||||||
|
* @param ContainerInterface $container
|
||||||
|
*
|
||||||
|
* @throws NotDatabasesException
|
||||||
|
*/
|
||||||
|
public function __construct(ContainerInterface $container)
|
||||||
|
{
|
||||||
|
if ($container->has('databases') === false) {
|
||||||
|
throw new NotDatabasesException('Datenbank ist nicht Vorhanden');
|
||||||
|
}
|
||||||
|
$this->databases = $container->get('databases');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Response $response
|
||||||
|
* @param JsonSerializable|JsonSerializable[] $data
|
||||||
|
* @param int $status_code
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
protected function printResponse(Response $response, mixed $data, int $status_code): Response {
|
||||||
|
$payload = json_encode($data);
|
||||||
|
|
||||||
|
$response->getBody()->write($payload);
|
||||||
|
|
||||||
|
return $response
|
||||||
|
->withHeader('Content-Type', 'application/json')
|
||||||
|
->withStatus($status_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php /** @noinspection PhpMissingFieldTypeInspection */
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Controller;
|
||||||
|
|
||||||
|
use Slim\Exception\HttpSpecializedException;
|
||||||
|
|
||||||
|
class HttpConflictRequestException extends HttpSpecializedException
|
||||||
|
{
|
||||||
|
/** @var int */
|
||||||
|
protected $code = 409;
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
protected $message = 'Conflict.';
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
protected $title = '409 Conflict';
|
||||||
|
|
||||||
|
/** @var string */
|
||||||
|
protected $description = 'The 409 (Conflict) status code indicates that the request could not be completed due to a conflict with the current state of the target resource.';
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Controller;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
class NotDatabasesException extends Exception
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Controller;
|
||||||
|
|
||||||
|
use DateInterval;
|
||||||
|
use DateTime;
|
||||||
|
use Exception;
|
||||||
|
use Fig\Http\Message\StatusCodeInterface;
|
||||||
|
use Slim\Exception\HttpBadRequestException;
|
||||||
|
use Slim\Exception\HttpInternalServerErrorException;
|
||||||
|
use Slim\Exception\HttpNotFoundException;
|
||||||
|
use Slim\Psr7\Request;
|
||||||
|
use Slim\Psr7\Response;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordAlreadyExistException;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursRepository;
|
||||||
|
|
||||||
|
class WorkingHoursController extends AbstractController
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param Response $response
|
||||||
|
* @param mixed[] $args
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*
|
||||||
|
* @throws HttpBadRequestException
|
||||||
|
* @throws HttpNotFoundException
|
||||||
|
*/
|
||||||
|
public function update(Request $request, Response $response, array $args): Response
|
||||||
|
{
|
||||||
|
$body = $request->getParsedBody();
|
||||||
|
$repository = new WorkingHoursRepository($this->databases);
|
||||||
|
$model = $this->buildModel($request, $args['id'], $body['workingTime']);
|
||||||
|
try {
|
||||||
|
$repository->update($model);
|
||||||
|
} catch (RepositoryRecordNotFoundException $exception) {
|
||||||
|
throw new HttpNotFoundException($request, 'Der Eintrag ist nicht vorhanden.', $exception);
|
||||||
|
}
|
||||||
|
return $this->printResponse($response, $model, StatusCodeInterface::STATUS_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param Response $response
|
||||||
|
* @param mixed[] $args
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*
|
||||||
|
* @throws HttpInternalServerErrorException
|
||||||
|
* @noinspection PhpUnusedParameterInspection
|
||||||
|
*/
|
||||||
|
public function browse(Request $request, Response $response, array $args): Response
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursRepository($this->databases);
|
||||||
|
try {
|
||||||
|
return $this->printResponse($response, $repository->findAll(), StatusCodeInterface::STATUS_OK);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
throw new HttpInternalServerErrorException($request, 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param Response $response
|
||||||
|
* @param mixed[] $args
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*
|
||||||
|
* @throws HttpNotFoundException
|
||||||
|
* @throws HttpInternalServerErrorException
|
||||||
|
*/
|
||||||
|
public function read(Request $request, Response $response, array $args): Response
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursRepository($this->databases);
|
||||||
|
try {
|
||||||
|
return $this->printResponse($response, $repository->findByKey($args['id']), StatusCodeInterface::STATUS_OK);
|
||||||
|
} catch (RepositoryRecordNotFoundException $exception) {
|
||||||
|
throw new HttpNotFoundException($request, 'Der Eintrag ist nicht vorhanden.', $exception);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
throw new HttpInternalServerErrorException($request, 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param Response $response
|
||||||
|
* @param mixed[] $args
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*
|
||||||
|
* @throws HttpBadRequestException
|
||||||
|
* @throws HttpConflictRequestException
|
||||||
|
* @throws HttpInternalServerErrorException
|
||||||
|
* @noinspection PhpUnusedParameterInspection
|
||||||
|
*/
|
||||||
|
public function creat(Request $request, Response $response, array $args): Response
|
||||||
|
{
|
||||||
|
$body = $request->getParsedBody();
|
||||||
|
$repository = new WorkingHoursRepository($this->databases);
|
||||||
|
$model = $this->buildModel($request, $body['workingDay'], $body['workingTime']);
|
||||||
|
try {
|
||||||
|
$repository->insert($model);
|
||||||
|
} catch (RepositoryRecordAlreadyExistException $exception) {
|
||||||
|
throw new HttpConflictRequestException($request, 'Der Eintrag ist schon vorhanden.', $exception);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
throw new HttpInternalServerErrorException($request, 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception);
|
||||||
|
}
|
||||||
|
return $this->printResponse($response, $model, StatusCodeInterface::STATUS_CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param string $date
|
||||||
|
* @param string $time
|
||||||
|
*
|
||||||
|
* @return WorkingHours
|
||||||
|
* @throws HttpBadRequestException
|
||||||
|
*/
|
||||||
|
protected function buildModel(Request $request, string $date, string $time): WorkingHours
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$workingDay = new DateTime($date);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
throw new HttpBadRequestException($request, 'Der Wert für das Datum ist falsch', $exception);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$workingTime = new DateInterval('P0000-00-00T' . $time);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
throw new HttpBadRequestException($request, 'Der Wert für die Zeit ist falsch', $exception);
|
||||||
|
}
|
||||||
|
return (new WorkingHours())->setWorkingDay($workingDay)->setWorkingTime($workingTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Controller;
|
||||||
|
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
use Fig\Http\Message\StatusCodeInterface;
|
||||||
|
use Slim\Exception\HttpInternalServerErrorException;
|
||||||
|
use Slim\Psr7\Request;
|
||||||
|
use Slim\Psr7\Response;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursMonthlyViewRepository;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursWeeklyViewRepository;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursYearlyViewRepository;
|
||||||
|
|
||||||
|
class WorkingHoursViewController extends AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param Response $response
|
||||||
|
* @param mixed[] $args
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*
|
||||||
|
* @throws HttpInternalServerErrorException
|
||||||
|
* @noinspection PhpUnusedParameterInspection
|
||||||
|
*/
|
||||||
|
public function browseWeekly(Request $request, Response $response, array $args): Response
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursWeeklyViewRepository($this->databases);
|
||||||
|
try {
|
||||||
|
return $this->printResponse($response, $repository->findAll(), StatusCodeInterface::STATUS_OK);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
throw new HttpInternalServerErrorException($request,
|
||||||
|
'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param Response $response
|
||||||
|
* @param mixed[] $args
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*
|
||||||
|
* @throws HttpInternalServerErrorException
|
||||||
|
* @noinspection PhpUnusedParameterInspection
|
||||||
|
*/
|
||||||
|
public function browseMonthly(Request $request, Response $response, array $args): Response
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursMonthlyViewRepository($this->databases);
|
||||||
|
try {
|
||||||
|
return $this->printResponse($response, $repository->findAll(), StatusCodeInterface::STATUS_OK);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
throw new HttpInternalServerErrorException($request,
|
||||||
|
'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param Response $response
|
||||||
|
* @param mixed[] $args
|
||||||
|
*
|
||||||
|
* @return Response
|
||||||
|
*
|
||||||
|
* @throws HttpInternalServerErrorException
|
||||||
|
* @noinspection PhpUnusedParameterInspection
|
||||||
|
*/
|
||||||
|
public function browseYearly(Request $request, Response $response, array $args): Response
|
||||||
|
{
|
||||||
|
$repository = new WorkingHoursYearlyViewRepository($this->databases);
|
||||||
|
try {
|
||||||
|
return $this->printResponse($response, $repository->findAll(), StatusCodeInterface::STATUS_OK);
|
||||||
|
} catch (Exception $exception) {
|
||||||
|
throw new HttpInternalServerErrorException($request,
|
||||||
|
'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<?php /** @noinspection PhpUndefinedClassInspection */
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Middleware;
|
||||||
|
|
||||||
|
|
||||||
|
use JetBrains\PhpStorm\ArrayShape;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Slim\App;
|
||||||
|
use Slim\Exception\HttpException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class ErrorHandler
|
||||||
|
{
|
||||||
|
protected App $app;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ErrorHandler constructor.
|
||||||
|
*
|
||||||
|
* @param App $app
|
||||||
|
*/
|
||||||
|
public function __construct(App $app)
|
||||||
|
{
|
||||||
|
$this->app = $app;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Throwable $exception
|
||||||
|
*
|
||||||
|
* @return mixed[]
|
||||||
|
*/
|
||||||
|
#[ArrayShape([
|
||||||
|
'timestamp' => "false|string",
|
||||||
|
'status' => "int",
|
||||||
|
"error" => "string",
|
||||||
|
'message' => "string",
|
||||||
|
'path' => "string"
|
||||||
|
])] protected function buildJsonArray(Throwable $exception): array
|
||||||
|
{
|
||||||
|
$payload = [
|
||||||
|
'timestamp' => date('Y-m-d\TH:m:sP'),
|
||||||
|
'status' => $exception->getCode(),
|
||||||
|
"error" => '',
|
||||||
|
'message' => $exception->getMessage(),
|
||||||
|
'path' => $exception->getFile()
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($exception instanceof HttpException) {
|
||||||
|
$payload['error'] = $exception->getTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ServerRequestInterface $request
|
||||||
|
* @param Throwable $exception
|
||||||
|
* @param bool $displayErrorDetails
|
||||||
|
* @param bool $logErrors
|
||||||
|
* @param bool $logErrorDetails
|
||||||
|
* @param LoggerInterface|null $logger
|
||||||
|
*
|
||||||
|
* @return ResponseInterface
|
||||||
|
*/
|
||||||
|
public function __invoke(
|
||||||
|
ServerRequestInterface $request,
|
||||||
|
Throwable $exception,
|
||||||
|
bool $displayErrorDetails,
|
||||||
|
bool $logErrors,
|
||||||
|
bool $logErrorDetails,
|
||||||
|
?LoggerInterface $logger = null
|
||||||
|
): ResponseInterface
|
||||||
|
{
|
||||||
|
if ($logger !== null && $logErrors === true) {
|
||||||
|
$logger->error($exception->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $this->buildJsonArray($exception);
|
||||||
|
|
||||||
|
$response = $this->app->getResponseFactory()->createResponse($exception->getCode());
|
||||||
|
$response->getBody()->write(
|
||||||
|
json_encode($payload, JSON_UNESCAPED_UNICODE)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Middleware;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||||
|
|
||||||
|
class JsonBodyParserMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
public function process(Request $request, RequestHandler $handler): Response
|
||||||
|
{
|
||||||
|
$contentType = $request->getHeaderLine('Content-Type');
|
||||||
|
|
||||||
|
if (strstr($contentType, 'application/json')) {
|
||||||
|
$contents = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE) {
|
||||||
|
$request = $request->withParsedBody($contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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 RepositoryRecordAlreadyExistException extends RepositoryException
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 RepositoryRecordAlreadyExistException
|
||||||
|
*/
|
||||||
|
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 RepositoryRecordAlreadyExistException
|
||||||
|
* @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 RepositoryRecordAlreadyExistException();
|
||||||
|
} /** @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,35 @@
|
|||||||
|
<?php
|
||||||
|
namespace Helper;
|
||||||
|
|
||||||
|
// here you can define custom actions
|
||||||
|
// all public methods declared in helper class will be available in $I
|
||||||
|
|
||||||
|
use Codeception\Module;
|
||||||
|
|
||||||
|
class Api extends Module
|
||||||
|
{
|
||||||
|
|
||||||
|
const FORMAT_TIME = 'string:regex(/\d+:[0-5]\d:[0-5]\d/)';
|
||||||
|
const FORMAT_DATE = 'string:regex(/\d{4}-\d{2}-\d{2}/)';
|
||||||
|
|
||||||
|
const WORKING_HOURS_JSON_FORMAT = [
|
||||||
|
'workingDay' => self::FORMAT_DATE,
|
||||||
|
'workingTime' => self::FORMAT_TIME,
|
||||||
|
];
|
||||||
|
|
||||||
|
const WORKING_HOURS_VIEW_JSON_FORMAT = [
|
||||||
|
"period" => "string",
|
||||||
|
"periodDesignation" => "string",
|
||||||
|
"totalHours" => self::FORMAT_TIME,
|
||||||
|
"workingDays" => 'integer:>0',
|
||||||
|
"overtime" => self::FORMAT_TIME,
|
||||||
|
];
|
||||||
|
|
||||||
|
const ERROR_JSON_FORMAT = [
|
||||||
|
"timestamp" => "string:date",
|
||||||
|
"status" => "integer",
|
||||||
|
"error" => "string",
|
||||||
|
"message" => "string",
|
||||||
|
"path" => "string",
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
actor: ApiTester
|
||||||
|
modules:
|
||||||
|
enabled:
|
||||||
|
- \Helper\Api
|
||||||
|
- REST:
|
||||||
|
url: http://localhost:8091/
|
||||||
|
depends: PhpBrowser
|
||||||
|
part: Json
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php /** @noinspection PhpUnused */
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\Views;
|
||||||
|
|
||||||
|
use ApiTester;
|
||||||
|
use Codeception\Util\HttpCode;
|
||||||
|
use Helper\Api;
|
||||||
|
|
||||||
|
class BrowseWorkingHoursMonthlyCest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param ApiTester $I
|
||||||
|
*/
|
||||||
|
public function browseWorkingHoursView(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->sendGet('/views/working-hours/monthly');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_VIEW_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php /** @noinspection PhpUnused */
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\Views;
|
||||||
|
|
||||||
|
use ApiTester;
|
||||||
|
use Codeception\Util\HttpCode;
|
||||||
|
use Helper\Api;
|
||||||
|
|
||||||
|
class BrowseWorkingHoursWeeklyCest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param ApiTester $I
|
||||||
|
*/
|
||||||
|
public function browseWorkingHoursView(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->sendGet('/views/working-hours/weekly');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_VIEW_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php /** @noinspection PhpUnused */
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\Views;
|
||||||
|
|
||||||
|
use ApiTester;
|
||||||
|
use Codeception\Util\HttpCode;
|
||||||
|
use Helper\Api;
|
||||||
|
|
||||||
|
class BrowseWorkingHoursYearlyCest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param ApiTester $I
|
||||||
|
*/
|
||||||
|
public function browseWorkingHoursView(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->sendGet('/views/working-hours/yearly');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_VIEW_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php /** @noinspection PhpUnused */
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\WorkingHours;
|
||||||
|
|
||||||
|
use ApiTester;
|
||||||
|
use Codeception\Util\HttpCode;
|
||||||
|
use Helper\Api;
|
||||||
|
|
||||||
|
class BrowseWorkingHoursCest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param ApiTester $I
|
||||||
|
*/
|
||||||
|
public function browseWorkingHours(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->sendGet('/working-hours');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php /** @noinspection PhpUnused */
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\WorkingHours;
|
||||||
|
|
||||||
|
use ApiTester;
|
||||||
|
use Codeception\Util\HttpCode;
|
||||||
|
use Helper\Api;
|
||||||
|
|
||||||
|
class CreateWorkingHoursCest
|
||||||
|
{
|
||||||
|
public function createWorkingHoursWithNewValidRecord(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('accept', 'application/json');
|
||||||
|
$I->haveHttpHeader('content-type', 'application/json');
|
||||||
|
$I->sendPost('/working-hours', [
|
||||||
|
'workingDay' => '2020-04-01',
|
||||||
|
'workingTime' => '08:00:00',
|
||||||
|
]);
|
||||||
|
$I->seeResponseCodeIs(HttpCode::CREATED);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tests
|
||||||
|
public function createWorkingHoursWithNewInvalidRecord(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('accept', 'application/json');
|
||||||
|
$I->haveHttpHeader('content-type', 'application/json');
|
||||||
|
$I->sendPost('/working-hours', [
|
||||||
|
'workingDay' => '2020-04-01',
|
||||||
|
'workingTime' => 8.0,
|
||||||
|
]);
|
||||||
|
$I->seeResponseCodeIs(HttpCode::BAD_REQUEST);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// tests
|
||||||
|
public function createWorkingHoursWithExistingValidRecord(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('accept', 'application/json');
|
||||||
|
$I->haveHttpHeader('content-type', 'application/json');
|
||||||
|
$I->sendPost('/working-hours', [
|
||||||
|
'workingDay' => '2020-01-15',
|
||||||
|
'workingTime' => '08:00:00',
|
||||||
|
]);
|
||||||
|
$I->seeResponseCodeIs(HttpCode::CONFLICT);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php /** @noinspection PhpUnused */
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\WorkingHours;
|
||||||
|
|
||||||
|
use ApiTester;
|
||||||
|
use Codeception\Util\HttpCode;
|
||||||
|
use Helper\Api;
|
||||||
|
|
||||||
|
class ReadWorkingHoursCest
|
||||||
|
{
|
||||||
|
public function readExistingWorkingHours(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->sendGet('/working-hours/2020-01-15');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function readNotExistingWorkingHours(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->sendGet('/working-hours/2020-04-15');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::NOT_FOUND);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php /** @noinspection PhpUnused */
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\WorkingHours;
|
||||||
|
|
||||||
|
use ApiTester;
|
||||||
|
use Codeception\Util\HttpCode;
|
||||||
|
use Helper\Api;
|
||||||
|
|
||||||
|
class UpdateWorkingHoursCest
|
||||||
|
{
|
||||||
|
public function updateWorkingHoursWithExistingValidRecord(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('accept', 'application/json');
|
||||||
|
$I->haveHttpHeader('content-type', 'application/json');
|
||||||
|
$I->sendPut('/working-hours/2020-01-15', [
|
||||||
|
'workingDay' => '2020-01-15',
|
||||||
|
'workingTime' => '10:00:00',
|
||||||
|
]);
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateWorkingHoursWithNewValidRecord(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('accept', 'application/json');
|
||||||
|
$I->haveHttpHeader('content-type', 'application/json');
|
||||||
|
$I->sendPut('/working-hours/2020-04-15', [
|
||||||
|
'workingDay' => '2020-04-15',
|
||||||
|
'workingTime' => '10:00:00',
|
||||||
|
]);
|
||||||
|
$I->seeResponseCodeIs(HttpCode::NOT_FOUND);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateWorkingHoursWithNewInvalidRecord(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('accept', 'application/json');
|
||||||
|
$I->haveHttpHeader('content-type', 'application/json');
|
||||||
|
$I->sendPut('/working-hours/2020-01-15', [
|
||||||
|
'workingDay' => '2020-01-15',
|
||||||
|
'workingTime' => 8.0,
|
||||||
|
]);
|
||||||
|
$I->seeResponseCodeIs(HttpCode::BAD_REQUEST);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Controller;
|
||||||
|
|
||||||
|
use Codeception\Test\Unit;
|
||||||
|
use Exception;
|
||||||
|
use PDO;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
use Slim\Exception\HttpBadRequestException;
|
||||||
|
use Slim\Exception\HttpInternalServerErrorException;
|
||||||
|
use Slim\Exception\HttpNotFoundException;
|
||||||
|
use Slim\Psr7\Request;
|
||||||
|
use Slim\Psr7\Response;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Controller\HttpConflictRequestException;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Controller\NotDatabasesException;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursController;
|
||||||
|
|
||||||
|
class WorkingHoursControllerTest extends Unit
|
||||||
|
{
|
||||||
|
|
||||||
|
const EXISTING_DATE = '2020-01-28';
|
||||||
|
const EXISTING_INTERVAL = '07:20:00';
|
||||||
|
const NEW_DATE = '2020-04-01';
|
||||||
|
const NEW_INTERVAL = '07:59:00';
|
||||||
|
|
||||||
|
protected ContainerInterface $container;
|
||||||
|
protected Request $request;
|
||||||
|
protected Response $response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function _before(): void
|
||||||
|
{
|
||||||
|
parent::_before();
|
||||||
|
/** @noinspection SpellCheckingInspection */
|
||||||
|
$this->container = $this->makeEmpty(ContainerInterface::class, [
|
||||||
|
'has' => true,
|
||||||
|
'get' => new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass')
|
||||||
|
]);
|
||||||
|
$this->request = $this->makeEmpty(Request::class, [
|
||||||
|
'getParsedBody' => [
|
||||||
|
'workingDay' => self::EXISTING_DATE,
|
||||||
|
'workingTime' => self::EXISTING_INTERVAL,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
$this->response = $this->makeEmpty(Response::class, [
|
||||||
|
'getBody' => $this->makeEmpty(StreamInterface::class, [
|
||||||
|
'write' => function (mixed $data) {
|
||||||
|
$this->assertIsString($data);
|
||||||
|
$this->assertJson($data);
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
'withHeader' => $this->makeEmpty(Response::class, [
|
||||||
|
'withStatus' => $this->makeEmpty(Response::class),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NotDatabasesException
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testConstructWithNonDatabase(): void
|
||||||
|
{
|
||||||
|
$this->container = $this->makeEmpty(ContainerInterface::class, [
|
||||||
|
'has' => false
|
||||||
|
]);
|
||||||
|
$this->expectException(NotDatabasesException::class);
|
||||||
|
new WorkingHoursController($this->container);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NotDatabasesException
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testBrowse(): void
|
||||||
|
{
|
||||||
|
$controller = new WorkingHoursController($this->container);
|
||||||
|
$response = $controller->browse($this->request, $this->response, []);
|
||||||
|
$this->assertInstanceOf(Response::class, $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NotDatabasesException
|
||||||
|
* @throws HttpBadRequestException
|
||||||
|
* @throws HttpNotFoundException
|
||||||
|
*/
|
||||||
|
public function testUpdateExistRecord(): void
|
||||||
|
{
|
||||||
|
$controller = new WorkingHoursController($this->container);
|
||||||
|
$response = $controller->update($this->request, $this->response, [
|
||||||
|
'id' => self::EXISTING_DATE
|
||||||
|
]);
|
||||||
|
$this->assertInstanceOf(Response::class, $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NotDatabasesException
|
||||||
|
* @throws HttpBadRequestException
|
||||||
|
* @throws HttpNotFoundException
|
||||||
|
*/
|
||||||
|
public function testUpdateNotExistRecord(): void
|
||||||
|
{
|
||||||
|
$controller = new WorkingHoursController($this->container);
|
||||||
|
$this->expectException(HttpNotFoundException::class);
|
||||||
|
$controller->update($this->request, $this->response, [
|
||||||
|
'id' => self::NEW_DATE
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[][]
|
||||||
|
*/
|
||||||
|
public function invalidDataProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
self::NEW_DATE,
|
||||||
|
'07:59'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'2020-13-33',
|
||||||
|
self::NEW_INTERVAL
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $date
|
||||||
|
* @param string $time
|
||||||
|
*
|
||||||
|
* @dataProvider invalidDataProvider
|
||||||
|
*
|
||||||
|
* @throws HttpBadRequestException
|
||||||
|
* @throws HttpConflictRequestException
|
||||||
|
* @throws HttpInternalServerErrorException
|
||||||
|
* @throws NotDatabasesException
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testCreatInvalidData(string $date, string $time): void
|
||||||
|
{
|
||||||
|
$this->request = $this->makeEmpty(Request::class, [
|
||||||
|
'getParsedBody' => [
|
||||||
|
'workingDay' => $date,
|
||||||
|
'workingTime' => $time,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
$controller = new WorkingHoursController($this->container);
|
||||||
|
$this->expectException(HttpBadRequestException::class);
|
||||||
|
$controller->creat($this->request, $this->response, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws HttpBadRequestException
|
||||||
|
* @throws NotDatabasesException
|
||||||
|
* @throws HttpInternalServerErrorException
|
||||||
|
* @throws HttpConflictRequestException
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testCreatNewRecord(): void
|
||||||
|
{
|
||||||
|
$this->request = $this->makeEmpty(Request::class, [
|
||||||
|
'getParsedBody' => [
|
||||||
|
'workingDay' => self::NEW_DATE,
|
||||||
|
'workingTime' => self::NEW_INTERVAL,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
$controller = new WorkingHoursController($this->container);
|
||||||
|
$response = $controller->creat($this->request, $this->response, []);
|
||||||
|
$this->assertInstanceOf(Response::class, $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws HttpBadRequestException
|
||||||
|
* @throws HttpConflictRequestException
|
||||||
|
* @throws HttpInternalServerErrorException
|
||||||
|
* @throws NotDatabasesException
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testCreatExistRecord(): void
|
||||||
|
{
|
||||||
|
$this->request = $this->makeEmpty(Request::class, [
|
||||||
|
'getParsedBody' => [
|
||||||
|
'workingDay' => self::EXISTING_DATE,
|
||||||
|
'workingTime' => self::EXISTING_INTERVAL,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
$controller = new WorkingHoursController($this->container);
|
||||||
|
$this->expectException(HttpConflictRequestException::class);
|
||||||
|
$controller->creat($this->request, $this->response, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws HttpInternalServerErrorException
|
||||||
|
* @throws HttpNotFoundException
|
||||||
|
* @throws NotDatabasesException
|
||||||
|
*/
|
||||||
|
public function testReadExistRecord(): void
|
||||||
|
{
|
||||||
|
$controller = new WorkingHoursController($this->container);
|
||||||
|
$response = $controller->read($this->request, $this->response, [
|
||||||
|
'id' => self::EXISTING_DATE
|
||||||
|
]);
|
||||||
|
$this->assertInstanceOf(Response::class, $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws HttpInternalServerErrorException
|
||||||
|
* @throws HttpNotFoundException
|
||||||
|
* @throws NotDatabasesException
|
||||||
|
*/
|
||||||
|
public function testReadNotExistRecord(): void
|
||||||
|
{
|
||||||
|
$controller = new WorkingHoursController($this->container);
|
||||||
|
$this->expectException(HttpNotFoundException::class);
|
||||||
|
$controller->read($this->request, $this->response, [
|
||||||
|
'id' => self::NEW_DATE
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Controller;
|
||||||
|
|
||||||
|
use Codeception\Test\Unit;
|
||||||
|
use Exception;
|
||||||
|
use PDO;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
use Slim\Psr7\Request;
|
||||||
|
use Slim\Psr7\Response;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Controller\NotDatabasesException;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursViewController;
|
||||||
|
|
||||||
|
class WorkingHoursViewControllerTest extends Unit
|
||||||
|
{
|
||||||
|
protected ContainerInterface $container;
|
||||||
|
protected Request $request;
|
||||||
|
protected Response $response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function _before(): void
|
||||||
|
{
|
||||||
|
parent::_before();
|
||||||
|
/** @noinspection SpellCheckingInspection */
|
||||||
|
$this->container = $this->makeEmpty(ContainerInterface::class, [
|
||||||
|
'has' => true,
|
||||||
|
'get' => new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass')
|
||||||
|
]);
|
||||||
|
$this->request = $this->makeEmpty(Request::class, []);
|
||||||
|
$this->response = $this->makeEmpty(Response::class, [
|
||||||
|
'getBody' => $this->makeEmpty(StreamInterface::class, [
|
||||||
|
'write' => function (mixed $data) {
|
||||||
|
$this->assertIsString($data);
|
||||||
|
$this->assertJson($data);
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
'withHeader' => $this->makeEmpty(Response::class, [
|
||||||
|
'withStatus' => $this->makeEmpty(Response::class),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws NotDatabasesException
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testConstructWithNonDatabase(): void
|
||||||
|
{
|
||||||
|
$this->container = $this->makeEmpty(ContainerInterface::class, [
|
||||||
|
'has' => false
|
||||||
|
]);
|
||||||
|
$this->expectException(NotDatabasesException::class);
|
||||||
|
new WorkingHoursViewController($this->container);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testBrowseMonthly(): void
|
||||||
|
{
|
||||||
|
$controller = new WorkingHoursViewController($this->container);
|
||||||
|
$response = $controller->browseMonthly($this->request, $this->response, []);
|
||||||
|
$this->assertInstanceOf(Response::class, $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testBrowseYearly(): void
|
||||||
|
{
|
||||||
|
$controller = new WorkingHoursViewController($this->container);
|
||||||
|
$response = $controller->browseYearly($this->request, $this->response, []);
|
||||||
|
$this->assertInstanceOf(Response::class, $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testBrowseWeekly(): void
|
||||||
|
{
|
||||||
|
$controller = new WorkingHoursViewController($this->container);
|
||||||
|
$response = $controller->browseWeekly($this->request, $this->response, []);
|
||||||
|
$this->assertInstanceOf(Response::class, $response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Middleware;
|
||||||
|
|
||||||
|
use Codeception\Stub\Expected;
|
||||||
|
use Codeception\Test\Unit;
|
||||||
|
use Exception;
|
||||||
|
use Psr\Http\Message\ResponseFactoryInterface;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Slim\App;
|
||||||
|
use Slim\Exception\HttpException;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Middleware\ErrorHandler;
|
||||||
|
|
||||||
|
class ErrorHandlerTest extends Unit
|
||||||
|
{
|
||||||
|
|
||||||
|
protected App $app;
|
||||||
|
protected ServerRequestInterface $request;
|
||||||
|
protected Exception $exception;
|
||||||
|
protected LoggerInterface $logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
protected function _before(): void
|
||||||
|
{
|
||||||
|
parent::_before();
|
||||||
|
$this->app = $this->makeEmpty(App::class, [
|
||||||
|
'getResponseFactory' => $this->makeEmpty(ResponseFactoryInterface::class, [
|
||||||
|
'createResponse' => $this->makeEmpty(ResponseInterface::class, [
|
||||||
|
'getBody' => $this->makeEmpty(StreamInterface::class, [
|
||||||
|
'write' => function (mixed $data) {
|
||||||
|
$this->assertIsString($data);
|
||||||
|
$this->assertJson($data);
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
$this->request = $this->makeEmpty(ServerRequestInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testUseWithLogger(): void
|
||||||
|
{
|
||||||
|
$this->exception = $this->make(Exception::class, [
|
||||||
|
'message' => '',
|
||||||
|
'code' => 400,
|
||||||
|
'file' => '/path(to/file',
|
||||||
|
]);
|
||||||
|
$this->logger = $this->makeEmpty(LoggerInterface::class, [
|
||||||
|
'error' => Expected::once(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$middleWare = new ErrorHandler($this->app);
|
||||||
|
$middleWare($this->request, $this->exception, true, true, true, $this->logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testUseWithHttpException(): void
|
||||||
|
{
|
||||||
|
$this->exception = $this->make(HttpException::class, [
|
||||||
|
'message' => '',
|
||||||
|
'code' => 400,
|
||||||
|
'file' => '/path(to/file',
|
||||||
|
'getTitle' => Expected::once('The Title'),
|
||||||
|
]);
|
||||||
|
$this->logger = $this->makeEmpty(LoggerInterface::class, []);
|
||||||
|
|
||||||
|
$middleWare = new ErrorHandler($this->app);
|
||||||
|
$middleWare($this->request, $this->exception, true, true, true, $this->logger);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testUseWithGeneralException(): void
|
||||||
|
{
|
||||||
|
$this->exception = $this->make(Exception::class, [
|
||||||
|
'message' => '',
|
||||||
|
'code' => 400,
|
||||||
|
'file' => '/path(to/file',
|
||||||
|
]);
|
||||||
|
$this->logger = $this->makeEmpty(LoggerInterface::class, []);
|
||||||
|
|
||||||
|
$middleWare = new ErrorHandler($this->app);
|
||||||
|
$middleWare($this->request, $this->exception, true, true, true, $this->logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,193 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Repositories;
|
||||||
|
|
||||||
|
use Codeception\Test\Unit;
|
||||||
|
use DateInterval;
|
||||||
|
use DateTime;
|
||||||
|
use Exception;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use PDO;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\ModelInterface;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordAlreadyExistException;
|
||||||
|
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 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(RepositoryRecordAlreadyExistException::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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
volumes:
|
||||||
|
logs:
|
||||||
|
driver: local
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
build: ./api/
|
||||||
|
environment:
|
||||||
|
docker: "true"
|
||||||
|
ports:
|
||||||
|
- 8090:80
|
||||||
|
volumes:
|
||||||
|
- ./api:/var/www
|
||||||
|
- logs:/var/www/logs
|
||||||
|
test-api:
|
||||||
|
build: ./api/
|
||||||
|
environment:
|
||||||
|
docker: "true"
|
||||||
|
DATABASES_HOST: psql.torsten-hettstedt.net
|
||||||
|
DATABASES_NAME: testdb
|
||||||
|
DATABASES_USER: bruce
|
||||||
|
DATABASES_PASS: mypass
|
||||||
|
ports:
|
||||||
|
- 8091: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