43 Commits
Author SHA1 Message Date
Torstem_JetBrain c711e0ae24 Es ist möglich einen neuen Eintrag zu generieren. 2021-04-10 11:26:34 +02:00
Torstem_JetBrain 74fd96cf25 Ein Eintrag kann verändert werden. 2021-04-09 21:56:08 +02:00
Torstem_JetBrain 9686a8505e Das löschen eines Eintrages wird nicht unterstützt. 2021-04-09 21:53:52 +02:00
Torstem_JetBrain bc83fc8dd1 Die Einträge werden aufgelistet und angezeigt. 2021-04-09 21:19:19 +02:00
Torstem_JetBrain 9d5af9a0fb Die Daten für die Ansichten werden von einer API abgefragt. 2021-04-09 18:20:47 +02:00
Torstem_JetBrain 7f8a50799f Die Web-App kann per *dotEnv* konfiguriert werden. 2021-04-09 18:19:42 +02:00
TorstenHettstedt 365c2f041c Merge pull request 'html-sites' (#3) from html-sites into master
Reviewed-on: #3
2021-04-08 20:15:59 +02:00
Torstem_JetBrain 89ad394773 Die gewünschte HTML-Struktur wurde übernommen. 2021-04-08 19:09:44 +02:00
Torstem_JetBrain 881e7362f8 Großschreibung ist gefordert. 2021-04-08 19:08:50 +02:00
Torstem_JetBrain c8d9bc040e Die HTML-Dateien für die Anzeige und Bearbeitung der Daten sind vorhanden. 2021-04-08 18:12:05 +02:00
Torstem_JetBrain e3ef53b119 Übernahme der Namen aus der OpenAPI-Datei. 2021-04-08 13:29:31 +02:00
Torstem_JetBrain f78fdfbbd5 Definition der Beispieldaten für jede Liste. 2021-04-08 13:20:08 +02:00
Torstem_JetBrain 02771828c9 Das Fav-Icon wurde hinzugefügt. 2021-04-08 12:49:54 +02:00
Torstem_JetBrain 5a88ff3c6f Die Links müssen bleiben. 2021-04-08 12:44:26 +02:00
Torstem_JetBrain f36324bfbd Die grundlegenden HTML-Dateien sind vorhanden. 2021-04-08 12:30:36 +02:00
Torstem_JetBrain 9b99c6a4dc Für die Svelte-Oberfläche sind alle notwendigen Dateien vorhanden. 2021-04-08 12:28:26 +02:00
Torstem_JetBrain fe3a4f69bf Die Struktur der Oberfläche für die Eintragungen wurde skizziert. 2021-04-07 21:28:10 +02:00
TorstenHettstedt 31a3f4cc01 Merge pull request 'api-erstellen' (#1) from api-erstellen into master
Reviewed-on: #1
2021-04-07 13:00:46 +02:00
Torstem_JetBrain 0a59cb60db Die Gemeinsamkeiten der Controller wurde in eine abstrakte Klasse gepackt. 2021-04-07 12:59:51 +02:00
Torstem_JetBrain 9430f3c74c Eine fehlende Datenbank löst schon in der Erstellung des Objektes einen Fehler aus.
Es wurden noch kleinere Fehler beseitigt.
2021-04-07 12:45:27 +02:00
Torstem_JetBrain 2b31e17e14 Eine fehlende Datenbank löst schon in der Erstellung des Objektes einen Fehler aus. 2021-04-07 12:29:46 +02:00
Torstem_JetBrain 9e35ab3b07 Eine fehlende Datenbank sollt schon in der Erstellung des Objektes einen Fehler auslösen. 2021-04-07 12:20:14 +02:00
Torstem_JetBrain 056bf82c7b Die Eigenschaft wird nicht genutzt. 2021-04-07 12:16:47 +02:00
Torstem_JetBrain be54a659c9 Das Logger-Objekt wird in einer speziellen Methode überprüft. 2021-04-07 12:15:00 +02:00
Torstem_JetBrain 8086b86eb2 Bessere Name. 2021-04-07 12:11:01 +02:00
Torstem_JetBrain 3e2865dbe7 Der Fehler kann abgefangen werden, ähnlich read() 2021-04-07 12:08:37 +02:00
Torstem_JetBrain 60eb1f92e8 Der Container wird nur bei der Initialisierung benötigt. 2021-04-07 12:07:55 +02:00
Torstem_JetBrain 9a542c9108 Die Definition der Typen war nicht ausreichend. 2021-04-07 11:58:15 +02:00
Torstem_JetBrain a035ea9648 Kann weg. 2021-04-07 11:55:38 +02:00
Torstem_JetBrain 46f3a6491d Diese Datei hat nichts im Repo verloren 2021-04-07 11:50:35 +02:00
Torstem_JetBrain e070db18b5 Test erweitert um die Coverage zu erhöhen. 2021-04-06 23:21:44 +02:00
Torstem_JetBrain a6675605e4 Kleine Code-Verbesserungen. 2021-04-06 21:33:33 +02:00
Torstem_JetBrain 520db45d6d Vereinfachung der 'index.php' und Entfernung nicht benötigter Klassen. 2021-04-06 21:26:35 +02:00
Torstem_JetBrain 2d3a1074f6 Die Error-Middleware wurde in eine Klasse verpackt. 2021-04-06 21:21:47 +02:00
Torstem_JetBrain a1e8b1aa51 Es wird die geforderte Error-JSON benutzt. 2021-04-06 21:02:29 +02:00
Torstem_JetBrain f25c9d8994 FIXING: Die Filter sind in *Codception* nicht definiert. 2021-04-06 21:00:01 +02:00
Torstem_JetBrain 3854737622 Die Abfragen erzeugen die richtigen HTTP-Codes. Ist die Anfrage richtig, sind auch die Rückgaben richtig. 2021-04-06 20:01:15 +02:00
Torstem_JetBrain d16a4e427d Copy & Past Fehler. 2021-04-06 19:57:29 +02:00
Torstem_JetBrain 4871ff14dc Die Namen der Eigenschaften waren nicht perfekt. 2021-04-06 19:56:41 +02:00
Torstem_JetBrain ac99e079ee Für die API-Tests wird eine andere Datenbank-Verbindung benötigt. Sonst werden die vorhandenen Daten verändert. 2021-04-06 16:52:51 +02:00
Torstem_JetBrain 09ffadc596 Die Anzeige der Ansicht-Tabellen ist erfolgreich. 2021-04-06 16:28:04 +02:00
Torstem_JetBrain 4da5c1242c Die Verbindung zur Datenbank ist eingebaut. 2021-04-06 16:26:43 +02:00
Torstem_JetBrain e0b8deae8b Es war nicht ganz klar, wir der Aufbau für *Regexe* war und das die Stunden deutlich größer als 60 sein können. 2021-04-06 16:23:30 +02:00
46 changed files with 1489 additions and 72 deletions
+10
View File
@@ -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
View File
@@ -105,3 +105,4 @@ Temporary Items
/api/vendor/ /api/vendor/
/api/tests/_* /api/tests/_*
/api/tests/*.suite.yml /api/tests/*.suite.yml
/api/.env
+5
View File
@@ -0,0 +1,5 @@
## Datenbank-Einstellungen
DATABASES_HOST=psql.domain.tld
DATABASES_NAME=db
DATABASES_USER=user
DATABASES_PASS=1234
+3
View File
@@ -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
+1
View File
@@ -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"
}, },
+34 -2
View File
@@ -1,12 +1,44 @@
<?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\WorkingHoursController;
use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursViewController;
use TorstenHettstedt\TimekeepingApi\Middleware\ErrorHandler;
use TorstenHettstedt\TimekeepingApi\Middleware\JsonBodyParserMiddleware;
$container = new Container();
$container->set('databases', function () {
$dsn = "pgsql:host=${_ENV['DATABASES_HOST']};port=5432;dbname=${_ENV['DATABASES_NAME']}";
return new PDO($dsn, $_ENV['DATABASES_USER'], $_ENV['DATABASES_PASS']);
});
AppFactory::setContainer($container);
$app = AppFactory::create(); $app = AppFactory::create();
$app->add(new JsonBodyParserMiddleware());
$app->get('/', 'TorstenHettstedt\TimekeepingApi\Controller\HelloWorldController:read'); $app->group('/views/working-hours', function (RouteCollectorProxy $group) {
$group->get('/weekly', WorkingHoursViewController::class . ':browseWeekly');
$group->get('/monthly', WorkingHoursViewController::class . ':browseMonthly');
$group->get('/yearly', WorkingHoursViewController::class . ':browseYearly');
});
$app->addErrorMiddleware(true, true, true); $app->group('/working-hours', function (RouteCollectorProxy $group) {
$group->get('', WorkingHoursController::class . ':browse');
$group->post('', WorkingHoursController::class . ':creat');
$group->group('/{id:\d\d\d\d-\d\d-\d\d}', function (RouteCollectorProxy $group) {
$group->get('', WorkingHoursController::class . ':read');
$group->put('', WorkingHoursController::class . ':update');
});
});
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorMiddleware->setDefaultErrorHandler(new ErrorHandler($app));
$app->run(); $app->run();
+5 -3
View File
@@ -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,12 +168,13 @@
"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": "string", "type": "string",
"pattern": "[0-5]\\d:[0-5]\\d:[0-5]\\d", "pattern": "[0-5]\\d:[0-5]\\d:[0-5]\\d",
+47
View File
@@ -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,30 +0,0 @@
<?php /** @noinspection PhpUnused */
namespace TorstenHettstedt\TimekeepingApi\Controller;
use Slim\Psr7\Response;
use Slim\Psr7\Request;
class HelloWorldController
{
/**
* @param Request $request
* @param Response $response
* @param mixed[] $args
*
* @return Response
*
* @noinspection PhpUnusedParameterInspection
*/
public function read(Request $request, Response $response, array $args): Response
{
$payload = json_encode('Hallo World');
$response->getBody()->write($payload);
return $response
->withHeader('Content-Type', 'application/json')
->withStatus(200);
}
}
@@ -0,0 +1,20 @@
<?php /** @noinspection PhpMissingFieldTypeInspection */
namespace TorstenHettstedt\TimekeepingApi\Controller;
use Slim\Exception\HttpSpecializedException;
class HttpConflictRequestException extends HttpSpecializedException
{
/** @var int */
protected $code = 409;
/** @var string */
protected $message = 'Conflict.';
/** @var string */
protected $title = '409 Conflict';
/** @var string */
protected $description = 'The 409 (Conflict) status code indicates that the request could not be completed due to a conflict with the current state of the target resource.';
}
@@ -0,0 +1,11 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Controller;
use Exception;
class NotDatabasesException extends Exception
{
}
@@ -0,0 +1,137 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Controller;
use DateInterval;
use DateTime;
use Exception;
use Fig\Http\Message\StatusCodeInterface;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpInternalServerErrorException;
use Slim\Exception\HttpNotFoundException;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordAlreadyExistException;
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException;
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursRepository;
class WorkingHoursController extends AbstractController
{
/**
* @param Request $request
* @param Response $response
* @param mixed[] $args
*
* @return Response
*
* @throws HttpBadRequestException
* @throws HttpNotFoundException
*/
public function update(Request $request, Response $response, array $args): Response
{
$body = $request->getParsedBody();
$repository = new WorkingHoursRepository($this->databases);
$model = $this->buildModel($request, $args['id'], $body['workingTime']);
try {
$repository->update($model);
} catch (RepositoryRecordNotFoundException $exception) {
throw new HttpNotFoundException($request, 'Der Eintrag ist nicht vorhanden.', $exception);
}
return $this->printResponse($response, $model, StatusCodeInterface::STATUS_OK);
}
/**
* @param Request $request
* @param Response $response
* @param mixed[] $args
*
* @return Response
*
* @throws HttpInternalServerErrorException
* @noinspection PhpUnusedParameterInspection
*/
public function browse(Request $request, Response $response, array $args): Response
{
$repository = new WorkingHoursRepository($this->databases);
try {
return $this->printResponse($response, $repository->findAll(), StatusCodeInterface::STATUS_OK);
} catch (Exception $exception) {
throw new HttpInternalServerErrorException($request, 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception);
}
}
/**
* @param Request $request
* @param Response $response
* @param mixed[] $args
*
* @return Response
*
* @throws HttpNotFoundException
* @throws HttpInternalServerErrorException
*/
public function read(Request $request, Response $response, array $args): Response
{
$repository = new WorkingHoursRepository($this->databases);
try {
return $this->printResponse($response, $repository->findByKey($args['id']), StatusCodeInterface::STATUS_OK);
} catch (RepositoryRecordNotFoundException $exception) {
throw new HttpNotFoundException($request, 'Der Eintrag ist nicht vorhanden.', $exception);
} catch (Exception $exception) {
throw new HttpInternalServerErrorException($request, 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception);
}
}
/**
* @param Request $request
* @param Response $response
* @param mixed[] $args
*
* @return Response
*
* @throws HttpBadRequestException
* @throws HttpConflictRequestException
* @throws HttpInternalServerErrorException
* @noinspection PhpUnusedParameterInspection
*/
public function creat(Request $request, Response $response, array $args): Response
{
$body = $request->getParsedBody();
$repository = new WorkingHoursRepository($this->databases);
$model = $this->buildModel($request, $body['workingDay'], $body['workingTime']);
try {
$repository->insert($model);
} catch (RepositoryRecordAlreadyExistException $exception) {
throw new HttpConflictRequestException($request, 'Der Eintrag ist schon vorhanden.', $exception);
} catch (Exception $exception) {
throw new HttpInternalServerErrorException($request, 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception);
}
return $this->printResponse($response, $model, StatusCodeInterface::STATUS_CREATED);
}
/**
* @param Request $request
* @param string $date
* @param string $time
*
* @return WorkingHours
* @throws HttpBadRequestException
*/
protected function buildModel(Request $request, string $date, string $time): WorkingHours
{
try {
$workingDay = new DateTime($date);
} catch (Exception $exception) {
throw new HttpBadRequestException($request, 'Der Wert für das Datum ist falsch', $exception);
}
try {
$workingTime = new DateInterval('P0000-00-00T' . $time);
} catch (Exception $exception) {
throw new HttpBadRequestException($request, 'Der Wert für die Zeit ist falsch', $exception);
}
return (new WorkingHours())->setWorkingDay($workingDay)->setWorkingTime($workingTime);
}
}
@@ -0,0 +1,80 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Controller;
use Exception;
use Fig\Http\Message\StatusCodeInterface;
use Slim\Exception\HttpInternalServerErrorException;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursMonthlyViewRepository;
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursWeeklyViewRepository;
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursYearlyViewRepository;
class WorkingHoursViewController extends AbstractController
{
/**
* @param Request $request
* @param Response $response
* @param mixed[] $args
*
* @return Response
*
* @throws HttpInternalServerErrorException
* @noinspection PhpUnusedParameterInspection
*/
public function browseWeekly(Request $request, Response $response, array $args): Response
{
$repository = new WorkingHoursWeeklyViewRepository($this->databases);
try {
return $this->printResponse($response, $repository->findAll(), StatusCodeInterface::STATUS_OK);
} catch (Exception $exception) {
throw new HttpInternalServerErrorException($request,
'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception);
}
}
/**
* @param Request $request
* @param Response $response
* @param mixed[] $args
*
* @return Response
*
* @throws HttpInternalServerErrorException
* @noinspection PhpUnusedParameterInspection
*/
public function browseMonthly(Request $request, Response $response, array $args): Response
{
$repository = new WorkingHoursMonthlyViewRepository($this->databases);
try {
return $this->printResponse($response, $repository->findAll(), StatusCodeInterface::STATUS_OK);
} catch (Exception $exception) {
throw new HttpInternalServerErrorException($request,
'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception);
}
}
/**
* @param Request $request
* @param Response $response
* @param mixed[] $args
*
* @return Response
*
* @throws HttpInternalServerErrorException
* @noinspection PhpUnusedParameterInspection
*/
public function browseYearly(Request $request, Response $response, array $args): Response
{
$repository = new WorkingHoursYearlyViewRepository($this->databases);
try {
return $this->printResponse($response, $repository->findAll(), StatusCodeInterface::STATUS_OK);
} catch (Exception $exception) {
throw new HttpInternalServerErrorException($request,
'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception);
}
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php /** @noinspection PhpUndefinedClassInspection */
namespace TorstenHettstedt\TimekeepingApi\Middleware;
use JetBrains\PhpStorm\ArrayShape;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Slim\App;
use Slim\Exception\HttpException;
use Throwable;
class ErrorHandler
{
protected App $app;
/**
* ErrorHandler constructor.
*
* @param App $app
*/
public function __construct(App $app)
{
$this->app = $app;
}
/**
* @param Throwable $exception
*
* @return mixed[]
*/
#[ArrayShape([
'timestamp' => "false|string",
'status' => "int",
"error" => "string",
'message' => "string",
'path' => "string"
])] protected function buildJsonArray(Throwable $exception): array
{
$payload = [
'timestamp' => date('Y-m-d\TH:m:sP'),
'status' => $exception->getCode(),
"error" => '',
'message' => $exception->getMessage(),
'path' => $exception->getFile()
];
if ($exception instanceof HttpException) {
$payload['error'] = $exception->getTitle();
}
return $payload;
}
/**
* @param ServerRequestInterface $request
* @param Throwable $exception
* @param bool $displayErrorDetails
* @param bool $logErrors
* @param bool $logErrorDetails
* @param LoggerInterface|null $logger
*
* @return ResponseInterface
*/
public function __invoke(
ServerRequestInterface $request,
Throwable $exception,
bool $displayErrorDetails,
bool $logErrors,
bool $logErrorDetails,
?LoggerInterface $logger = null
): ResponseInterface
{
if ($logger !== null && $logErrors === true) {
$logger->error($exception->getMessage());
}
$payload = $this->buildJsonArray($exception);
$response = $this->app->getResponseFactory()->createResponse($exception->getCode());
$response->getBody()->write(
json_encode($payload, JSON_UNESCAPED_UNICODE)
);
return $response;
}
}
@@ -0,0 +1,25 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
class JsonBodyParserMiddleware implements MiddlewareInterface
{
public function process(Request $request, RequestHandler $handler): Response
{
$contentType = $request->getHeaderLine('Content-Type');
if (strstr($contentType, 'application/json')) {
$contents = json_decode(file_get_contents('php://input'), true);
if (json_last_error() === JSON_ERROR_NONE) {
$request = $request->withParsedBody($contents);
}
}
return $handler->handle($request);
}
}
@@ -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;
@@ -85,7 +85,7 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri
/** /**
* @param WorkingHours|ModelInterface $model * @param WorkingHours|ModelInterface $model
* *
* @throws RepositoryModelAlreadyExists * @throws RepositoryRecordAlreadyExistException
* @throws Exception * @throws Exception
*/ */
public function insert(mixed $model): void public function insert(mixed $model): void
@@ -99,7 +99,7 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri
try { try {
$this->findByKey($workingDay); $this->findByKey($workingDay);
throw new RepositoryModelAlreadyExists(); throw new RepositoryRecordAlreadyExistException();
} /** @noinspection PhpUnusedLocalVariableInspection */ } /** @noinspection PhpUnusedLocalVariableInspection */
catch (RepositoryRecordNotFoundException $exception) { catch (RepositoryRecordNotFoundException $exception) {
$stmt = $this->database->prepare('insert into public."Arbeitszeiten" ("Datum", "Arbeitszeit") values (?, ?) on conflict do nothing'); $stmt = $this->database->prepare('insert into public."Arbeitszeiten" ("Datum", "Arbeitszeit") values (?, ?) on conflict do nothing');
+7 -6
View File
@@ -9,11 +9,12 @@ use Codeception\Module;
class Api extends Module class Api extends Module
{ {
const FORMAT_TIME = 'string:regex([0-5]\d:[0-5]\d:[0-5]\d)'; 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 = [ const WORKING_HOURS_JSON_FORMAT = [
'date' => 'string:date', 'workingDay' => self::FORMAT_DATE,
'workingHours' => self::FORMAT_TIME, 'workingTime' => self::FORMAT_TIME,
]; ];
const WORKING_HOURS_VIEW_JSON_FORMAT = [ const WORKING_HOURS_VIEW_JSON_FORMAT = [
@@ -25,10 +26,10 @@ class Api extends Module
]; ];
const ERROR_JSON_FORMAT = [ const ERROR_JSON_FORMAT = [
"timestamp" => "string:date-time", "timestamp" => "string:date",
"status" => 0, "status" => "integer",
"error" => "string", "error" => "string",
"message" => "string", "message" => "string",
"path" => "string:path", "path" => "string",
]; ];
} }
+1 -1
View File
@@ -3,6 +3,6 @@ modules:
enabled: enabled:
- \Helper\Api - \Helper\Api
- REST: - REST:
url: http://localhost:8090/ url: http://localhost:8091/
depends: PhpBrowser depends: PhpBrowser
part: Json part: Json
@@ -13,8 +13,8 @@ class CreateWorkingHoursCest
$I->haveHttpHeader('accept', 'application/json'); $I->haveHttpHeader('accept', 'application/json');
$I->haveHttpHeader('content-type', 'application/json'); $I->haveHttpHeader('content-type', 'application/json');
$I->sendPost('/working-hours', [ $I->sendPost('/working-hours', [
'date' => '2020-04-01', 'workingDay' => '2020-04-01',
'workingHours' => '08:00:00', 'workingTime' => '08:00:00',
]); ]);
$I->seeResponseCodeIs(HttpCode::CREATED); $I->seeResponseCodeIs(HttpCode::CREATED);
$I->seeResponseIsJson(); $I->seeResponseIsJson();
@@ -27,8 +27,8 @@ class CreateWorkingHoursCest
$I->haveHttpHeader('accept', 'application/json'); $I->haveHttpHeader('accept', 'application/json');
$I->haveHttpHeader('content-type', 'application/json'); $I->haveHttpHeader('content-type', 'application/json');
$I->sendPost('/working-hours', [ $I->sendPost('/working-hours', [
'date' => '2020-04-01', 'workingDay' => '2020-04-01',
'workingHours' => 8.0, 'workingTime' => 8.0,
]); ]);
$I->seeResponseCodeIs(HttpCode::BAD_REQUEST); $I->seeResponseCodeIs(HttpCode::BAD_REQUEST);
$I->seeResponseIsJson(); $I->seeResponseIsJson();
@@ -41,8 +41,8 @@ class CreateWorkingHoursCest
$I->haveHttpHeader('accept', 'application/json'); $I->haveHttpHeader('accept', 'application/json');
$I->haveHttpHeader('content-type', 'application/json'); $I->haveHttpHeader('content-type', 'application/json');
$I->sendPost('/working-hours', [ $I->sendPost('/working-hours', [
'date' => '2020-01-15', 'workingDay' => '2020-01-15',
'workingHours' => '08:00:00', 'workingTime' => '08:00:00',
]); ]);
$I->seeResponseCodeIs(HttpCode::CONFLICT); $I->seeResponseCodeIs(HttpCode::CONFLICT);
$I->seeResponseIsJson(); $I->seeResponseIsJson();
@@ -13,8 +13,8 @@ class UpdateWorkingHoursCest
$I->haveHttpHeader('accept', 'application/json'); $I->haveHttpHeader('accept', 'application/json');
$I->haveHttpHeader('content-type', 'application/json'); $I->haveHttpHeader('content-type', 'application/json');
$I->sendPut('/working-hours/2020-01-15', [ $I->sendPut('/working-hours/2020-01-15', [
'date' => '2020-01-15', 'workingDay' => '2020-01-15',
'workingHours' => '10:00:00', 'workingTime' => '10:00:00',
]); ]);
$I->seeResponseCodeIs(HttpCode::OK); $I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseIsJson(); $I->seeResponseIsJson();
@@ -25,9 +25,9 @@ class UpdateWorkingHoursCest
{ {
$I->haveHttpHeader('accept', 'application/json'); $I->haveHttpHeader('accept', 'application/json');
$I->haveHttpHeader('content-type', 'application/json'); $I->haveHttpHeader('content-type', 'application/json');
$I->sendPut('/working-hours/2020-01-15', [ $I->sendPut('/working-hours/2020-04-15', [
'date' => '2020-04-15', 'workingDay' => '2020-04-15',
'workingHours' => '10:00:00', 'workingTime' => '10:00:00',
]); ]);
$I->seeResponseCodeIs(HttpCode::NOT_FOUND); $I->seeResponseCodeIs(HttpCode::NOT_FOUND);
$I->seeResponseIsJson(); $I->seeResponseIsJson();
@@ -39,8 +39,8 @@ class UpdateWorkingHoursCest
$I->haveHttpHeader('accept', 'application/json'); $I->haveHttpHeader('accept', 'application/json');
$I->haveHttpHeader('content-type', 'application/json'); $I->haveHttpHeader('content-type', 'application/json');
$I->sendPut('/working-hours/2020-01-15', [ $I->sendPut('/working-hours/2020-01-15', [
'date' => '2020-01-15', 'workingDay' => '2020-01-15',
'workingHours' => 8.0, 'workingTime' => 8.0,
]); ]);
$I->seeResponseCodeIs(HttpCode::BAD_REQUEST); $I->seeResponseCodeIs(HttpCode::BAD_REQUEST);
$I->seeResponseIsJson(); $I->seeResponseIsJson();
@@ -0,0 +1,222 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Controller;
use Codeception\Test\Unit;
use Exception;
use PDO;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\StreamInterface;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpInternalServerErrorException;
use Slim\Exception\HttpNotFoundException;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
use TorstenHettstedt\TimekeepingApi\Controller\HttpConflictRequestException;
use TorstenHettstedt\TimekeepingApi\Controller\NotDatabasesException;
use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursController;
class WorkingHoursControllerTest extends Unit
{
const EXISTING_DATE = '2020-01-28';
const EXISTING_INTERVAL = '07:20:00';
const NEW_DATE = '2020-04-01';
const NEW_INTERVAL = '07:59:00';
protected ContainerInterface $container;
protected Request $request;
protected Response $response;
/**
* @throws Exception
*/
protected function _before(): void
{
parent::_before();
/** @noinspection SpellCheckingInspection */
$this->container = $this->makeEmpty(ContainerInterface::class, [
'has' => true,
'get' => new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass')
]);
$this->request = $this->makeEmpty(Request::class, [
'getParsedBody' => [
'workingDay' => self::EXISTING_DATE,
'workingTime' => self::EXISTING_INTERVAL,
]
]);
$this->response = $this->makeEmpty(Response::class, [
'getBody' => $this->makeEmpty(StreamInterface::class, [
'write' => function (mixed $data) {
$this->assertIsString($data);
$this->assertJson($data);
},
]),
'withHeader' => $this->makeEmpty(Response::class, [
'withStatus' => $this->makeEmpty(Response::class),
]),
]);
}
/**
* @throws NotDatabasesException
* @throws Exception
*/
public function testConstructWithNonDatabase(): void
{
$this->container = $this->makeEmpty(ContainerInterface::class, [
'has' => false
]);
$this->expectException(NotDatabasesException::class);
new WorkingHoursController($this->container);
}
/**
* @throws NotDatabasesException
* @throws Exception
*/
public function testBrowse(): void
{
$controller = new WorkingHoursController($this->container);
$response = $controller->browse($this->request, $this->response, []);
$this->assertInstanceOf(Response::class, $response);
}
/**
* @throws NotDatabasesException
* @throws HttpBadRequestException
* @throws HttpNotFoundException
*/
public function testUpdateExistRecord(): void
{
$controller = new WorkingHoursController($this->container);
$response = $controller->update($this->request, $this->response, [
'id' => self::EXISTING_DATE
]);
$this->assertInstanceOf(Response::class, $response);
}
/**
* @throws NotDatabasesException
* @throws HttpBadRequestException
* @throws HttpNotFoundException
*/
public function testUpdateNotExistRecord(): void
{
$controller = new WorkingHoursController($this->container);
$this->expectException(HttpNotFoundException::class);
$controller->update($this->request, $this->response, [
'id' => self::NEW_DATE
]);
}
/**
* @return string[][]
*/
public function invalidDataProvider(): array
{
return [
[
self::NEW_DATE,
'07:59'
],
[
'2020-13-33',
self::NEW_INTERVAL
],
];
}
/**
* @param string $date
* @param string $time
*
* @dataProvider invalidDataProvider
*
* @throws HttpBadRequestException
* @throws HttpConflictRequestException
* @throws HttpInternalServerErrorException
* @throws NotDatabasesException
* @throws Exception
*/
public function testCreatInvalidData(string $date, string $time): void
{
$this->request = $this->makeEmpty(Request::class, [
'getParsedBody' => [
'workingDay' => $date,
'workingTime' => $time,
]
]);
$controller = new WorkingHoursController($this->container);
$this->expectException(HttpBadRequestException::class);
$controller->creat($this->request, $this->response, []);
}
/**
* @throws HttpBadRequestException
* @throws NotDatabasesException
* @throws HttpInternalServerErrorException
* @throws HttpConflictRequestException
* @throws Exception
*/
public function testCreatNewRecord(): void
{
$this->request = $this->makeEmpty(Request::class, [
'getParsedBody' => [
'workingDay' => self::NEW_DATE,
'workingTime' => self::NEW_INTERVAL,
]
]);
$controller = new WorkingHoursController($this->container);
$response = $controller->creat($this->request, $this->response, []);
$this->assertInstanceOf(Response::class, $response);
}
/**
* @throws HttpBadRequestException
* @throws HttpConflictRequestException
* @throws HttpInternalServerErrorException
* @throws NotDatabasesException
* @throws Exception
*/
public function testCreatExistRecord(): void
{
$this->request = $this->makeEmpty(Request::class, [
'getParsedBody' => [
'workingDay' => self::EXISTING_DATE,
'workingTime' => self::EXISTING_INTERVAL,
]
]);
$controller = new WorkingHoursController($this->container);
$this->expectException(HttpConflictRequestException::class);
$controller->creat($this->request, $this->response, []);
}
/**
* @throws HttpInternalServerErrorException
* @throws HttpNotFoundException
* @throws NotDatabasesException
*/
public function testReadExistRecord(): void
{
$controller = new WorkingHoursController($this->container);
$response = $controller->read($this->request, $this->response, [
'id' => self::EXISTING_DATE
]);
$this->assertInstanceOf(Response::class, $response);
}
/**
* @throws HttpInternalServerErrorException
* @throws HttpNotFoundException
* @throws NotDatabasesException
*/
public function testReadNotExistRecord(): void
{
$controller = new WorkingHoursController($this->container);
$this->expectException(HttpNotFoundException::class);
$controller->read($this->request, $this->response, [
'id' => self::NEW_DATE
]);
}
}
@@ -0,0 +1,88 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Controller;
use Codeception\Test\Unit;
use Exception;
use PDO;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\StreamInterface;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
use TorstenHettstedt\TimekeepingApi\Controller\NotDatabasesException;
use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursViewController;
class WorkingHoursViewControllerTest extends Unit
{
protected ContainerInterface $container;
protected Request $request;
protected Response $response;
/**
* @throws Exception
*/
protected function _before(): void
{
parent::_before();
/** @noinspection SpellCheckingInspection */
$this->container = $this->makeEmpty(ContainerInterface::class, [
'has' => true,
'get' => new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass')
]);
$this->request = $this->makeEmpty(Request::class, []);
$this->response = $this->makeEmpty(Response::class, [
'getBody' => $this->makeEmpty(StreamInterface::class, [
'write' => function (mixed $data) {
$this->assertIsString($data);
$this->assertJson($data);
},
]),
'withHeader' => $this->makeEmpty(Response::class, [
'withStatus' => $this->makeEmpty(Response::class),
]),
]);
}
/**
* @throws NotDatabasesException
* @throws Exception
*/
public function testConstructWithNonDatabase(): void
{
$this->container = $this->makeEmpty(ContainerInterface::class, [
'has' => false
]);
$this->expectException(NotDatabasesException::class);
new WorkingHoursViewController($this->container);
}
/**
* @throws Exception
*/
public function testBrowseMonthly(): void
{
$controller = new WorkingHoursViewController($this->container);
$response = $controller->browseMonthly($this->request, $this->response, []);
$this->assertInstanceOf(Response::class, $response);
}
/**
* @throws Exception
*/
public function testBrowseYearly(): void
{
$controller = new WorkingHoursViewController($this->container);
$response = $controller->browseYearly($this->request, $this->response, []);
$this->assertInstanceOf(Response::class, $response);
}
/**
* @throws Exception
*/
public function testBrowseWeekly(): void
{
$controller = new WorkingHoursViewController($this->container);
$response = $controller->browseWeekly($this->request, $this->response, []);
$this->assertInstanceOf(Response::class, $response);
}
}
@@ -0,0 +1,97 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Middleware;
use Codeception\Stub\Expected;
use Codeception\Test\Unit;
use Exception;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Log\LoggerInterface;
use Slim\App;
use Slim\Exception\HttpException;
use TorstenHettstedt\TimekeepingApi\Middleware\ErrorHandler;
class ErrorHandlerTest extends Unit
{
protected App $app;
protected ServerRequestInterface $request;
protected Exception $exception;
protected LoggerInterface $logger;
/**
* @throws Exception
*/
protected function _before(): void
{
parent::_before();
$this->app = $this->makeEmpty(App::class, [
'getResponseFactory' => $this->makeEmpty(ResponseFactoryInterface::class, [
'createResponse' => $this->makeEmpty(ResponseInterface::class, [
'getBody' => $this->makeEmpty(StreamInterface::class, [
'write' => function (mixed $data) {
$this->assertIsString($data);
$this->assertJson($data);
},
]),
]),
]),
]);
$this->request = $this->makeEmpty(ServerRequestInterface::class);
}
/**
* @throws Exception
*/
public function testUseWithLogger(): void
{
$this->exception = $this->make(Exception::class, [
'message' => '',
'code' => 400,
'file' => '/path(to/file',
]);
$this->logger = $this->makeEmpty(LoggerInterface::class, [
'error' => Expected::once(),
]);
$middleWare = new ErrorHandler($this->app);
$middleWare($this->request, $this->exception, true, true, true, $this->logger);
}
/**
* @throws Exception
*/
public function testUseWithHttpException(): void
{
$this->exception = $this->make(HttpException::class, [
'message' => '',
'code' => 400,
'file' => '/path(to/file',
'getTitle' => Expected::once('The Title'),
]);
$this->logger = $this->makeEmpty(LoggerInterface::class, []);
$middleWare = new ErrorHandler($this->app);
$middleWare($this->request, $this->exception, true, true, true, $this->logger);
}
/**
* @throws Exception
*/
public function testUseWithGeneralException(): void
{
$this->exception = $this->make(Exception::class, [
'message' => '',
'code' => 400,
'file' => '/path(to/file',
]);
$this->logger = $this->makeEmpty(LoggerInterface::class, []);
$middleWare = new ErrorHandler($this->app);
$middleWare($this->request, $this->exception, true, true, true, $this->logger);
}
}
@@ -8,10 +8,9 @@ use DateTime;
use Exception; use Exception;
use InvalidArgumentException; use InvalidArgumentException;
use PDO; use PDO;
use PDOStatement;
use TorstenHettstedt\TimekeepingApi\Models\ModelInterface; 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;
@@ -24,7 +23,6 @@ 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
{ {
@@ -104,7 +102,7 @@ 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);
} }
+13
View File
@@ -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
+11
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
# Url ohne abschlißenden Slash
API_URL= http://localhost:8080
+6
View File
@@ -0,0 +1,6 @@
.DS_Store
node_modules
package-lock.json
yarn.lock
public
/.env
+30
View File
@@ -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

+1
View File
@@ -0,0 +1 @@
+21
View File
@@ -0,0 +1,21 @@
<!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">
<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>
+51
View File
@@ -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
}
};
+67
View File
@@ -0,0 +1,67 @@
<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);
let r = {
"workingDay": "2020-01-15",
"workingTime": "09:00:00"
};
let records = {
"title" : "Einträge",
"records" : [r, r]
};
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'), records))
.listen();
onDestroy(router.unlisten);
</script>
+15
View File
@@ -0,0 +1,15 @@
<!--suppress HtmlUnknownTarget -->
<nav>
<ul>
<li><a class="{ isActive('home') }" href="/">Home</a></li>
<li><a class="{ isActive('weekly') }" href="/views/weekly">Wochenansicht</a></li>
<li><a class="{ isActive('monthly') }" href="/views/monthly">Monatsansicht</a></li>
<li><a class="{ isActive('yearly') }" href="/views/yearly">Jahresansicht</a></li>
<li><a class="{ isActive('working-hours') }" href="/working-hours">Einträge</a></li>
</ul>
</nav>
<script>
export let active;
$: isActive = str => active === str ? 'selected' : '';
</script>
+5
View File
@@ -0,0 +1,5 @@
import App from './components/App.svelte';
new App({
target: document.body
});
+5
View File
@@ -0,0 +1,5 @@
<Views />
<script>
import Views from "./WeeklyViews.svelte";
</script>
+31
View File
@@ -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} />
+40
View File
@@ -0,0 +1,40 @@
<script>
export let params;
console.log(params)
</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>
<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>{record.overtime}</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</article>
</section>
+31
View File
@@ -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} />
+197
View File
@@ -0,0 +1,197 @@
<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 => {
activeRecord = null;
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) {
Records.browse();
}
})
.catch(err => {
console.log(err);
});
},
'add': record => {
console.log(record);
// noinspection JSUnresolvedVariable
fetch(env.API_URL + '/working-hours', {
method: 'POST',
mode: 'no-cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
referrerPolicy: 'no-referrer',
body: JSON.stringify(record)
})
.then(res => {
if (!res.ok) {
throw new Error('Fail!');
}
})
.then(() => {
newRecord = false;
Records.browse();
})
.catch(err => {
console.log(err);
});
},
'update': (date, record) => {
console.log(record);
// noinspection JSUnresolvedVariable
fetch(env.API_URL + '/working-hours/' + date, {
method: 'PUT',
mode: 'no-cors',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
referrerPolicy: 'no-referrer',
body: JSON.stringify(record)
})
.then(res => {
if (!res.ok) {
throw new Error('Fail!');
}
})
.then(() => {
Records.browse();
})
.catch(err => {
console.log(err);
});
}
};
Records.browse();
</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={() => {activeRecord = {}; newRecord = true}}>Neuer Eintrag
<IconifyIcon icon={documentIcon}/>
</button>
</fieldset>
</form>
<table>
<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={() => Records.read(record.workingDay)}>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} placeholder="Arbeitstag" type="date"
id="workingDay">
</div>
<div>
<label for="workingTime">Arbeitszeit</label>
<input bind:value={activeRecord.workingTime} placeholder="Arbeitszeit" type="time"
id="workingTime">
</div>
</fieldset>
<fieldset name="buttons">
{#if newRecord === true}
<button on:click={() => Records.add(activeRecord)}>Übernehmen
<IconifyIcon icon={checkIcon} color="green"/>
</button>
{:else}
<button on:click={() => Records.update(activeRecord.workingDay, activeRecord)}>Übernehmen
<IconifyIcon icon={checkIcon} color="green"/>
</button>
{/if}
<button on:click={() => Records.read(null)}>Abbrechen
<IconifyIcon icon={xIcon}/>
</button>
</fieldset>
</form>
</article>
</section>
{/if}
+31
View File
@@ -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} />