Compare commits
88
Commits
7e8ed66cb7
..
1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12ed91a6da | ||
|
|
cc9941d1d3 | ||
|
|
2b2753e009 | ||
|
|
735b195c06 | ||
|
|
0bd8932fa1 | ||
|
|
114e31029c | ||
|
|
de0535be32 | ||
|
|
b74e9bbd69 | ||
|
|
338c3ce5e4 | ||
|
|
1b4983a06f | ||
|
|
74193f3b46 | ||
|
|
2e0e6db55a | ||
|
|
c528944746 | ||
|
|
05d46ec407 | ||
|
|
7b48e7e9e2 | ||
|
|
343f12b605 | ||
|
|
6203f98e93 | ||
|
|
87f3bb3f0e | ||
|
|
8d00e0c104 | ||
|
|
3fe73f3b10 | ||
|
|
b5af0c5a54 | ||
|
|
e29a6dcab9 | ||
|
|
8ee9666250 | ||
|
|
edcb80421b | ||
|
|
16e05ce819 | ||
|
|
5c15580738 | ||
|
|
0a5e40d7cb | ||
|
|
ae7491f002 | ||
|
|
8296a2a281 | ||
|
|
c711e0ae24 | ||
|
|
74fd96cf25 | ||
|
|
9686a8505e | ||
|
|
bc83fc8dd1 | ||
|
|
9d5af9a0fb | ||
|
|
7f8a50799f | ||
|
|
365c2f041c | ||
|
|
89ad394773 | ||
|
|
881e7362f8 | ||
|
|
c8d9bc040e | ||
|
|
e3ef53b119 | ||
|
|
f78fdfbbd5 | ||
|
|
02771828c9 | ||
|
|
5a88ff3c6f | ||
|
|
f36324bfbd | ||
|
|
9b99c6a4dc | ||
|
|
fe3a4f69bf | ||
|
|
31a3f4cc01 | ||
|
|
0a59cb60db | ||
|
|
9430f3c74c | ||
|
|
2b31e17e14 | ||
|
|
9e35ab3b07 | ||
|
|
056bf82c7b | ||
|
|
be54a659c9 | ||
|
|
8086b86eb2 | ||
|
|
3e2865dbe7 | ||
|
|
60eb1f92e8 | ||
|
|
9a542c9108 | ||
|
|
a035ea9648 | ||
|
|
46f3a6491d | ||
|
|
e070db18b5 | ||
|
|
a6675605e4 | ||
|
|
520db45d6d | ||
|
|
2d3a1074f6 | ||
|
|
a1e8b1aa51 | ||
|
|
f25c9d8994 | ||
|
|
3854737622 | ||
|
|
d16a4e427d | ||
|
|
4871ff14dc | ||
|
|
ac99e079ee | ||
|
|
09ffadc596 | ||
|
|
4da5c1242c | ||
|
|
e0b8deae8b | ||
|
|
d3df8bdc7e | ||
|
|
b53b1cdd3c | ||
|
|
0054ab5afc | ||
|
|
d11070ffbb | ||
|
|
5cdc11d9df | ||
|
|
add7895191 | ||
|
|
20fec2dc07 | ||
|
|
123f5eef81 | ||
|
|
0e98a933e1 | ||
|
|
c2676abfa2 | ||
|
|
675c5cb9ae | ||
|
|
c8219d9c88 | ||
|
|
edc7ed5fe6 | ||
|
|
e113a80efc | ||
|
|
397416cd31 | ||
|
|
d0799eaae2 |
@@ -0,0 +1,10 @@
|
|||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = false
|
||||||
+1
-1
@@ -105,4 +105,4 @@ Temporary Items
|
|||||||
/api/vendor/
|
/api/vendor/
|
||||||
/api/tests/_*
|
/api/tests/_*
|
||||||
/api/tests/*.suite.yml
|
/api/tests/*.suite.yml
|
||||||
/api/html/swagger/
|
/api/.env
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
## Datenbank-Einstellungen
|
||||||
|
DATABASES_HOST=psql.domain.tld
|
||||||
|
DATABASES_NAME=db
|
||||||
|
DATABASES_USER=user
|
||||||
|
DATABASES_PASS=1234
|
||||||
|
|
||||||
|
## CORS Einstellungen
|
||||||
|
CORS_ALLOW_ORIGIN_REGEX=/https?:\/\/localhost:\d+/
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
FROM php:8.0-apache
|
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
|
WORKDIR /var/www
|
||||||
|
|
||||||
RUN a2enmod rewrite
|
RUN a2enmod rewrite
|
||||||
@@ -17,3 +17,7 @@ modules:
|
|||||||
dump: tests/_data/dump.sql
|
dump: tests/_data/dump.sql
|
||||||
cleanup: true # reload dump between tests
|
cleanup: true # reload dump between tests
|
||||||
populate: true # load dump before all tests
|
populate: true # load dump before all tests
|
||||||
|
coverage:
|
||||||
|
enabled: true
|
||||||
|
include:
|
||||||
|
- src/*
|
||||||
+6
-2
@@ -19,6 +19,7 @@
|
|||||||
"slim/slim": "^4.7.1",
|
"slim/slim": "^4.7.1",
|
||||||
"vlucas/phpdotenv": "^4.2",
|
"vlucas/phpdotenv": "^4.2",
|
||||||
"slim/psr7": "^1.3",
|
"slim/psr7": "^1.3",
|
||||||
|
"php-di/slim-bridge": "^3.1.0",
|
||||||
"jetbrains/phpstorm-attributes": "^1.0.0",
|
"jetbrains/phpstorm-attributes": "^1.0.0",
|
||||||
"myclabs/php-enum": "^1.8.0"
|
"myclabs/php-enum": "^1.8.0"
|
||||||
},
|
},
|
||||||
@@ -27,7 +28,8 @@
|
|||||||
"codeception/codeception": "^4.1.18",
|
"codeception/codeception": "^4.1.18",
|
||||||
"codeception/module-phpbrowser": "^1.0.0",
|
"codeception/module-phpbrowser": "^1.0.0",
|
||||||
"codeception/module-asserts": "^1.0.0",
|
"codeception/module-asserts": "^1.0.0",
|
||||||
"codeception/module-db": "^1.1.0"
|
"codeception/module-db": "^1.1.0",
|
||||||
|
"codeception/module-rest": "^1.2.8"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@@ -36,7 +38,9 @@
|
|||||||
},
|
},
|
||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"TorstenHettstedt\\TimekeepingApi\\Tests\\Unit\\": "tests/unit"
|
"TorstenHettstedt\\TimekeepingApi\\Tests\\Unit\\": "tests/unit",
|
||||||
|
"TorstenHettstedt\\TimekeepingApi\\Tests\\Api\\": "tests/api",
|
||||||
|
"Helper\\": "tests/_support/Helper"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
# absolute physical path to the directory that contains this htaccess file.
|
# absolute physical path to the directory that contains this htaccess file.
|
||||||
# RewriteBase /
|
# RewriteBase /
|
||||||
|
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
RewriteRule ^ index.php [QSA,L]
|
RewriteRule ^ index.php [QSA,L]
|
||||||
</IfModule>
|
</IfModule>
|
||||||
+44
-2
@@ -1,12 +1,54 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
use DI\Container;
|
||||||
use Slim\Factory\AppFactory;
|
use Slim\Factory\AppFactory;
|
||||||
|
use Slim\Routing\RouteCollectorProxy;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Controller\PreflightController;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursController;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursViewController;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Middleware\ErrorHandler;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Middleware\JsonBodyParserMiddleware;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Middleware\OriginAccessControlHandler;
|
||||||
|
|
||||||
|
$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 = AppFactory::create();
|
||||||
|
$app->add(new JsonBodyParserMiddleware());
|
||||||
|
$app->addBodyParsingMiddleware();
|
||||||
|
$app->add(new OriginAccessControlHandler());
|
||||||
|
$app->addRoutingMiddleware();
|
||||||
|
|
||||||
$app->get('/', 'TorstenHettstedt\TimekeepingApi\Controller\HelloWorldController:read');
|
$app->group('/views/working-hours', function (RouteCollectorProxy $group) {
|
||||||
|
$group->options('/weekly', PreflightController::class . ':preflight');
|
||||||
|
$group->get('/weekly', WorkingHoursViewController::class . ':browseWeekly');
|
||||||
|
$group->options('/monthly', PreflightController::class . ':preflight');
|
||||||
|
$group->get('/monthly', WorkingHoursViewController::class . ':browseMonthly');
|
||||||
|
$group->options('/yearly', PreflightController::class . ':preflight');
|
||||||
|
$group->get('/yearly', WorkingHoursViewController::class . ':browseYearly');
|
||||||
|
});
|
||||||
|
|
||||||
$app->addErrorMiddleware(true, true, true);
|
$app->group('/working-hours', function (RouteCollectorProxy $group) {
|
||||||
|
$group->options('', PreflightController::class . ':preflight');
|
||||||
|
$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->options('', PreflightController::class . ':preflight');
|
||||||
|
$group->get('', WorkingHoursController::class . ':read');
|
||||||
|
$group->put('', WorkingHoursController::class . ':update');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
|
||||||
|
$errorMiddleware->setDefaultErrorHandler(new ErrorHandler($app));
|
||||||
|
|
||||||
$app->run();
|
$app->run();
|
||||||
@@ -82,6 +82,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"Eintragungen Arbeitszeiten"
|
"Eintragungen Arbeitszeiten"
|
||||||
],
|
],
|
||||||
|
"description": "Angabe des Datum im Request-Body werden ignoriert",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"$ref": "#/components/parameters/date"
|
"$ref": "#/components/parameters/date"
|
||||||
@@ -167,14 +168,17 @@
|
|||||||
"description": "Eintrag der Arbeitszeit für einen Tag",
|
"description": "Eintrag der Arbeitszeit für einen Tag",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"date": {
|
"workingDay": {
|
||||||
"description": "Datum eines Arbeitstages",
|
"description": "Datum eines Arbeitstages",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date"
|
"format": "date",
|
||||||
|
"required": false
|
||||||
},
|
},
|
||||||
"workingHours": {
|
"workingTime": {
|
||||||
"description": "Arbeitszeit des Tages",
|
"description": "Arbeitszeit des Tages",
|
||||||
"type": "integer"
|
"type": "string",
|
||||||
|
"pattern": "[0-5]\\d:[0-5]\\d:[0-5]\\d",
|
||||||
|
"example": "08:01:00"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
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,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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php /** @noinspection PhpUnused */
|
|
||||||
|
|
||||||
|
|
||||||
namespace TorstenHettstedt\TimekeepingApi\Controller;
|
|
||||||
|
|
||||||
use Slim\Psr7\Response;
|
|
||||||
use Slim\Psr7\Request;
|
|
||||||
|
|
||||||
class HelloWorldController
|
|
||||||
{
|
|
||||||
/** @noinspection PhpUnusedParameterInspection */
|
|
||||||
public function read(Request $request, Response $response, $args): Response
|
|
||||||
{
|
|
||||||
$payload = json_encode('Hallo World');
|
|
||||||
|
|
||||||
$response->getBody()->write($payload);
|
|
||||||
|
|
||||||
return $response
|
|
||||||
->withHeader('Content-Type', 'application/json')
|
|
||||||
->withStatus(200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Controller;
|
||||||
|
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Slim\Psr7\Request;
|
||||||
|
use Slim\Psr7\Response;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Middleware\OriginAccessControlHandler;
|
||||||
|
|
||||||
|
class PreflightController
|
||||||
|
{
|
||||||
|
public function preflight(Request $request, Response $response): ResponseInterface
|
||||||
|
{
|
||||||
|
return (new OriginAccessControlHandler())->originAccessControl($request, $response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,92 @@
|
|||||||
|
<?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)
|
||||||
|
);
|
||||||
|
|
||||||
|
$originAccessControlHandler = new OriginAccessControlHandler();
|
||||||
|
|
||||||
|
return $originAccessControlHandler->originAccessControl($request, $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,58 @@
|
|||||||
|
<?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;
|
||||||
|
use Slim\Routing\RouteContext;
|
||||||
|
|
||||||
|
class OriginAccessControlHandler implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process an incoming server request.
|
||||||
|
*
|
||||||
|
* Processes an incoming server request in order to produce a response.
|
||||||
|
* If unable to produce the response itself, it may delegate to the provided
|
||||||
|
* request handler to do so.
|
||||||
|
*/
|
||||||
|
public function process(Request $request, RequestHandler $handler): Response
|
||||||
|
{
|
||||||
|
$response = $handler->handle($request);
|
||||||
|
return $this->originAccessControl($request, $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function originAccessControl(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
|
||||||
|
$routeContext = RouteContext::fromRequest($request);
|
||||||
|
$routingResults = $routeContext->getRoutingResults();
|
||||||
|
$methods = $routingResults->getAllowedMethods();
|
||||||
|
$requestHeaders = $request->getHeaderLine('Access-Control-Request-Headers');
|
||||||
|
|
||||||
|
if ($this->testRoute($request)) {
|
||||||
|
// Stunden Minuten Sekunden
|
||||||
|
$accessControlMaxAge = 24 * 60 * 60;
|
||||||
|
$response = $response->withHeader('Access-Control-Allow-Origin', $this->buildOrigin($request));
|
||||||
|
$response = $response->withHeader('Access-Control-Allow-Methods', implode(',', $methods));
|
||||||
|
$response = $response->withHeader('Access-Control-Allow-Headers', $requestHeaders);
|
||||||
|
$response = $response->withHeader('Access-Control-Max-Age', (string)$accessControlMaxAge);
|
||||||
|
}
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function testRoute(Request $request): bool
|
||||||
|
{
|
||||||
|
$corsAllowOriginRegex = $_ENV['CORS_ALLOW_ORIGIN_REGEX'] ?? '/https?:\/\/.*:.*/';
|
||||||
|
return preg_match($corsAllowOriginRegex, $this->buildOrigin($request));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildOrigin(Request $request): string
|
||||||
|
{
|
||||||
|
return $request->getHeaderLine('Origin') ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,12 +91,13 @@ class WorkingHoursView implements ModelInterface
|
|||||||
'overtime' => "string"
|
'overtime' => "string"
|
||||||
])] public function jsonSerialize(): array
|
])] public function jsonSerialize(): array
|
||||||
{
|
{
|
||||||
|
$formatOvertime = (($this->getOvertime()->invert === 1) ? '-' : '') . '%H:%I:%S';
|
||||||
return [
|
return [
|
||||||
'period' => $this->getPeriod()->format($this->getPeriodDesignation()->getValue()),
|
'period' => $this->getPeriod()->format($this->getPeriodDesignation()->getValue()),
|
||||||
'periodDesignation' => strtolower($this->getPeriodDesignation()->getKey()),
|
'periodDesignation' => strtolower($this->getPeriodDesignation()->getKey()),
|
||||||
'workingDays' => $this->getWorkingDays(),
|
'workingDays' => $this->getWorkingDays(),
|
||||||
'totalHours' => $this->getTotalHours()->format('%H:%I:%S'),
|
'totalHours' => $this->getTotalHours()->format('%H:%I:%S'),
|
||||||
'overtime' => $this->getOvertime()->format('%H:%I:%S')
|
'overtime' => $this->getOvertime()->format($formatOvertime)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
|
|
||||||
namespace TorstenHettstedt\TimekeepingApi\Repositories;
|
|
||||||
|
|
||||||
|
|
||||||
class RepositoryModelAlreadyExists extends RepositoryException
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Repositories;
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryRecordAlreadyExistException extends RepositoryException
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ interface RepositoryWriterInterface
|
|||||||
/**
|
/**
|
||||||
* @param T $model
|
* @param T $model
|
||||||
*
|
*
|
||||||
* @throws RepositoryModelAlreadyExists
|
* @throws RepositoryRecordAlreadyExistException
|
||||||
*/
|
*/
|
||||||
public function insert(mixed $model): void;
|
public function insert(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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ namespace TorstenHettstedt\TimekeepingApi\Repositories;
|
|||||||
use DateInterval;
|
use DateInterval;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use InvalidArgumentException;
|
||||||
use PDO;
|
use PDO;
|
||||||
use PDOException;
|
use PDOException;
|
||||||
use TorstenHettstedt\TimekeepingApi\Models\ModelInterface;
|
use TorstenHettstedt\TimekeepingApi\Models\ModelInterface;
|
||||||
@@ -43,13 +44,13 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri
|
|||||||
public function findAll(): array
|
public function findAll(): array
|
||||||
{
|
{
|
||||||
$models = [];
|
$models = [];
|
||||||
$stmt = $this->database->prepare('SELECT "Datum" as workingDay, "Arbeitszeit" as workingTime FROM public."Arbeitszeiten"');
|
$stmt = $this->database->prepare('select "Datum" as "workingDay", "Arbeitszeit" as "workingTime" from public."Arbeitszeiten" order by "Datum"');
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
while ($row = $stmt->fetch()) {
|
while ($row = $stmt->fetch()) {
|
||||||
$model = new WorkingHours();
|
$model = new WorkingHours();
|
||||||
$model
|
$model
|
||||||
->setWorkingDay(new DateTime($row['workingday']))
|
->setWorkingDay(new DateTime($row['workingDay']))
|
||||||
->setWorkingTime(new DateInterval('P0000-00-00T'.$row['workingtime']));
|
->setWorkingTime(new DateInterval('P0000-00-00T' . $row['workingTime']));
|
||||||
$models[] = $model;
|
$models[] = $model;
|
||||||
}
|
}
|
||||||
return $models;
|
return $models;
|
||||||
@@ -67,7 +68,7 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri
|
|||||||
*/
|
*/
|
||||||
public function findByKey(mixed $primary_key): WorkingHours
|
public function findByKey(mixed $primary_key): WorkingHours
|
||||||
{
|
{
|
||||||
$stmt = $this->database->prepare('SELECT "Datum" as workingDay, "Arbeitszeit" as workingTime FROM public."Arbeitszeiten" WHERE "Datum" = ?');
|
$stmt = $this->database->prepare('select "Datum" as "workingDay", "Arbeitszeit" as "workingTime" from public."Arbeitszeiten" where "Datum" = ?');
|
||||||
$stmt->bindParam(1, $primary_key);
|
$stmt->bindParam(1, $primary_key);
|
||||||
$stmt->execute();
|
$stmt->execute();
|
||||||
$row = $stmt->fetch();
|
$row = $stmt->fetch();
|
||||||
@@ -76,32 +77,77 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri
|
|||||||
}
|
}
|
||||||
$model = new WorkingHours();
|
$model = new WorkingHours();
|
||||||
$model
|
$model
|
||||||
->setWorkingDay(new DateTime($row['workingday']))
|
->setWorkingDay(new DateTime($row['workingDay']))
|
||||||
->setWorkingTime(new DateInterval('P0000-00-00T'.$row['workingtime']));
|
->setWorkingTime(new DateInterval('P0000-00-00T' . $row['workingTime']));
|
||||||
return $model;
|
return $model;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param WorkingHours|ModelInterface $model
|
* @param WorkingHours|ModelInterface $model
|
||||||
|
*
|
||||||
|
* @throws RepositoryRecordAlreadyExistException
|
||||||
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
public function insert(mixed $model): void
|
public function insert(mixed $model): void
|
||||||
{
|
{
|
||||||
// TODO: Implement insert() method.
|
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
|
* @param WorkingHours|ModelInterface $model
|
||||||
|
*
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
*/
|
*/
|
||||||
public function update(mixed $model): void
|
public function update(mixed $model): void
|
||||||
{
|
{
|
||||||
// TODO: Implement update() method.
|
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
|
* @param WorkingHours|ModelInterface $model
|
||||||
|
*
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
*/
|
*/
|
||||||
public function delete(mixed $model): void
|
public function delete(mixed $model): void
|
||||||
{
|
{
|
||||||
// TODO: Implement delete() method.
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,8 +23,9 @@ DROP VIEW IF EXISTS public."Arbeitszeiten - Jahr";
|
|||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE public."Arbeitszeiten" (
|
CREATE TABLE public."Arbeitszeiten" (
|
||||||
"Datum" date NOT NULL,
|
"Datum" date not null,
|
||||||
"Arbeitszeit" interval
|
"Arbeitszeit" interval(6) null,
|
||||||
|
constraint "Arbeitszeiten_pkey" primary key ("Datum")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?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",
|
||||||
|
];
|
||||||
|
|
||||||
|
const TEST_ORIGIN = 'http://localhost:1234';
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
actor: ApiTester
|
||||||
|
modules:
|
||||||
|
enabled:
|
||||||
|
- \Helper\Api
|
||||||
|
- REST:
|
||||||
|
url: http://localhost:8091/
|
||||||
|
depends: PhpBrowser
|
||||||
|
part: Json
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php /** @noinspection PhpUnused */
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\Views;
|
||||||
|
|
||||||
|
use ApiTester;
|
||||||
|
use Codeception\Util\HttpCode;
|
||||||
|
use Helper\Api;
|
||||||
|
|
||||||
|
class BrowseWorkingHoursMonthlyCest
|
||||||
|
{
|
||||||
|
public static function preflight(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->haveHttpHeader('Access-Control-Request-Method', 'GET');
|
||||||
|
$I->sendOptions('/views/working-hours/monthly');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Methods');
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Headers');
|
||||||
|
$I->seeHttpHeader('Access-Control-Max-Age');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ApiTester $I
|
||||||
|
*/
|
||||||
|
public function browseWorkingHoursView(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->sendGet('/views/working-hours/monthly');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_VIEW_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php /** @noinspection PhpUnused */
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\Views;
|
||||||
|
|
||||||
|
use ApiTester;
|
||||||
|
use Codeception\Util\HttpCode;
|
||||||
|
use Helper\Api;
|
||||||
|
|
||||||
|
class BrowseWorkingHoursWeeklyCest
|
||||||
|
{
|
||||||
|
public static function preflight(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->haveHttpHeader('Access-Control-Request-Method', 'GET');
|
||||||
|
$I->sendOptions('/views/working-hours/weekly');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Methods');
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Headers');
|
||||||
|
$I->seeHttpHeader('Access-Control-Max-Age');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ApiTester $I
|
||||||
|
*/
|
||||||
|
public function browseWorkingHoursView(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->sendGet('/views/working-hours/weekly');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_VIEW_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php /** @noinspection PhpUnused */
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\Views;
|
||||||
|
|
||||||
|
use ApiTester;
|
||||||
|
use Codeception\Util\HttpCode;
|
||||||
|
use Helper\Api;
|
||||||
|
|
||||||
|
class BrowseWorkingHoursYearlyCest
|
||||||
|
{
|
||||||
|
public static function preflight(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->haveHttpHeader('Access-Control-Request-Method', 'GET');
|
||||||
|
$I->sendOptions('/views/working-hours/yearly');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Methods');
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Headers');
|
||||||
|
$I->seeHttpHeader('Access-Control-Max-Age');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ApiTester $I
|
||||||
|
*/
|
||||||
|
public function browseWorkingHoursView(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->sendGet('/views/working-hours/yearly');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_VIEW_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?php /** @noinspection PhpUnused */
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\WorkingHours;
|
||||||
|
|
||||||
|
use ApiTester;
|
||||||
|
use Codeception\Util\HttpCode;
|
||||||
|
use Helper\Api;
|
||||||
|
|
||||||
|
class BrowseWorkingHoursCest
|
||||||
|
{
|
||||||
|
public static function preflight(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->haveHttpHeader('Access-Control-Request-Method', 'GET');
|
||||||
|
$I->sendOptions('/working-hours');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Methods');
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Headers');
|
||||||
|
$I->seeHttpHeader('Access-Control-Max-Age');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ApiTester $I
|
||||||
|
*/
|
||||||
|
public function browseWorkingHours(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->sendGet('/working-hours');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php /** @noinspection PhpUnused */
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\WorkingHours;
|
||||||
|
|
||||||
|
use ApiTester;
|
||||||
|
use Codeception\Util\HttpCode;
|
||||||
|
use Helper\Api;
|
||||||
|
|
||||||
|
class CreateWorkingHoursCest
|
||||||
|
{
|
||||||
|
public static function preflight(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->haveHttpHeader('Access-Control-Request-Method', 'POST');
|
||||||
|
$I->sendOptions('/working-hours');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Methods');
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Headers');
|
||||||
|
$I->seeHttpHeader('Access-Control-Max-Age');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createWorkingHoursWithNewValidRecord(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('accept', 'application/json');
|
||||||
|
$I->haveHttpHeader('content-type', 'application/json');
|
||||||
|
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->sendPost('/working-hours', [
|
||||||
|
'workingDay' => '2020-04-01',
|
||||||
|
'workingTime' => '08:00:00',
|
||||||
|
]);
|
||||||
|
$I->seeResponseCodeIs(HttpCode::CREATED);
|
||||||
|
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$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->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->sendPost('/working-hours', [
|
||||||
|
'workingDay' => '2020-04-01',
|
||||||
|
'workingTime' => 8.0,
|
||||||
|
]);
|
||||||
|
$I->seeResponseCodeIs(HttpCode::BAD_REQUEST);
|
||||||
|
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$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->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->sendPost('/working-hours', [
|
||||||
|
'workingDay' => '2020-01-15',
|
||||||
|
'workingTime' => '08:00:00',
|
||||||
|
]);
|
||||||
|
$I->seeResponseCodeIs(HttpCode::CONFLICT);
|
||||||
|
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php /** @noinspection PhpUnused */
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\WorkingHours;
|
||||||
|
|
||||||
|
use ApiTester;
|
||||||
|
use Codeception\Util\HttpCode;
|
||||||
|
use Helper\Api;
|
||||||
|
|
||||||
|
class ReadWorkingHoursCest
|
||||||
|
{
|
||||||
|
public static function preflight(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->haveHttpHeader('Access-Control-Request-Method', 'GET');
|
||||||
|
$I->sendOptions('/working-hours/2020-04-15');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Methods');
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Headers');
|
||||||
|
$I->seeHttpHeader('Access-Control-Max-Age');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function readExistingWorkingHours(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->sendGet('/working-hours/2020-01-15');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function readNotExistingWorkingHours(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->sendGet('/working-hours/2020-04-15');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::NOT_FOUND);
|
||||||
|
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->seeResponseIsJson();
|
||||||
|
$I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php /** @noinspection PhpUnused */
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\WorkingHours;
|
||||||
|
|
||||||
|
use ApiTester;
|
||||||
|
use Codeception\Util\HttpCode;
|
||||||
|
use Helper\Api;
|
||||||
|
|
||||||
|
class UpdateWorkingHoursCest
|
||||||
|
{
|
||||||
|
public static function preflight(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->haveHttpHeader('Access-Control-Request-Method', 'PUT');
|
||||||
|
$I->sendOptions('/working-hours/2020-01-15');
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Methods');
|
||||||
|
$I->seeHttpHeader('Access-Control-Allow-Headers');
|
||||||
|
$I->seeHttpHeader('Access-Control-Max-Age');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateWorkingHoursWithExistingValidRecord(ApiTester $I): void
|
||||||
|
{
|
||||||
|
$I->haveHttpHeader('accept', 'application/json');
|
||||||
|
$I->haveHttpHeader('content-type', 'application/json');
|
||||||
|
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->sendPut('/working-hours/2020-01-15', [
|
||||||
|
'workingDay' => '2020-01-15',
|
||||||
|
'workingTime' => '10:00:00',
|
||||||
|
]);
|
||||||
|
$I->seeResponseCodeIs(HttpCode::OK);
|
||||||
|
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$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->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->sendPut('/working-hours/2020-04-15', [
|
||||||
|
'workingDay' => '2020-04-15',
|
||||||
|
'workingTime' => '10:00:00',
|
||||||
|
]);
|
||||||
|
$I->seeResponseCodeIs(HttpCode::NOT_FOUND);
|
||||||
|
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$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->haveHttpHeader('Origin', Api::TEST_ORIGIN);
|
||||||
|
$I->sendPut('/working-hours/2020-01-15', [
|
||||||
|
'workingDay' => '2020-01-15',
|
||||||
|
'workingTime' => 8.0,
|
||||||
|
]);
|
||||||
|
$I->seeResponseCodeIs(HttpCode::BAD_REQUEST);
|
||||||
|
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
|
||||||
|
$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,116 @@
|
|||||||
|
<?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 Slim\Interfaces\RouteParserInterface;
|
||||||
|
use Slim\Routing\RouteContext;
|
||||||
|
use Slim\Routing\RoutingResults;
|
||||||
|
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();
|
||||||
|
/** @noinspection PhpFieldAssignmentTypeMismatchInspection */
|
||||||
|
$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);
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
/** @noinspection PhpFieldAssignmentTypeMismatchInspection */
|
||||||
|
$this->request = $this->makeEmpty(ServerRequestInterface::class, [
|
||||||
|
'getAttribute' => function($name) {
|
||||||
|
return match ($name) {
|
||||||
|
RouteContext::ROUTE_PARSER => $this->makeEmpty(RouteParserInterface::class),
|
||||||
|
RouteContext::ROUTING_RESULTS => $this->makeEmpty(RoutingResults::class),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function testUseWithLogger(): void
|
||||||
|
{
|
||||||
|
/** @noinspection PhpFieldAssignmentTypeMismatchInspection */
|
||||||
|
$this->exception = $this->make(Exception::class, [
|
||||||
|
'message' => '',
|
||||||
|
'code' => 400,
|
||||||
|
'file' => '/path(to/file',
|
||||||
|
]);
|
||||||
|
/** @noinspection PhpFieldAssignmentTypeMismatchInspection */
|
||||||
|
$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
|
||||||
|
{
|
||||||
|
/** @noinspection PhpFieldAssignmentTypeMismatchInspection */
|
||||||
|
$this->exception = $this->make(HttpException::class, [
|
||||||
|
'message' => '',
|
||||||
|
'code' => 400,
|
||||||
|
'file' => '/path(to/file',
|
||||||
|
'getTitle' => Expected::once('The Title'),
|
||||||
|
]);
|
||||||
|
/** @noinspection PhpFieldAssignmentTypeMismatchInspection */
|
||||||
|
$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
|
||||||
|
{
|
||||||
|
/** @noinspection PhpFieldAssignmentTypeMismatchInspection */
|
||||||
|
$this->exception = $this->make(Exception::class, [
|
||||||
|
'message' => '',
|
||||||
|
'code' => 400,
|
||||||
|
'file' => '/path(to/file',
|
||||||
|
]);
|
||||||
|
/** @noinspection PhpFieldAssignmentTypeMismatchInspection */
|
||||||
|
$this->logger = $this->makeEmpty(LoggerInterface::class, []);
|
||||||
|
|
||||||
|
$middleWare = new ErrorHandler($this->app);
|
||||||
|
$middleWare($this->request, $this->exception, true, true, true, $this->logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,13 +17,15 @@ class WorkingHoursViewTest extends Unit
|
|||||||
*/
|
*/
|
||||||
public function workingHoursProvider(): array
|
public function workingHoursProvider(): array
|
||||||
{
|
{
|
||||||
|
$date = new DateTime('2020-11-30');
|
||||||
|
$work_days = 15;
|
||||||
|
$total_hours = new DateInterval("PT32H22M");
|
||||||
|
$overtime = new DateInterval("PT22M");
|
||||||
|
$absence_time = clone $overtime;
|
||||||
|
$absence_time->invert = 1;
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
new DateTime('2020-11-30'),
|
$date, PeriodDesignationEnum::WEEKLY(), $work_days, $total_hours, $overtime,
|
||||||
PeriodDesignationEnum::WEEKLY(),
|
|
||||||
15,
|
|
||||||
new DateInterval("PT32H22M"),
|
|
||||||
new DateInterval("PT22M"),
|
|
||||||
[
|
[
|
||||||
'period' => '2020#49',
|
'period' => '2020#49',
|
||||||
'periodDesignation' => 'weekly',
|
'periodDesignation' => 'weekly',
|
||||||
@@ -33,11 +35,17 @@ class WorkingHoursViewTest extends Unit
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
new DateTime('2020-11-30'),
|
$date, PeriodDesignationEnum::WEEKLY(), $work_days, $total_hours, $absence_time,
|
||||||
PeriodDesignationEnum::MONTHLY(),
|
[
|
||||||
15,
|
'period' => '2020#49',
|
||||||
new DateInterval("PT32H22M"),
|
'periodDesignation' => 'weekly',
|
||||||
new DateInterval("PT22M"),
|
'workingDays' => 15,
|
||||||
|
'totalHours' => '32:22:00',
|
||||||
|
'overtime' => '-00:22:00'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
$date, PeriodDesignationEnum::MONTHLY(), $work_days, $total_hours, $overtime,
|
||||||
[
|
[
|
||||||
'period' => '2020-11',
|
'period' => '2020-11',
|
||||||
'periodDesignation' => 'monthly',
|
'periodDesignation' => 'monthly',
|
||||||
@@ -47,12 +55,17 @@ class WorkingHoursViewTest extends Unit
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
new DateTime('2020-11-30'),
|
$date, PeriodDesignationEnum::MONTHLY(), $work_days, $total_hours, $absence_time,
|
||||||
PeriodDesignationEnum::YEARLY(),
|
|
||||||
15,
|
|
||||||
new DateInterval("PT32H22M"),
|
|
||||||
new DateInterval("PT22M"),
|
|
||||||
[
|
[
|
||||||
|
'period' => '2020-11',
|
||||||
|
'periodDesignation' => 'monthly',
|
||||||
|
'workingDays' => 15,
|
||||||
|
'totalHours' => '32:22:00',
|
||||||
|
'overtime' => '-00:22:00'
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
$date, PeriodDesignationEnum::YEARLY(), $work_days, $total_hours, $overtime, [
|
||||||
'period' => '2020',
|
'period' => '2020',
|
||||||
'periodDesignation' => 'yearly',
|
'periodDesignation' => 'yearly',
|
||||||
'workingDays' => 15,
|
'workingDays' => 15,
|
||||||
@@ -60,6 +73,15 @@ class WorkingHoursViewTest extends Unit
|
|||||||
'overtime' => '00:22:00'
|
'overtime' => '00:22:00'
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
$date, PeriodDesignationEnum::YEARLY(), $work_days, $total_hours, $absence_time, [
|
||||||
|
'period' => '2020',
|
||||||
|
'periodDesignation' => 'yearly',
|
||||||
|
'workingDays' => 15,
|
||||||
|
'totalHours' => '32:22:00',
|
||||||
|
'overtime' => '-00:22:00'
|
||||||
|
],
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ use Codeception\Test\Unit;
|
|||||||
use DateInterval;
|
use DateInterval;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use InvalidArgumentException;
|
||||||
use PDO;
|
use PDO;
|
||||||
use PDOStatement;
|
use TorstenHettstedt\TimekeepingApi\Models\ModelInterface;
|
||||||
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
|
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
|
||||||
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryModelAlreadyExists;
|
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordAlreadyExistException;
|
||||||
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException;
|
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException;
|
||||||
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursRepository;
|
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursRepository;
|
||||||
|
|
||||||
@@ -22,14 +23,17 @@ class WorkingHoursRepositoryTest extends Unit
|
|||||||
const RECORDS_COUNT = 61;
|
const RECORDS_COUNT = 61;
|
||||||
|
|
||||||
protected PDO $pdoObject;
|
protected PDO $pdoObject;
|
||||||
protected PDOStatement $pdoStatement;
|
|
||||||
|
|
||||||
protected function _before(): void
|
protected function _before(): void
|
||||||
{
|
{
|
||||||
|
/** @noinspection SpellCheckingInspection */
|
||||||
$this->pdoObject = new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass');
|
$this->pdoObject = new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass');
|
||||||
parent::_before();
|
parent::_before();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
public function testFindAll(): void
|
public function testFindAll(): void
|
||||||
{
|
{
|
||||||
$repository = new WorkingHoursRepository($this->pdoObject);
|
$repository = new WorkingHoursRepository($this->pdoObject);
|
||||||
@@ -58,6 +62,17 @@ class WorkingHoursRepositoryTest extends Unit
|
|||||||
$repository->findByKey(self::NEW_DATE);
|
$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 RepositoryRecordNotFoundException
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
@@ -87,10 +102,22 @@ class WorkingHoursRepositoryTest extends Unit
|
|||||||
'workingDay' => new DateTime(self::EXISTING_DATE),
|
'workingDay' => new DateTime(self::EXISTING_DATE),
|
||||||
'workingTime' => new DateInterval(self::NEW_INTERVAL),
|
'workingTime' => new DateInterval(self::NEW_INTERVAL),
|
||||||
]);
|
]);
|
||||||
$this->expectException(RepositoryModelAlreadyExists::class);
|
$this->expectException(RepositoryRecordAlreadyExistException::class);
|
||||||
$repository->insert($model);
|
$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 RepositoryRecordNotFoundException
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
@@ -107,6 +134,10 @@ class WorkingHoursRepositoryTest extends Unit
|
|||||||
$repository->findByKey(self::EXISTING_DATE);
|
$repository->findByKey(self::EXISTING_DATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RepositoryRecordNotFoundException
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
public function testNotExistsDelete(): void
|
public function testNotExistsDelete(): void
|
||||||
{
|
{
|
||||||
$repository = new WorkingHoursRepository($this->pdoObject);
|
$repository = new WorkingHoursRepository($this->pdoObject);
|
||||||
@@ -118,6 +149,18 @@ class WorkingHoursRepositoryTest extends Unit
|
|||||||
$repository->delete($model);
|
$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 RepositoryRecordNotFoundException
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Repositories;
|
||||||
|
|
||||||
|
use Codeception\Test\Unit;
|
||||||
|
use DateTime;
|
||||||
|
use DateTimeZone;
|
||||||
|
use Exception;
|
||||||
|
use PDO;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursWeeklyViewRepository;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursMonthlyViewRepository;
|
||||||
|
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursYearlyViewRepository;
|
||||||
|
|
||||||
|
class WorkingHoursViewRepositoryTest extends Unit
|
||||||
|
{
|
||||||
|
protected PDO $pdoObject;
|
||||||
|
|
||||||
|
protected function _before(): void
|
||||||
|
{
|
||||||
|
/** @noinspection SpellCheckingInspection */
|
||||||
|
$this->pdoObject = new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass');
|
||||||
|
parent::_before();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
*/
|
||||||
|
public function listObjectProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[WorkingHoursYearlyViewRepository::class, 1],
|
||||||
|
[WorkingHoursMonthlyViewRepository::class, 3],
|
||||||
|
[WorkingHoursWeeklyViewRepository::class, 13],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function existingObjectProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[WorkingHoursYearlyViewRepository::class, '2020', 61, new DateTime('2020-01-01', new DateTimeZone('UCT'))],
|
||||||
|
[WorkingHoursMonthlyViewRepository::class, '2020 February', 20, new DateTime('2020-02-01', new DateTimeZone('UCT'))],
|
||||||
|
[WorkingHoursWeeklyViewRepository::class, '2020#03', 5, new DateTime('2020-01-13', new DateTimeZone('UCT'))],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array[]
|
||||||
|
*/
|
||||||
|
public function notExistingObjectProvider(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[WorkingHoursYearlyViewRepository::class, '2022',],
|
||||||
|
[WorkingHoursMonthlyViewRepository::class, '2020 May',],
|
||||||
|
[WorkingHoursWeeklyViewRepository::class, '2020#30',],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $period_class
|
||||||
|
* @param int $count_records
|
||||||
|
*
|
||||||
|
* @dataProvider listObjectProvider
|
||||||
|
*/
|
||||||
|
public function testFindAll(string $period_class, int $count_records): void
|
||||||
|
{
|
||||||
|
$repository = new $period_class($this->pdoObject);
|
||||||
|
$models = $repository->findAll();
|
||||||
|
$this->assertIsArray($models);
|
||||||
|
$this->assertContainsOnly(WorkingHoursView::class, $models);
|
||||||
|
$this->assertCount($count_records, $models);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @dataProvider existingObjectProvider
|
||||||
|
*
|
||||||
|
* @param string $period_class
|
||||||
|
* @param string $search
|
||||||
|
* @param int $workingDays
|
||||||
|
* @param DateTime $period
|
||||||
|
*/
|
||||||
|
public function testExistingFindByKey(string $period_class, string $search, int $workingDays, DateTime $period): void
|
||||||
|
{
|
||||||
|
$repository = new $period_class($this->pdoObject);
|
||||||
|
$model = $repository->findByKey($search);
|
||||||
|
$this->assertInstanceOf(WorkingHoursView::class, $model);
|
||||||
|
$this->assertEquals($workingDays, $model->getWorkingDays());
|
||||||
|
$this->assertEquals($period->format('Ymd'), $model->getPeriod()->format('Ymd'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $period_class
|
||||||
|
* @param string $search
|
||||||
|
*
|
||||||
|
* @dataProvider notExistingObjectProvider
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function testNotExistingFindByKey(string $period_class, string $search): void
|
||||||
|
{
|
||||||
|
$repository = new $period_class($this->pdoObject);
|
||||||
|
$this->expectException(RepositoryRecordNotFoundException::class);
|
||||||
|
$repository->findByKey($search);
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
@@ -8,6 +8,19 @@ services:
|
|||||||
docker: "true"
|
docker: "true"
|
||||||
ports:
|
ports:
|
||||||
- 8090:80
|
- 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:
|
volumes:
|
||||||
- ./api:/var/www
|
- ./api:/var/www
|
||||||
- logs:/var/www/logs
|
- logs:/var/www/logs
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
@startuml
|
||||||
|
'https://plantuml.com/salt
|
||||||
|
|
||||||
|
salt
|
||||||
|
{
|
||||||
|
Datum |"Datum <&calendar>"
|
||||||
|
Uhrzeit | "Uhrzeit <&clock>"
|
||||||
|
[Übernehmen <&plus>]
|
||||||
|
[Abbrechen <&action-undo>]
|
||||||
|
}
|
||||||
|
@enduml
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
@startuml
|
||||||
|
'https://plantuml.com/salt
|
||||||
|
|
||||||
|
salt
|
||||||
|
{
|
||||||
|
Woche vom <&calendar> bis zum <&calendar>
|
||||||
|
[<&chevron-left> Woche zurück] | [Woche vor <&chevron-right>]
|
||||||
|
{!
|
||||||
|
Montag | "00:00:00 <&clock>"
|
||||||
|
Dienstag | "00:00:00 <&clock>"
|
||||||
|
Mittwoch | "00:00:00 <&clock>"
|
||||||
|
Donnerstag | "00:00:00 <&clock>"
|
||||||
|
Freitag | "00:00:00 <&clock>"
|
||||||
|
<color:red>Samstag | "<color:red>00:00:00 <&clock>"
|
||||||
|
<color:red>Sontag | "<color:red>00:00:00 <&clock>"
|
||||||
|
}
|
||||||
|
[Übernehmen <&plus>]
|
||||||
|
[Abbrechen <&action-undo>]
|
||||||
|
}
|
||||||
|
@enduml
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Url ohne abschlißenden Slash
|
||||||
|
API_URL= http://localhost:8080
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
public
|
||||||
|
/.env
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"name": "svelte-demo",
|
||||||
|
"scripts": {
|
||||||
|
"build": "rollup -c",
|
||||||
|
"autobuild": "rollup -c -w",
|
||||||
|
"dev": "run-p start:dev autobuild",
|
||||||
|
"start": "sirv public --single",
|
||||||
|
"start:dev": "sirv public --dev --single"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ganalytics": "^3.1.2",
|
||||||
|
"navaid": "^1.0.2",
|
||||||
|
"open-iconic": "^1.1.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify-icons/oi": "^1.1.0",
|
||||||
|
"@iconify/svelte": "^1.0.4",
|
||||||
|
"@rollup/plugin-commonjs": "^15.0.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^9.0.0",
|
||||||
|
"@rollup/plugin-replace": "^2.3.0",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
|
"npm-run-all": "^4.1.3",
|
||||||
|
"rollup": "^2.30.0",
|
||||||
|
"rollup-plugin-svelte": "^6.0.0",
|
||||||
|
"rollup-plugin-terser": "^7.0.0",
|
||||||
|
"sirv-cli": "^1.0.8",
|
||||||
|
"svelte": "^3.4.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<!--suppress HtmlUnknownTarget -->
|
||||||
|
<!--suppress JSUnresolvedLibraryURL -->
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<base href="/" />
|
||||||
|
<meta charset="utf8">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
|
||||||
|
<title>Svelte app</title>
|
||||||
|
|
||||||
|
<link rel='icon' type='image/png' href='/favicon.png'>
|
||||||
|
<link rel="stylesheet" href="/global.css" media="screen">
|
||||||
|
<link rel="stylesheet" href="/print.css" media="print">
|
||||||
|
<link rel="stylesheet" href="/bundle.css">
|
||||||
|
<script type="module" src="https://unpkg.com/dimport?module" data-main="/index.js"></script>
|
||||||
|
<script nomodule src="https://unpkg.com/dimport/nomodule" data-main="/index.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Einstellungen auf die Grundlagen zurücksetzen,
|
||||||
|
// beinhaltet auch Einstellungen für die neuen HTML5-Tags
|
||||||
|
//noinspection CssUnknownTarget
|
||||||
|
@import "meyer_resets.css";
|
||||||
|
// Import und Einbindung der Zeichensätze
|
||||||
|
@import "lesslib/fonts.less";
|
||||||
|
// Bestimmung der Farben
|
||||||
|
@import "lesslib/colors.less";
|
||||||
|
// Mixins des Layouts
|
||||||
|
@import "lesslib/mixins.less";
|
||||||
|
// Layout-Unabhängige Mixins
|
||||||
|
@import "lesslib/mixins.framework.less";
|
||||||
|
// Bestimmung des grundsätzlichen Erscheinungsbildes der einzelnen Elemente,
|
||||||
|
// einschließlich des Hauptbereiches
|
||||||
|
@import "lesslib/basics.less";
|
||||||
|
// Darstellung unabhängig der Darstellungsgrößen
|
||||||
|
@import "lesslib/layout.less";
|
||||||
|
// Darstellung in den verschiedenen Ansichtsgrößen
|
||||||
|
@import "lesslib/media_queries.less";
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
html {
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: #000;
|
||||||
|
font-size: 1em;
|
||||||
|
font-family: OpenSans, "Lucida Grande", "Lucida Sans Unicode", Verdana, Helvetica, Arial, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong, b {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
em, i {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section article {
|
||||||
|
font-size: 95%;
|
||||||
|
p, ul, ol, dl {
|
||||||
|
line-height: 1.5em;
|
||||||
|
.hyphens();
|
||||||
|
font-size: 1.00em;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
ul, ol {
|
||||||
|
li {
|
||||||
|
list-style: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dl {
|
||||||
|
dt {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h1, h2, h3 {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.50em;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.30em;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 1.20em;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
colgroup:not(:first-child) col:first-child{
|
||||||
|
border-left: solid thin;
|
||||||
|
}
|
||||||
|
thead {
|
||||||
|
th {
|
||||||
|
font-size: 100%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
fieldset {
|
||||||
|
legend {
|
||||||
|
font-size: 1.30em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
Definition der Farben
|
||||||
|
*/
|
||||||
|
|
||||||
|
@hueMain: 35;
|
||||||
|
@hueSub: 215;
|
||||||
|
|
||||||
|
@mainColor: hsl(@hueMain, 90%, 29%);
|
||||||
|
@redColor: hsl(0, 90%, 29%);
|
||||||
|
@mainColorDark: darken(@mainColor,66%,relativ);
|
||||||
|
@mainColorLight: lighten(@mainColor,66%,relativ);
|
||||||
|
@subColor: hsl(@hueSub, 90%, 29%);
|
||||||
|
@subColorDark: darken(@subColor,66%,relativ);
|
||||||
|
@subColorLight: lighten(@subColor,66%,relativ);
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: OpenSans;
|
||||||
|
src: url(../fonts/opensans-regular.eot);
|
||||||
|
src: url(../fonts/opensans-regular.eot?#iefix) format("embedded-opentype"),
|
||||||
|
url(../fonts/opensans-regular.woff) format("woff"),
|
||||||
|
url(../fonts/opensans-regular.ttf) format("truetype"),
|
||||||
|
url(../fonts/opensans-regular.svg#open_sansregular) format("svg");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: OpenSans;
|
||||||
|
src: url(../fonts/opensans-italic.eot);
|
||||||
|
src: url(../fonts/opensans-italic.eot?#iefix) format("embedded-opentype"),
|
||||||
|
url(../fonts/opensans-italic.woff) format("woff"),
|
||||||
|
url(../fonts/opensans-italic.ttf) format("truetype"),
|
||||||
|
url(../fonts/opensans-italic.svg#open_sansitalic) format("svg");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: OpenSans;
|
||||||
|
src: url(../fonts/opensans-bold.eot);
|
||||||
|
src: url(../fonts/opensans-bold.eot?#iefix) format("embedded-opentype"),
|
||||||
|
url(../fonts/opensans-bold.woff) format("woff"),
|
||||||
|
url(../fonts/opensans-bold.ttf) format("truetype"),
|
||||||
|
url(../fonts/opensans-bold.svg#open_sansbold) format("svg");
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: OpenSans;
|
||||||
|
src: url(../fonts/opensans-bolditalic.eot);
|
||||||
|
src: url(../fonts/opensans-bolditalic.eot?#iefix) format("embedded-opentype"),
|
||||||
|
url(../fonts/opensans-bolditalic.woff) format("woff"),
|
||||||
|
url(../fonts/opensans-bolditalic.ttf) format("truetype"),
|
||||||
|
url(../fonts/opensans-bolditalic.svg#open_sansbold_italic) format("svg");
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
@breiteMainBereich: 70rem;
|
||||||
|
@breiteTextBereich: 60rem;
|
||||||
|
@breiteNavBereich: @breiteMainBereich - @breiteTextBereich;
|
||||||
|
|
||||||
|
html {
|
||||||
|
body {
|
||||||
|
background: @mainColorLight;
|
||||||
|
> header, > footer {
|
||||||
|
color: @mainColorDark;
|
||||||
|
background: @mainColor;
|
||||||
|
font-family: serif;
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
text-indent: inherit;
|
||||||
|
&:first-letter {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> header h1 {
|
||||||
|
margin: 1rem auto;
|
||||||
|
}
|
||||||
|
> footer {
|
||||||
|
color: contrast(@mainColorDark);
|
||||||
|
}
|
||||||
|
& > nav {
|
||||||
|
ul {
|
||||||
|
background: darken(@mainColorLight,33%,relativ);
|
||||||
|
li {
|
||||||
|
font-weight: bold;
|
||||||
|
&:not(:first-child) {
|
||||||
|
border-left: @mainColorDark solid thin;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: @mainColorDark;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background: lighten(@mainColor,20%,relativ);
|
||||||
|
a {
|
||||||
|
color: lighten(@mainColor,66%,relativ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.selected, &.selected:hover {
|
||||||
|
background: lighten(@mainColor,10%,relativ);
|
||||||
|
a {
|
||||||
|
color: lighten(@mainColor,66%,relativ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
* {
|
||||||
|
border-style: none;
|
||||||
|
border-color: @mainColorDark;
|
||||||
|
border-width: medium;
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
@sectionMargin: 1rem;
|
||||||
|
h1, h2 {
|
||||||
|
margin: @sectionMargin;
|
||||||
|
}
|
||||||
|
p, ul, ol, dl {
|
||||||
|
margin: @sectionMargin;
|
||||||
|
text-indent: @sectionMargin * 2;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
thead {
|
||||||
|
tr:last-child {
|
||||||
|
td, th {
|
||||||
|
border-bottom: @mainColor solid thin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tbody {
|
||||||
|
td.absence-time {
|
||||||
|
color: @redColor;
|
||||||
|
text-decoration-line: underline;
|
||||||
|
text-decoration-style: dashed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
@formLabelWidth: 10rem;
|
||||||
|
@formItemWidth: 40rem;
|
||||||
|
fieldset {
|
||||||
|
width: @formItemWidth + @formLabelWidth + 2rem;
|
||||||
|
margin: 1rem auto;
|
||||||
|
border-style: solid ;
|
||||||
|
border-color: @mainColor;
|
||||||
|
padding: @sectionMargin;
|
||||||
|
&.tasten {
|
||||||
|
border: none;
|
||||||
|
background-color: @mainColorLight;
|
||||||
|
}
|
||||||
|
legend {
|
||||||
|
margin: @sectionMargin;
|
||||||
|
margin-left: @sectionMargin * 2;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
& > label {
|
||||||
|
width: @formLabelWidth;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
display: inline-block;
|
||||||
|
width: @formItemWidth;
|
||||||
|
padding: 0.25rem;
|
||||||
|
margin-bottom: 1.0rem;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: thin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
.button(@mainColor, spin(lighten(@mainColor,66%,relativ),@subColor));
|
||||||
|
&.sub-button {
|
||||||
|
.button(lighten(@mainColor,20%,relativ), spin(@mainColor,@subColor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
text-align: right;
|
||||||
|
padding: 0;
|
||||||
|
p {
|
||||||
|
padding: 0.5rem;
|
||||||
|
display: inline-block;
|
||||||
|
margin: inherit;
|
||||||
|
text-indent: inherit;
|
||||||
|
&:first-letter {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
background: @mainColorLight ;
|
||||||
|
color: @mainColor ;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 85%;
|
||||||
|
}
|
||||||
|
nav li {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
border-bottom-style: solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
//Media Queries
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
* {
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
header, footer, main, nav, aside {
|
||||||
|
body > & {
|
||||||
|
width: 19cm;
|
||||||
|
margin: auto;
|
||||||
|
.border-box();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header, footer{
|
||||||
|
padding: 0.5rem;
|
||||||
|
;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
& > header {
|
||||||
|
font-size: 200%;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
& > nav, > header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
padding: 1.5rem 0.5rem 3.25rem;
|
||||||
|
.flexKinder(flow,'row');
|
||||||
|
width: @breiteTextBereich;
|
||||||
|
section {
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
h1, h2, p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
padding: 0;
|
||||||
|
p {
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 85%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
article.liste {
|
||||||
|
page-break-after: always;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h1, h2 {
|
||||||
|
margin-left: 5em;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.75em;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
margin: auto;
|
||||||
|
td, th {
|
||||||
|
padding: 0.5rem;
|
||||||
|
collapse: true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aside {
|
||||||
|
padding-top: 2rem;
|
||||||
|
.flexKinder(flow,'row');
|
||||||
|
width: @breiteNavBereich;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 831px) {
|
||||||
|
header, footer, main, nav, aside {
|
||||||
|
body > & {
|
||||||
|
width: @breiteMainBereich;
|
||||||
|
margin: auto;
|
||||||
|
.border-box();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header, footer{
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
& > header {
|
||||||
|
font-size: 200%;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
& > nav {
|
||||||
|
ul {
|
||||||
|
.flexEltern();
|
||||||
|
margin: 0;
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1.5rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
padding: 1.5rem 0.5rem 3.25rem;
|
||||||
|
.flexKinder(flow,'row');
|
||||||
|
width: @breiteTextBereich;
|
||||||
|
section {
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
h1, h2, p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
padding: 0;
|
||||||
|
p {
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 85%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h1, h2 {
|
||||||
|
margin-left: 5em;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.75em;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
margin: auto;
|
||||||
|
td, th {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aside {
|
||||||
|
padding-top: 2rem;
|
||||||
|
.flexKinder(flow,'row');
|
||||||
|
width: @breiteNavBereich;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 830px) {
|
||||||
|
html body {
|
||||||
|
@boxHeight: 5rem;
|
||||||
|
& > header {
|
||||||
|
height: @boxHeight;
|
||||||
|
font-size: @boxHeight;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
& > footer {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
& > nav {
|
||||||
|
ul {
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
border: none;
|
||||||
|
width: auto;
|
||||||
|
height: 1.5em;
|
||||||
|
padding-top: 0.25em;
|
||||||
|
a {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
&:not(:first-child) {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main section {
|
||||||
|
header {
|
||||||
|
padding: 0 0.5rem;
|
||||||
|
h1, h2, p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
h1, h2 {
|
||||||
|
margin-left: 5em;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.75em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
@formLabelWidth: 10rem;
|
||||||
|
@formItemWidth: 40rem;
|
||||||
|
@sectionMargin: 1rem;
|
||||||
|
margin: @sectionMargin;
|
||||||
|
fieldset {
|
||||||
|
width: auto;
|
||||||
|
margin: 1rem auto;
|
||||||
|
padding: @sectionMargin;
|
||||||
|
legend {
|
||||||
|
margin: @sectionMargin;
|
||||||
|
margin-left: @sectionMargin * 2;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
& > label {
|
||||||
|
width: auto;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
input, textarea {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
padding: 0.25rem;
|
||||||
|
margin: 0.5rem;
|
||||||
|
margin-bottom: 1.0rem;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: thin;
|
||||||
|
}
|
||||||
|
div.radio-button {
|
||||||
|
padding-left: 20%;
|
||||||
|
label, input {
|
||||||
|
display: inline;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
margin-top: inherit;
|
||||||
|
margin-right: 1rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
margin-bottom: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
height: 10rem;
|
||||||
|
}
|
||||||
|
ul.suche {
|
||||||
|
width: 30rem;
|
||||||
|
margin-left: @formLabelWidth + 1.5rem;
|
||||||
|
margin-top: -1rem;
|
||||||
|
background: white;
|
||||||
|
position: absolute;
|
||||||
|
border: solid thin;
|
||||||
|
li {list-style: none;
|
||||||
|
text-indent: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background: @mainColorLight;
|
||||||
|
color: @mainColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 90%;
|
||||||
|
margin: 5%;
|
||||||
|
td, th {
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
colgroup:not(:first-child) col:first-child{
|
||||||
|
border-left: solid thin;
|
||||||
|
}
|
||||||
|
thead {
|
||||||
|
th {
|
||||||
|
font-size: 100%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 720px) {
|
||||||
|
html body {
|
||||||
|
@boxHeight: 2.5rem;
|
||||||
|
& > header {
|
||||||
|
height: @boxHeight;
|
||||||
|
font-size: @boxHeight;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > nav {
|
||||||
|
.hamburgerMenu();
|
||||||
|
}
|
||||||
|
main section header {
|
||||||
|
h1, h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin: 0.5rem auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
margin: 1rem 0;
|
||||||
|
td, th {
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
colgroup:not(:first-child) col:first-child{
|
||||||
|
border-left: solid thin;
|
||||||
|
}
|
||||||
|
thead {
|
||||||
|
th {
|
||||||
|
font-size: 100%;
|
||||||
|
font-weight: bold;
|
||||||
|
.hyphens();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
.hyphens() {
|
||||||
|
-webkit-hyphens: auto;
|
||||||
|
-moz-hyphens: auto;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-box() {
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical-gradient(@stop1, @stop2) {
|
||||||
|
.gradient(0deg, @stop1, @stop2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-gradient(@stop1, @stop2) {
|
||||||
|
.gradient(-90deg, @stop1, @stop2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient(@winkel, @stop1, @stop2) {
|
||||||
|
@winkelOld: @winkel + 90deg;
|
||||||
|
@groundColor: @stop2;
|
||||||
|
background: @groundColor -moz-linear-gradient(@winkelOld, @stop2, @stop1);
|
||||||
|
background: @groundColor -webkit-linear-gradient(@winkelOld, @stop2, @stop1);
|
||||||
|
background: @groundColor -o-linear-gradient(@winkelOld, @stop2, @stop1);
|
||||||
|
background: @groundColor -ms-linear-gradient(@winkelOld, @stop2, @stop1);
|
||||||
|
background: @groundColor linear-gradient(@winkel, @stop2, @stop1);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-shadow(...) {
|
||||||
|
-webkit-box-shadow: @arguments;
|
||||||
|
-moz-box-shadow: @arguments;
|
||||||
|
box-shadow: @arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-radius(...) {
|
||||||
|
-webkit-border-radius: @arguments;
|
||||||
|
-moz-border-radius: @arguments;
|
||||||
|
border-radius: @arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nowrap() {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hamburgerMenu() {
|
||||||
|
color: white;
|
||||||
|
width: 3rem;
|
||||||
|
height: 2rem;
|
||||||
|
top: 0.25rem;
|
||||||
|
left: 0.25rem;
|
||||||
|
.border-radius(4px);
|
||||||
|
border: none;
|
||||||
|
.vertical-gradient(@mainColorDark; @mainColor);
|
||||||
|
> ul {
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 100%;
|
||||||
|
background: fade(@mainColorDark,80%);
|
||||||
|
.border-radius(5px);
|
||||||
|
display: none;
|
||||||
|
> li {
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
line-height: normal;
|
||||||
|
border: none;
|
||||||
|
&:not(:first-child) {
|
||||||
|
border-top: 1px #888 solid;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
.nowrap();
|
||||||
|
display: block;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
color: white;
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: white;
|
||||||
|
.vertical-gradient(@mainColorLight; @mainColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:first-child {
|
||||||
|
a {
|
||||||
|
.border-radius( 5px 5px 0 0 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
a {
|
||||||
|
.border-radius( 0 0 5px 5px );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:hover, &:focus, &:active, &:checked {
|
||||||
|
ul {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:before {
|
||||||
|
content: "\2261";
|
||||||
|
font-size: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 0.8em;
|
||||||
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flexEltern() {
|
||||||
|
display:-ms-flexbox;
|
||||||
|
display: flexbox;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flexEltern(column) {
|
||||||
|
display:-ms-flexbox;
|
||||||
|
display: flexbox;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: flex;
|
||||||
|
-webkit-flex-direction: column;
|
||||||
|
-ms-flex-direction: column;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flexKinder(flow, @rest...) {
|
||||||
|
-webkit-flex-flow: @rest;
|
||||||
|
-ms-flex-flow: @rest;
|
||||||
|
flex-flow: @rest;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flexKinder(glow, @num) {
|
||||||
|
-webkit-flex-grow: @num;
|
||||||
|
-ms-flex: @num;
|
||||||
|
flex: @num;
|
||||||
|
flex-grow: @num;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.button(@color, @colorFont) {
|
||||||
|
@colorBackground: @color;
|
||||||
|
@colorBackgroundLight: lighten(@colorBackground,50%,relativ);
|
||||||
|
@colorShadow: fade(@colorBackground,50%);
|
||||||
|
height: 2.5em;
|
||||||
|
.border-radius(1.25em);
|
||||||
|
padding: 0.5em 1.25em;
|
||||||
|
.vertical-gradient(@colorBackgroundLight, @colorBackground);
|
||||||
|
.box-shadow(0.1em 0.2em 0.5em @colorShadow);
|
||||||
|
color: @colorFont;
|
||||||
|
font-weight: bold;
|
||||||
|
&:hover {
|
||||||
|
.vertical-gradient(@colorBackground, @colorBackgroundLight);
|
||||||
|
}
|
||||||
|
margin: 0.25rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
html {
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: #000;
|
||||||
|
font-size: 1em;
|
||||||
|
font-family: OpenSans, "Lucida Grande", "Lucida Sans Unicode", Verdana, Helvetica, Arial, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong, b {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
em, i {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
section article {
|
||||||
|
font-size: 95%;
|
||||||
|
p, ul, ol, dl {
|
||||||
|
line-height: 1.5em;
|
||||||
|
//noinspection TaskProblemsInspection,LessResolvedByNameOnly
|
||||||
|
.hyphens();
|
||||||
|
font-size: 1.00em;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
ul, ol {
|
||||||
|
li {
|
||||||
|
list-style: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dl {
|
||||||
|
dt {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h1, h2, h3 {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.50em;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.30em;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 1.20em;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
thead {
|
||||||
|
th {
|
||||||
|
font-size: 100%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
fieldset {
|
||||||
|
legend {
|
||||||
|
font-size: 1.30em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
div.zelle > label {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.http-error {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
Definition der Farben
|
||||||
|
*/
|
||||||
|
|
||||||
|
@hueMain: 180;
|
||||||
|
@hueSub: 60;
|
||||||
|
|
||||||
|
@mainColor: hsl(@hueMain, 90%, 29%);
|
||||||
|
@mainColorDark: darken(@mainColor,66%,relativ);
|
||||||
|
@mainColorLight: lighten(@mainColor,66%,relativ);
|
||||||
|
@subColor: hsl(@hueSub, 90%, 29%);
|
||||||
|
@subColorDark: darken(@subColor,66%,relativ);
|
||||||
|
@subColorLight: lighten(@subColor,66%,relativ);
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
html {
|
||||||
|
body {
|
||||||
|
> header, > footer {
|
||||||
|
background: none;
|
||||||
|
font-family: serif;
|
||||||
|
h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
text-indent: inherit;
|
||||||
|
&:first-letter {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> header {
|
||||||
|
border-bottom: black solid thick;
|
||||||
|
h1 {
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> nav {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
> footer {
|
||||||
|
color: contrast(@mainColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
* {
|
||||||
|
border-style: none;
|
||||||
|
border-color: @mainColorDark;
|
||||||
|
border-width: medium;
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
@sectionMargin: 1rem;
|
||||||
|
header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
h1, h2 {
|
||||||
|
margin: @sectionMargin;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: @sectionMargin;
|
||||||
|
text-indent: @sectionMargin * 2;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
colgroup:not(:first-child) col:first-child{
|
||||||
|
border-left: solid medium;
|
||||||
|
}
|
||||||
|
thead {
|
||||||
|
tr:last-child {
|
||||||
|
td, th {
|
||||||
|
border-bottom: @mainColorDark solid thick;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
th {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem 0.5rem;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-bottom: solid thin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
text-align: right;
|
||||||
|
padding: 0;
|
||||||
|
p {
|
||||||
|
padding: 0.5rem;
|
||||||
|
display: inline-block;
|
||||||
|
margin: inherit;
|
||||||
|
text-indent: inherit;
|
||||||
|
&:first-letter {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
background: @mainColorLight ;
|
||||||
|
color: @mainColor ;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 85%;
|
||||||
|
}
|
||||||
|
border-bottom-style: solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Einstellungen auf die Grundlagen zurücksetzen,
|
||||||
|
// beinhaltet auch Einstellungen für die neuen HTML5-Tags
|
||||||
|
//noinspection CssUnknownTarget
|
||||||
|
@import "meyer_resets.css";
|
||||||
|
// Bestimmung der Farben
|
||||||
|
@import "lesslib/colors.less";
|
||||||
|
// Mixins des Layouts
|
||||||
|
@import "lesslib/mixins.less";
|
||||||
|
// Layout-Unabhängige Mixins
|
||||||
|
@import "lesslib/mixins.framework.less";
|
||||||
|
// Bestimmung des grundsätzlichen Erscheinungsbildes der einzelnen Elemente,
|
||||||
|
// einschließlich des Hauptbereiches
|
||||||
|
@import "lesslib/print/basics.less";
|
||||||
|
// Darstellung unabhängig der Darstellungsgrößen
|
||||||
|
@import "lesslib/print/layout.less";
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/* http://meyerweb.com/eric/tools/css/reset/
|
||||||
|
v2.0 | 20110126
|
||||||
|
License: none (public domain)
|
||||||
|
*/
|
||||||
|
@media screen {
|
||||||
|
html, body, div, span, applet, object, iframe,
|
||||||
|
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||||
|
a, abbr, acronym, address, big, cite, code,
|
||||||
|
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||||
|
small, strike, strong, sub, sup, tt, var,
|
||||||
|
b, u, i, center,
|
||||||
|
dl, dt, dd, ol, ul, li,
|
||||||
|
fieldset, form, label, legend,
|
||||||
|
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||||
|
article, aside, canvas, details, embed,
|
||||||
|
figure, figcaption, footer, header, hgroup,
|
||||||
|
menu, nav, output, ruby, section, summary,
|
||||||
|
time, mark, audio, video {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 100%;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
article, aside, details, figcaption, figure,
|
||||||
|
main, /* Korektur: Torsten Lücke 2016-09-01 */
|
||||||
|
footer, header, hgroup, menu, nav, section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
ol, ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
blockquote, q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
blockquote:before, blockquote:after,
|
||||||
|
q:before, q:after {
|
||||||
|
content: '';
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* END: meyer_ressets */
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import svelte from 'rollup-plugin-svelte';
|
||||||
|
import replace from '@rollup/plugin-replace';
|
||||||
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
import { terser } from 'rollup-plugin-terser';
|
||||||
|
import {config} from 'dotenv';
|
||||||
|
|
||||||
|
const production = !process.env.ROLLUP_WATCH;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: 'src/index.js',
|
||||||
|
output: {
|
||||||
|
name: 'app',
|
||||||
|
format: 'esm',
|
||||||
|
sourcemap: true,
|
||||||
|
dir: 'public',
|
||||||
|
},
|
||||||
|
preserveEntrySignatures: false,
|
||||||
|
plugins: [
|
||||||
|
svelte({
|
||||||
|
// enable run-time checks when not in production
|
||||||
|
dev: !production,
|
||||||
|
// we'll extract any component CSS out into
|
||||||
|
// a separate file — better for performance
|
||||||
|
css: css => {
|
||||||
|
css.write('bundle.css');
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// If you have external dependencies installed from
|
||||||
|
// npm, you'll most likely need these plugins. In
|
||||||
|
// some cases you'll need additional configuration —
|
||||||
|
// consult the documentation for details:
|
||||||
|
// https://github.com/rollup/rollup-plugin-commonjs
|
||||||
|
resolve(),
|
||||||
|
commonjs(),
|
||||||
|
|
||||||
|
replace({
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(production ? 'production' : 'development'),
|
||||||
|
'env': JSON.stringify({...config().parsed /* attached the .env config*/})
|
||||||
|
}),
|
||||||
|
|
||||||
|
// If we're building for production (npm run build
|
||||||
|
// instead of npm run dev), minify
|
||||||
|
production && terser()
|
||||||
|
],
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
clearScreen: false
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<header>
|
||||||
|
Arbeitszeiten
|
||||||
|
</header>
|
||||||
|
<Nav {active} />
|
||||||
|
<main>
|
||||||
|
<svelte:component this={Route} {params} />
|
||||||
|
</main>
|
||||||
|
<footer><p>© Torsten Lücke 2021</p></footer>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Navaid from 'navaid';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import Nav from './Nav.svelte';
|
||||||
|
|
||||||
|
let Route, params={}, active;
|
||||||
|
let uri = location.pathname;
|
||||||
|
$: active = uri.split('/').pop() || 'home';
|
||||||
|
|
||||||
|
function run(thunk, obj) {
|
||||||
|
const target = uri;
|
||||||
|
|
||||||
|
thunk.then(m => {
|
||||||
|
if (target !== uri) return;
|
||||||
|
|
||||||
|
params = obj || {};
|
||||||
|
|
||||||
|
if (m.preload) {
|
||||||
|
m.preload({ params }).then(() => {
|
||||||
|
if (target !== uri) return;
|
||||||
|
Route = m.default;
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Route = m.default;
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function track(obj) {
|
||||||
|
uri = obj.state || obj.uri || location.pathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener('replacestate', track);
|
||||||
|
addEventListener('pushstate', track);
|
||||||
|
addEventListener('popstate', track);
|
||||||
|
|
||||||
|
const router = Navaid('/')
|
||||||
|
.on('/', () => run(import('../routes/Home.svelte')))
|
||||||
|
.on('/views/weekly', () => run(import('../routes/WeeklyViews.svelte')))
|
||||||
|
.on('/views/monthly', () => run(import('../routes/MonthlyViews.svelte')))
|
||||||
|
.on('/views/yearly', () => run(import('../routes/YearlyViews.svelte')))
|
||||||
|
.on('/working-hours', () => run(import('../routes/WorkingHours.svelte')))
|
||||||
|
.listen();
|
||||||
|
|
||||||
|
onDestroy(router.unlisten);
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!--suppress HtmlUnknownTarget -->
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li class="{ isActive('home') }"><a href="/">Home</a></li>
|
||||||
|
<li class="{ isActive('weekly') }"><a href="/views/weekly">Wochenansicht</a></li>
|
||||||
|
<li class="{ isActive('monthly') }"><a href="/views/monthly">Monatsansicht</a></li>
|
||||||
|
<li class="{ isActive('yearly') }"><a href="/views/yearly">Jahresansicht</a></li>
|
||||||
|
<li class="{ isActive('working-hours') }"><a href="/working-hours">Einträge</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export let active;
|
||||||
|
$: isActive = str => active === str ? 'selected' : '';
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import App from './components/App.svelte';
|
||||||
|
|
||||||
|
new App({
|
||||||
|
target: document.body
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<Views />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Views from "./WeeklyViews.svelte";
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script>
|
||||||
|
import Views from "./Views.svelte";
|
||||||
|
export let params;
|
||||||
|
params = {
|
||||||
|
title: 'Monatsansicht',
|
||||||
|
records: [],
|
||||||
|
isLoading: true
|
||||||
|
}
|
||||||
|
const View = {
|
||||||
|
'browse': () => {
|
||||||
|
fetch(env.API_URL + '/views/working-hours/monthly')
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Fail!');
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
params.records = data;
|
||||||
|
params.isLoading = false;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
params.isLoading = false;
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
View.browse();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Views {params} />
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script>
|
||||||
|
export let params;
|
||||||
|
console.log(params)
|
||||||
|
if (!String.prototype.startsWith) {
|
||||||
|
String.prototype.startsWith = function(searchString, position) {
|
||||||
|
position = position || 0;
|
||||||
|
return this.indexOf(searchString, position) === position;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
$: isNegative = time => time.startsWith('-') ? 'absence-time' : '';
|
||||||
|
$: cutMinusChar = time => time.startsWith('-') ? time.substr(1) : time;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{params.title}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<section id="list-records">
|
||||||
|
<header>
|
||||||
|
<h1>{params.title}</h1>
|
||||||
|
</header>
|
||||||
|
<article>
|
||||||
|
{#if params.isLoading === true}
|
||||||
|
<p>Loading...</p>
|
||||||
|
{:else}
|
||||||
|
<table>
|
||||||
|
<colgroup>
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Zeitraum</th>
|
||||||
|
<th>Arbeitstage</th>
|
||||||
|
<th>Arbeitsstunden</th>
|
||||||
|
<th>Über- / Unterstunden</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each params.records as record}
|
||||||
|
<tr>
|
||||||
|
<td>{record.period}</td>
|
||||||
|
<td>{record.workingDays}</td>
|
||||||
|
<td>{record.totalHours}</td>
|
||||||
|
<td class="{ isNegative(record.overtime) }">{cutMinusChar(record.overtime)}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script>
|
||||||
|
import Views from "./Views.svelte";
|
||||||
|
export let params;
|
||||||
|
params = {
|
||||||
|
title: 'Wochenansicht',
|
||||||
|
records: [],
|
||||||
|
isLoading: true
|
||||||
|
}
|
||||||
|
const View = {
|
||||||
|
'browse': () => {
|
||||||
|
fetch(env.API_URL + '/views/working-hours/weekly')
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Fail!');
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
params.records = data;
|
||||||
|
params.isLoading = false;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
params.isLoading = false;
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
View.browse();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Views {params} />
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
<script>
|
||||||
|
export let params;
|
||||||
|
let title = "Bearbeitung Einträge"
|
||||||
|
import IconifyIcon from '@iconify/svelte';
|
||||||
|
import wrenchIcon from '@iconify-icons/oi/wrench';
|
||||||
|
import checkIcon from '@iconify-icons/oi/check';
|
||||||
|
import xIcon from '@iconify-icons/oi/x';
|
||||||
|
import documentIcon from '@iconify-icons/oi/document';
|
||||||
|
let activeRecord = null;
|
||||||
|
let newRecord = false;
|
||||||
|
params = {
|
||||||
|
title: 'Einträge',
|
||||||
|
records: [],
|
||||||
|
isLoading: true
|
||||||
|
}
|
||||||
|
const Records = {
|
||||||
|
'browse': () => {
|
||||||
|
// noinspection JSUnresolvedVariable
|
||||||
|
fetch(env.API_URL + '/working-hours')
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Fail!');
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
Controller.cancelActiveRecord();
|
||||||
|
params.records = data;
|
||||||
|
params.isLoading = false;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
params.isLoading = false;
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'read': date => {
|
||||||
|
// noinspection JSUnresolvedVariable
|
||||||
|
fetch(env.API_URL + '/working-hours/' + date)
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Fail!');
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
activeRecord = data;
|
||||||
|
if (date === null) {
|
||||||
|
Controller.listRecords();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'add': async record => {
|
||||||
|
console.log(record);
|
||||||
|
// noinspection JSUnresolvedVariable
|
||||||
|
await fetch(env.API_URL + '/working-hours', {
|
||||||
|
method: 'POST',
|
||||||
|
mode: 'cors',
|
||||||
|
cache: 'no-cache',
|
||||||
|
credentials: 'omit',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(record)
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Fail!');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
Controller.listRecords();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'update': async (date, record) => {
|
||||||
|
console.log(record);
|
||||||
|
// noinspection JSUnresolvedVariable
|
||||||
|
await fetch(env.API_URL + '/working-hours/' + date, {
|
||||||
|
method: 'PUT',
|
||||||
|
mode: 'cors',
|
||||||
|
cache: 'no-cache',
|
||||||
|
credentials: 'omit',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(record)
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Fail!');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
Controller.listRecords();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const Controller = {
|
||||||
|
'listRecords': () => {
|
||||||
|
Controller.cancelActiveRecord();
|
||||||
|
Records.browse();
|
||||||
|
},
|
||||||
|
'newRecord': () => {
|
||||||
|
activeRecord = {workingTime: '00:00:00'};
|
||||||
|
newRecord = true;
|
||||||
|
},
|
||||||
|
'saveRecord': () => {
|
||||||
|
if (newRecord === true) {
|
||||||
|
Records.add(activeRecord);
|
||||||
|
} else {
|
||||||
|
Records.update(activeRecord.workingDay, activeRecord);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'selectActiveRecord': record => Records.read(record.workingDay),
|
||||||
|
'cancelActiveRecord': () => {
|
||||||
|
activeRecord = null;
|
||||||
|
newRecord = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const initSite = () => {
|
||||||
|
Controller.listRecords()
|
||||||
|
};
|
||||||
|
initSite()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{title}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
{#if activeRecord === null}
|
||||||
|
<section id="list-records">
|
||||||
|
<header>
|
||||||
|
<h1>{title}</h1>
|
||||||
|
</header>
|
||||||
|
<article>
|
||||||
|
{#if params.isLoading === true}
|
||||||
|
<p>Loading...</p>
|
||||||
|
{:else}
|
||||||
|
<!--suppress HtmlUnknownTarget -->
|
||||||
|
<form action="/working-hours">
|
||||||
|
<fieldset>
|
||||||
|
<button on:click={Controller.newRecord} type="button">Neuer Eintrag
|
||||||
|
<IconifyIcon icon={documentIcon}/>
|
||||||
|
</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
<table>
|
||||||
|
<colgroup>
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
<col />
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Arbeitsstunden</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each params.records as record}
|
||||||
|
<tr>
|
||||||
|
<td>{record.workingDay}</td>
|
||||||
|
<td>{record.workingTime}</td>
|
||||||
|
<td>
|
||||||
|
<button on:click={() => Controller.selectActiveRecord(record)} type="button">Bearbeiten
|
||||||
|
<IconifyIcon icon={wrenchIcon}/>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
{:else}
|
||||||
|
<section id="edit-record">
|
||||||
|
<header>
|
||||||
|
<h1>{title}</h1>
|
||||||
|
</header>
|
||||||
|
<article>
|
||||||
|
<!--suppress HtmlUnknownTarget -->
|
||||||
|
<form action="/working-hours" name="editRecord">
|
||||||
|
<fieldset name="record-data">
|
||||||
|
<div>
|
||||||
|
<label for="workingDay">Arbeitstag</label>
|
||||||
|
<input bind:value={activeRecord.workingDay} type="date"
|
||||||
|
id="workingDay" required pattern="\d\{4}-[0-1]\d-[0-3]\d\">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="workingTime">Arbeitszeit</label>
|
||||||
|
<input bind:value={activeRecord.workingTime} placeholder="Arbeitszeit" type="time"
|
||||||
|
id="workingTime" required pattern="[0-5]\d:[0-5]\d:[0-5]\d">
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset name="buttons">
|
||||||
|
<button on:click={Controller.saveRecord} type="button">Übernehmen
|
||||||
|
<IconifyIcon icon={checkIcon} color="green"/>
|
||||||
|
</button>
|
||||||
|
<button on:click={Controller.cancelActiveRecord} type="button" class="sub-button">Abbrechen
|
||||||
|
<IconifyIcon icon={xIcon}/>
|
||||||
|
</button>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script>
|
||||||
|
import Views from "./Views.svelte";
|
||||||
|
export let params;
|
||||||
|
params = {
|
||||||
|
title: 'Jahresansicht',
|
||||||
|
records: [],
|
||||||
|
isLoading: true
|
||||||
|
}
|
||||||
|
const View = {
|
||||||
|
'browse': () => {
|
||||||
|
fetch(env.API_URL + '/views/working-hours/yearly')
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Fail!');
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
params.records = data;
|
||||||
|
params.isLoading = false;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
params.isLoading = false;
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
View.browse();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Views {params} />
|
||||||
Reference in New Issue
Block a user