diff --git a/.gitignore b/.gitignore index 9ffe02b..87e8821 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,7 @@ Temporary Items /api/tests/_* /api/tests/*.suite.yml /api/.env +/ui/static/print.css* +/ui/static/global.css* +/ui/static/bundle.css* +/ui/.svelte-kit/ diff --git a/api/Dockerfile b/api/Dockerfile index 4d8c4ea..e269c7e 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.0-apache +FROM php:8.4-apache-bookworm RUN apt update && apt install -y libpq-dev RUN docker-php-ext-install -j$(nproc) pdo pdo_pgsql diff --git a/api/composer.json b/api/composer.json index d4a5b86..b1712b5 100644 --- a/api/composer.json +++ b/api/composer.json @@ -16,21 +16,22 @@ } ], "require": { + "php": "8.4.*", "slim/slim": "^4.7.1", - "vlucas/phpdotenv": "^4.2", - "slim/psr7": "^1.3", + "vlucas/phpdotenv": "^v5.6.1", + "slim/psr7": "^1.7.0", "php-di/slim-bridge": "^3.1.0", "jetbrains/phpstorm-attributes": "^1.0.0", "myclabs/php-enum": "^1.8.0", "ext-pdo": "*" }, "require-dev": { - "phpstan/phpstan": "^0.12.80", - "codeception/codeception": "^4.1.18", - "codeception/module-phpbrowser": "^1.0.0", - "codeception/module-asserts": "^1.0.0", - "codeception/module-db": "^1.1.0", - "codeception/module-rest": "^1.2.8" + "phpstan/phpstan": "^2.1.12", + "codeception/codeception": "^5.2.1", + "codeception/module-phpbrowser": "^3.0.1", + "codeception/module-asserts": "^3.1.0", + "codeception/module-db": "^3.2.2", + "codeception/module-rest": "^3.4.1" }, "autoload": { "psr-4": { diff --git a/api/html/index.php b/api/html/index.php index cbd9aca..addc91c 100644 --- a/api/html/index.php +++ b/api/html/index.php @@ -17,7 +17,7 @@ use TorstenHettstedt\TimekeepingApi\Middleware\OriginAccessControlHandler; $container = new Container(); $container->set('databases', function () { - $dsn = "pgsql:host=${_ENV['DATABASES_HOST']};port=5432;dbname=${_ENV['DATABASES_NAME']}"; + $dsn = "pgsql:host={$_ENV['DATABASES_HOST']};port=5432;dbname={$_ENV['DATABASES_NAME']}"; return new PDO($dsn, $_ENV['DATABASES_USER'], $_ENV['DATABASES_PASS']); }); diff --git a/api/src/Controller/AbstractController.php b/api/src/Controller/AbstractController.php index c0cf8a9..b9af4ce 100644 --- a/api/src/Controller/AbstractController.php +++ b/api/src/Controller/AbstractController.php @@ -4,7 +4,9 @@ namespace TorstenHettstedt\TimekeepingApi\Controller; use JsonSerializable; use PDO; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; use Slim\Psr7\Response; abstract class AbstractController @@ -18,6 +20,8 @@ abstract class AbstractController * @param ContainerInterface $container * * @throws NotDatabasesException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ public function __construct(ContainerInterface $container) { @@ -34,7 +38,8 @@ abstract class AbstractController * * @return Response */ - protected function printResponse(Response $response, mixed $data, int $status_code): Response { + protected function printResponse(Response $response, mixed $data, int $status_code): Response + { $payload = json_encode($data); $response->getBody()->write($payload); diff --git a/api/src/Controller/HttpConflictRequestException.php b/api/src/Controller/HttpConflictRequestException.php index f995249..8315fe4 100644 --- a/api/src/Controller/HttpConflictRequestException.php +++ b/api/src/Controller/HttpConflictRequestException.php @@ -8,13 +8,8 @@ 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.'; + protected string $title = '409 Conflict'; + protected string $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.'; } diff --git a/api/src/Controller/PreflightController.php b/api/src/Controller/PreflightController.php index 1ac6bf9..d017f7a 100644 --- a/api/src/Controller/PreflightController.php +++ b/api/src/Controller/PreflightController.php @@ -11,6 +11,7 @@ use TorstenHettstedt\TimekeepingApi\Middleware\OriginAccessControlHandler; class PreflightController { + /** @noinspection PhpUnused */ public function preflight(Request $request, Response $response): ResponseInterface { return (new OriginAccessControlHandler())->originAccessControl($request, $response); diff --git a/api/src/Controller/WorkingHoursController.php b/api/src/Controller/WorkingHoursController.php index be291db..7b14b97 100644 --- a/api/src/Controller/WorkingHoursController.php +++ b/api/src/Controller/WorkingHoursController.php @@ -21,9 +21,9 @@ class WorkingHoursController extends AbstractController { /** - * @param Request $request - * @param Response $response - * @param mixed[] $args + * @param Request $request + * @param Response $response + * @param array $args * * @return Response * @@ -44,9 +44,9 @@ class WorkingHoursController extends AbstractController } /** - * @param Request $request - * @param Response $response - * @param mixed[] $args + * @param Request $request + * @param Response $response + * @param array $args * * @return Response * @@ -59,21 +59,29 @@ class WorkingHoursController extends AbstractController $repository = new WorkingHoursRepository($this->databases); $queryParams = $request->getQueryParams(); try { - return $this->printResponse($response, $repository->findFiltered( - $queryParams['start-date'] ?? null, - $queryParams['end-date'] ?? null - ), StatusCodeInterface::STATUS_OK); + return $this->printResponse( + $response, + $repository->findFiltered( + $queryParams['start-date'] ?? null, + $queryParams['end-date'] ?? null + ), + StatusCodeInterface::STATUS_OK + ); } catch (RepositoryBadWhereDataException $exception) { throw new HttpBadRequestException($request, 'Ein Wert für das Datum im Query ist ungültig', $exception); } catch (Exception $exception) { - throw new HttpInternalServerErrorException($request, 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $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 + * @param Request $request + * @param Response $response + * @param array $args * * @return Response * @@ -88,14 +96,18 @@ class WorkingHoursController extends AbstractController } 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); + 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 + * @param Request $request + * @param Response $response + * @param array $args * * @return Response * @@ -114,7 +126,11 @@ class WorkingHoursController extends AbstractController } 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); + 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); } diff --git a/api/src/Controller/WorkingHoursViewController.php b/api/src/Controller/WorkingHoursViewController.php index 917b163..ff26ca0 100644 --- a/api/src/Controller/WorkingHoursViewController.php +++ b/api/src/Controller/WorkingHoursViewController.php @@ -15,9 +15,9 @@ use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursYearlyViewRepositor class WorkingHoursViewController extends AbstractController { /** - * @param Request $request - * @param Response $response - * @param mixed[] $args + * @param Request $request + * @param Response $response + * @param array $args * * @return Response * @@ -30,15 +30,17 @@ class WorkingHoursViewController extends AbstractController 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); + 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 + * @param Request $request + * @param Response $response + * @param array $args * * @return Response * @@ -51,15 +53,17 @@ class WorkingHoursViewController extends AbstractController 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); + 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 + * @param Request $request + * @param Response $response + * @param array $args * * @return Response * @@ -72,8 +76,10 @@ class WorkingHoursViewController extends AbstractController 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); + throw new HttpInternalServerErrorException( + $request, + 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception + ); } } diff --git a/api/src/Middleware/ErrorHandler.php b/api/src/Middleware/ErrorHandler.php index 80d5f15..38985df 100644 --- a/api/src/Middleware/ErrorHandler.php +++ b/api/src/Middleware/ErrorHandler.php @@ -1,10 +1,12 @@ - + */ protected App $app; /** * ErrorHandler constructor. * - * @param App $app + * @param App $app */ public function __construct(App $app) { @@ -29,7 +34,7 @@ class ErrorHandler /** * @param Throwable $exception * - * @return mixed[] + * @return array */ #[ArrayShape([ 'timestamp' => "false|string", @@ -71,8 +76,7 @@ class ErrorHandler bool $logErrors, bool $logErrorDetails, ?LoggerInterface $logger = null - ): ResponseInterface - { + ): ResponseInterface { if ($logger !== null && $logErrors === true) { $logger->error($exception->getMessage()); } diff --git a/api/src/Middleware/JsonBodyParserMiddleware.php b/api/src/Middleware/JsonBodyParserMiddleware.php index 1c020e6..e09d5ec 100644 --- a/api/src/Middleware/JsonBodyParserMiddleware.php +++ b/api/src/Middleware/JsonBodyParserMiddleware.php @@ -13,7 +13,7 @@ class JsonBodyParserMiddleware implements MiddlewareInterface { $contentType = $request->getHeaderLine('Content-Type'); - if (strstr($contentType, 'application/json')) { + if (str_contains($contentType, 'application/json')) { $contents = json_decode(file_get_contents('php://input'), true); if (json_last_error() === JSON_ERROR_NONE) { $request = $request->withParsedBody($contents); diff --git a/api/src/Middleware/OriginAccessControlHandler.php b/api/src/Middleware/OriginAccessControlHandler.php index a62de79..6eb301c 100644 --- a/api/src/Middleware/OriginAccessControlHandler.php +++ b/api/src/Middleware/OriginAccessControlHandler.php @@ -28,15 +28,14 @@ class OriginAccessControlHandler implements MiddlewareInterface 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; + // 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); @@ -53,6 +52,6 @@ class OriginAccessControlHandler implements MiddlewareInterface protected function buildOrigin(Request $request): string { - return $request->getHeaderLine('Origin') ?? ''; + return $request->getHeaderLine('Origin'); } } \ No newline at end of file diff --git a/api/src/Models/PeriodDesignationEnum.php b/api/src/Models/PeriodDesignationEnum.php index 8efffb6..b0d93a7 100644 --- a/api/src/Models/PeriodDesignationEnum.php +++ b/api/src/Models/PeriodDesignationEnum.php @@ -1,23 +1,16 @@ - - * - * @method static PeriodDesignationEnum WEEKLY() - * @method static PeriodDesignationEnum MONTHLY() - * @method static PeriodDesignationEnum YEARLY() */ -class PeriodDesignationEnum extends Enum +enum PeriodDesignationEnum: string { - private const WEEKLY = 'Y#W'; - private const MONTHLY = 'Y-m'; - private const YEARLY = 'Y'; + case WEEKLY = 'Y#W'; + case MONTHLY = 'Y-m'; + case YEARLY = 'Y'; } \ No newline at end of file diff --git a/api/src/Models/WorkingHours.php b/api/src/Models/WorkingHours.php index e485f31..e55e5b4 100644 --- a/api/src/Models/WorkingHours.php +++ b/api/src/Models/WorkingHours.php @@ -10,42 +10,26 @@ use JetBrains\PhpStorm\ArrayShape; class WorkingHours implements ModelInterface { - protected ?DateTime $workingDay = null; + protected ?DateTime $workingDay = null; protected ?DateInterval $workingTime = null; - /** - * @return DateTime|null - */ public function getWorkingDay(): ?DateTime { return $this->workingDay; } - /** - * @param DateTime $workingDay - * - * @return WorkingHours - */ - public function setWorkingDay(DateTime $workingDay): WorkingHours + public function setWorkingDay(DateTime $workingDay): static { $this->workingDay = $workingDay; return $this; } - /** - * @return DateInterval|null - */ public function getWorkingTime(): ?DateInterval { return $this->workingTime; } - /** - * @param DateInterval $workingTime - * - * @return WorkingHours - */ - public function setWorkingTime(DateInterval $workingTime): WorkingHours + public function setWorkingTime(DateInterval $workingTime): static { $this->workingTime = $workingTime; return $this; @@ -58,7 +42,7 @@ class WorkingHours implements ModelInterface public function jsonSerialize(): array { return [ - 'workingDay' => $this->getWorkingDay()->format('Y-m-d'), + 'workingDay' => $this->getWorkingDay()->format('Y-m-d'), 'workingTime' => $this->getWorkingTime()->format('%H:%I:%S') ]; } diff --git a/api/src/Models/WorkingHoursView.php b/api/src/Models/WorkingHoursView.php index b229ec8..77bf15d 100644 --- a/api/src/Models/WorkingHoursView.php +++ b/api/src/Models/WorkingHoursView.php @@ -10,70 +10,36 @@ use JetBrains\PhpStorm\ArrayShape; class WorkingHoursView implements ModelInterface { - protected DateTimeInterface $period; - protected PeriodDesignationEnum $periodDesignation; - protected int $workingDays; - protected DateInterval $totalHours; - protected DateInterval $overtime; - /** - * WorkingHoursView constructor. - * - * @param DateTimeInterface $period - * @param PeriodDesignationEnum $periodDesignation - * @param int $workingDays - * @param DateInterval $totalHours - * @param DateInterval $overtime - */ public function __construct( - DateTimeInterface $period, - PeriodDesignationEnum $periodDesignation, - int $workingDays, - DateInterval $totalHours, - DateInterval $overtime + protected DateTimeInterface $period, + protected PeriodDesignationEnum $periodDesignation, + protected int $workingDays, + protected DateInterval $totalHours, + protected DateInterval $overtime ) { - $this->period = $period; - $this->periodDesignation = $periodDesignation; - $this->workingDays = $workingDays; - $this->totalHours = $totalHours; - $this->overtime = $overtime; } - /** - * @return DateTimeInterface - */ public function getPeriod(): DateTimeInterface { return $this->period; } - /** - * @return PeriodDesignationEnum - */ public function getPeriodDesignation(): PeriodDesignationEnum { return $this->periodDesignation; } - /** - * @return int - */ public function getWorkingDays(): int { return $this->workingDays; } - /** - * @return DateInterval - */ public function getTotalHours(): DateInterval { return $this->totalHours; } - /** - * @return DateInterval - */ public function getOvertime(): DateInterval { return $this->overtime; @@ -84,17 +50,19 @@ class WorkingHoursView implements ModelInterface * * @return array */ - #[ArrayShape(['period' => "string", - 'periodDesignation' => "string", - 'totalHours' => "string", - 'workingDays' => "int", - 'overtime' => "string" - ])] public function jsonSerialize(): array + #[ArrayShape([ + 'period' => "string", + 'periodDesignation' => "string", + 'totalHours' => "string", + 'workingDays' => "int", + 'overtime' => "string" + ])] + public function jsonSerialize(): array { $formatOvertime = (($this->getOvertime()->invert === 1) ? '-' : '') . '%H:%I:%S'; return [ - 'period' => $this->getPeriod()->format($this->getPeriodDesignation()->getValue()), - 'periodDesignation' => strtolower($this->getPeriodDesignation()->getKey()), + 'period' => $this->getPeriod()->format($this->getPeriodDesignation()->value), + 'periodDesignation' => strtolower($this->getPeriodDesignation()->name), 'workingDays' => $this->getWorkingDays(), 'totalHours' => $this->getTotalHours()->format('%H:%I:%S'), 'overtime' => $this->getOvertime()->format($formatOvertime) diff --git a/api/src/Repositories/AbstractWorkingHoursViewRepository.php b/api/src/Repositories/AbstractWorkingHoursViewRepository.php index d8a757b..fbeb182 100644 --- a/api/src/Repositories/AbstractWorkingHoursViewRepository.php +++ b/api/src/Repositories/AbstractWorkingHoursViewRepository.php @@ -18,9 +18,8 @@ abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInt { protected const SQL_SELECT = ''; - protected const SQL_WHERE = ''; + protected const SQL_WHERE = ''; - protected PDO $database; protected PeriodDesignationEnum $periodDesignation; /** @@ -28,9 +27,8 @@ abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInt * * @param PDO $database */ - public function __construct(PDO $database) + public function __construct(protected PDO $database) { - $this->database = $database; $this->database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } @@ -51,7 +49,7 @@ abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInt $model = new WorkingHoursView( $this->buildDateFromPeriod($row['period']), $this->periodDesignation, - (int) $row['workingDays'], + (int)$row['workingDays'], $this->buildDateInterval($row['totalHours']), $this->buildDateInterval($row['overtime']) ); @@ -82,7 +80,7 @@ abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInt return new WorkingHoursView( $this->buildDateFromPeriod($row['period']), $this->periodDesignation, - (int) $row['workingDays'], + (int)$row['workingDays'], $this->buildDateInterval($row['totalHours']), $this->buildDateInterval($row['overtime']) ); diff --git a/api/src/Repositories/RepositoryReaderInterface.php b/api/src/Repositories/RepositoryReaderInterface.php index 00aa1dc..97eb4df 100644 --- a/api/src/Repositories/RepositoryReaderInterface.php +++ b/api/src/Repositories/RepositoryReaderInterface.php @@ -3,8 +3,10 @@ namespace TorstenHettstedt\TimekeepingApi\Repositories; +use TorstenHettstedt\TimekeepingApi\Models\ModelInterface; + /** - * @template T of \TorstenHettstedt\TimekeepingApi\Models\ModelInterface + * @template T of ModelInterface */ interface RepositoryReaderInterface { diff --git a/api/src/Repositories/RepositoryWriterInterface.php b/api/src/Repositories/RepositoryWriterInterface.php index bc5f8fc..8c7241d 100644 --- a/api/src/Repositories/RepositoryWriterInterface.php +++ b/api/src/Repositories/RepositoryWriterInterface.php @@ -3,8 +3,10 @@ namespace TorstenHettstedt\TimekeepingApi\Repositories; +use TorstenHettstedt\TimekeepingApi\Models\ModelInterface; + /** - * @template T of \TorstenHettstedt\TimekeepingApi\Models\ModelInterface + * @template T of ModelInterface */ interface RepositoryWriterInterface { diff --git a/api/src/Repositories/WorkingHoursMonthlyViewRepository.php b/api/src/Repositories/WorkingHoursMonthlyViewRepository.php index eb73c7e..f20e1eb 100644 --- a/api/src/Repositories/WorkingHoursMonthlyViewRepository.php +++ b/api/src/Repositories/WorkingHoursMonthlyViewRepository.php @@ -12,23 +12,20 @@ use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum; class WorkingHoursMonthlyViewRepository extends AbstractWorkingHoursViewRepository { - protected const SQL_SELECT = <<periodDesignation = PeriodDesignationEnum::MONTHLY(); + $this->periodDesignation = PeriodDesignationEnum::MONTHLY; } protected function buildDateFromPeriod(string $period): DateTimeInterface diff --git a/api/src/Repositories/WorkingHoursRepository.php b/api/src/Repositories/WorkingHoursRepository.php index 82c81d1..667036f 100644 --- a/api/src/Repositories/WorkingHoursRepository.php +++ b/api/src/Repositories/WorkingHoursRepository.php @@ -20,17 +20,16 @@ use TorstenHettstedt\TimekeepingApi\Models\WorkingHours; */ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWriterInterface { + protected const string SQL_SELECT = <<<'SQL' +select + "Datum" as "workingDay", + "Arbeitszeit" as "workingTime" +from public."Arbeitszeiten" +SQL; - protected PDO $database; - /** - * WorkingHoursRepository constructor. - * - * @param PDO $database - */ - public function __construct(PDO $database) + public function __construct(protected PDO $database) { - $this->database = $database; $this->database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } @@ -96,7 +95,7 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri protected function buildFindFilteredStatement(?string $start, ?string $end): ?PDOStatement { - $query = 'select "Datum" as "workingDay", "Arbeitszeit" as "workingTime" from public."Arbeitszeiten" '; + $query = static::SQL_SELECT . ' '; $query .= $this->buildFilterString($start, $end); $query .= 'order by "Datum"'; $stmt = $this->database->prepare($query); @@ -106,7 +105,7 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri if (is_string($end)) { $stmt->bindParam(':end', $end); } - return $stmt ?? null; + return $stmt ?: null; } /** @@ -121,7 +120,7 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri */ 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(static::SQL_SELECT . ' where "Datum" = ?'); $stmt->bindParam(1, $primary_key); $stmt->execute(); $row = $stmt->fetch(); @@ -155,7 +154,9 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri throw new RepositoryRecordAlreadyExistException(); } /** @noinspection PhpUnusedLocalVariableInspection */ 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' + ); $stmt->bindParam(1, $workingDay); $stmt->bindParam(2, $workingTime); $stmt->execute(); diff --git a/api/src/Repositories/WorkingHoursWeeklyViewRepository.php b/api/src/Repositories/WorkingHoursWeeklyViewRepository.php index d1043e1..a0627fc 100644 --- a/api/src/Repositories/WorkingHoursWeeklyViewRepository.php +++ b/api/src/Repositories/WorkingHoursWeeklyViewRepository.php @@ -11,23 +11,20 @@ use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum; class WorkingHoursWeeklyViewRepository extends AbstractWorkingHoursViewRepository { - protected const SQL_SELECT = <<periodDesignation = PeriodDesignationEnum::WEEKLY(); + $this->periodDesignation = PeriodDesignationEnum::WEEKLY; } protected function buildDateFromPeriod(string $period): DateTimeInterface diff --git a/api/src/Repositories/WorkingHoursYearlyViewRepository.php b/api/src/Repositories/WorkingHoursYearlyViewRepository.php index 1efb106..7a01e94 100644 --- a/api/src/Repositories/WorkingHoursYearlyViewRepository.php +++ b/api/src/Repositories/WorkingHoursYearlyViewRepository.php @@ -11,23 +11,20 @@ use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum; class WorkingHoursYearlyViewRepository extends AbstractWorkingHoursViewRepository { - protected const SQL_SELECT = <<periodDesignation = PeriodDesignationEnum::YEARLY(); + $this->periodDesignation = PeriodDesignationEnum::YEARLY; } /** diff --git a/api/tests/_data/dump.sql b/api/tests/_data/dump.sql index 0dd1931..ffc1978 100644 --- a/api/tests/_data/dump.sql +++ b/api/tests/_data/dump.sql @@ -13,10 +13,10 @@ SET xmloption = content; SET client_min_messages = warning; SET row_security = off; -DROP TABLE IF EXISTS public."Arbeitszeiten"; DROP VIEW IF EXISTS public."Arbeitszeiten - Woche"; DROP VIEW IF EXISTS public."Arbeitszeiten - Monat"; DROP VIEW IF EXISTS public."Arbeitszeiten - Jahr"; +DROP TABLE IF EXISTS public."Arbeitszeiten"; -- -- Name: Arbeitszeiten; Type: TABLE; Schema: public; Owner: torsten @@ -83,66 +83,66 @@ ALTER TABLE public."Arbeitszeiten - Woche" OWNER TO bruce; -- Data for Name: Arbeitszeiten; Type: TABLE DATA; Schema: public; Owner: torsten -- -COPY public."Arbeitszeiten" ("Datum", "Arbeitszeit") FROM stdin; -2020-01-07 08:06:00 -2020-01-08 08:16:00 -2020-01-09 07:49:00 -2020-01-10 07:30:00 -2020-01-13 08:06:00 -2020-01-14 08:04:00 -2020-01-15 08:20:00 -2020-01-16 07:13:00 -2020-01-17 07:57:00 -2020-01-20 07:53:00 -2020-01-21 07:49:00 -2020-01-22 07:56:00 -2020-01-23 07:55:00 -2020-01-24 07:08:00 -2020-01-27 08:23:00 -2020-01-28 07:20:00 -2020-01-29 08:13:00 -2020-01-30 08:43:00 -2020-01-31 07:21:00 -2020-02-03 07:56:00 -2020-02-04 08:05:00 -2020-02-05 07:58:00 -2020-02-06 08:01:00 -2020-02-07 07:59:00 -2020-02-10 07:50:00 -2020-02-11 08:01:00 -2020-02-12 08:14:00 -2020-02-13 08:12:00 -2020-02-14 07:51:00 -2020-02-17 07:59:00 -2020-02-18 08:05:00 -2020-02-19 07:34:00 -2020-02-20 07:33:00 -2020-02-21 07:56:00 -2020-02-24 08:02:00 -2020-02-25 08:13:00 -2020-02-26 08:42:00 -2020-02-27 07:49:00 -2020-02-28 08:16:00 -2020-03-02 08:22:00 -2020-03-03 08:20:00 -2020-03-04 08:12:00 -2020-03-05 08:32:00 -2020-03-06 04:41:00 -2020-03-09 07:05:00 -2020-03-10 07:34:00 -2020-03-11 07:39:00 -2020-03-12 07:50:00 -2020-03-13 08:26:00 -2020-03-16 07:51:00 -2020-03-17 07:50:00 -2020-03-18 07:19:00 -2020-03-19 07:55:00 -2020-03-20 05:43:00 -2020-03-23 08:05:00 -2020-03-24 08:21:00 -2020-03-25 08:10:00 -2020-03-26 10:31:00 -2020-03-27 04:55:00 -2020-03-30 08:21:00 -2020-03-31 08:01:00 -\. +INSERT INTO public."Arbeitszeiten" ("Datum", "Arbeitszeit") VALUES +('2020-01-07','08:06:00'), +('2020-01-08','08:16:00'), +('2020-01-09','07:49:00'), +('2020-01-10','07:30:00'), +('2020-01-13','08:06:00'), +('2020-01-14','08:04:00'), +('2020-01-15','08:20:00'), +('2020-01-16','07:13:00'), +('2020-01-17','07:57:00'), +('2020-01-20','07:53:00'), +('2020-01-21','07:49:00'), +('2020-01-22','07:56:00'), +('2020-01-23','07:55:00'), +('2020-01-24','07:08:00'), +('2020-01-27','08:23:00'), +('2020-01-28','07:20:00'), +('2020-01-29','08:13:00'), +('2020-01-30','08:43:00'), +('2020-01-31','07:21:00'), +('2020-02-03','07:56:00'), +('2020-02-04','08:05:00'), +('2020-02-05','07:58:00'), +('2020-02-06','08:01:00'), +('2020-02-07','07:59:00'), +('2020-02-10','07:50:00'), +('2020-02-11','08:01:00'), +('2020-02-12','08:14:00'), +('2020-02-13','08:12:00'), +('2020-02-14','07:51:00'), +('2020-02-17','07:59:00'), +('2020-02-18','08:05:00'), +('2020-02-19','07:34:00'), +('2020-02-20','07:33:00'), +('2020-02-21','07:56:00'), +('2020-02-24','08:02:00'), +('2020-02-25','08:13:00'), +('2020-02-26','08:42:00'), +('2020-02-27','07:49:00'), +('2020-02-28','08:16:00'), +('2020-03-02','08:22:00'), +('2020-03-03','08:20:00'), +('2020-03-04','08:12:00'), +('2020-03-05','08:32:00'), +('2020-03-06','04:41:00'), +('2020-03-09','07:05:00'), +('2020-03-10','07:34:00'), +('2020-03-11','07:39:00'), +('2020-03-12','07:50:00'), +('2020-03-13','08:26:00'), +('2020-03-16','07:51:00'), +('2020-03-17','07:50:00'), +('2020-03-18','07:19:00'), +('2020-03-19','07:55:00'), +('2020-03-20','05:43:00'), +('2020-03-23','08:05:00'), +('2020-03-24','08:21:00'), +('2020-03-25','08:10:00'), +('2020-03-26','10:31:00'), +('2020-03-27','04:55:00'), +('2020-03-30','08:21:00'), +('2020-03-31','08:01:00') +; \ No newline at end of file diff --git a/api/tests/api.suite.dist.yml b/api/tests/api.suite.dist.yml index 3c4624f..ea87ebd 100644 --- a/api/tests/api.suite.dist.yml +++ b/api/tests/api.suite.dist.yml @@ -3,6 +3,6 @@ modules: enabled: - \Helper\Api - REST: - url: http://localhost:8091/ + url: http://api:80/ depends: PhpBrowser part: Json \ No newline at end of file diff --git a/api/tests/unit/Controller/AbstractControllerTest.php b/api/tests/unit/Controller/AbstractControllerTest.php new file mode 100644 index 0000000..0f8627c --- /dev/null +++ b/api/tests/unit/Controller/AbstractControllerTest.php @@ -0,0 +1,52 @@ +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->response = $this->createMock(Response::class); + $this->response->expects($this->any())->method('getStatusCode')->willReturn(self::FICTIONAL_STATUS_CODE); + $this->response->expects($this->any())->method('getBody')->willReturn( + $this->makeEmpty(StreamInterface::class, [ + 'write' => function (mixed $data) { + $this->assertIsString($data); + $this->assertJson($data); + return strlen($data); + }, + 'getContents' => 'abc', + ]) + ); + $this->response->expects($this->any())->method('withHeader')->willReturn($this->response); + $this->response->expects($this->any())->method('withStatus')->willReturn($this->response); + } +} diff --git a/api/tests/unit/Controller/WorkingHoursControllerTest.php b/api/tests/unit/Controller/WorkingHoursControllerTest.php index 40d67e3..e6b9903 100644 --- a/api/tests/unit/Controller/WorkingHoursControllerTest.php +++ b/api/tests/unit/Controller/WorkingHoursControllerTest.php @@ -2,65 +2,46 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Controller; -use Codeception\Example; -use Codeception\Test\Unit; +use Codeception\Attribute\DataProvider; use Exception; -use PDO; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; -use Psr\Http\Message\StreamInterface; +use Psr\Container\NotFoundExceptionInterface; 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 +class WorkingHoursControllerTest extends AbstractControllerTest { - 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; + protected const int FICTIONAL_STATUS_CODE = 666; + public const string EXISTING_DATE = '2020-01-28'; + public const string EXISTING_INTERVAL = '07:20:00'; + public const string NEW_DATE = '2020-04-01'; + public const string NEW_INTERVAL = '07:59:00'; /** * @throws Exception + * @throws \PHPUnit\Framework\MockObject\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 ContainerExceptionInterface + * @throws NotFoundExceptionInterface * @throws Exception */ public function testConstructWithNonDatabase(): void @@ -103,12 +84,12 @@ class WorkingHoursControllerTest extends Unit /** * @param array $queryParams * - * @dataProvider valideFilterDataProvider - * - * @throws HttpInternalServerErrorException + * @throws ContainerExceptionInterface * @throws NotDatabasesException + * @throws NotFoundExceptionInterface * @throws Exception */ + #[DataProvider('valideFilterDataProvider')] public function testBrowseWithValideParameter(array $queryParams): void { $this->request = $this->makeEmpty(Request::class, [ @@ -116,7 +97,8 @@ class WorkingHoursControllerTest extends Unit ]); $controller = new WorkingHoursController($this->container); $response = $controller->browse($this->request, $this->response, []); - $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); } /** @@ -246,12 +228,12 @@ class WorkingHoursControllerTest extends Unit /** * @param array $queryParams * - * @dataProvider invalideFilterDataProvider - * - * @throws HttpInternalServerErrorException + * @throws ContainerExceptionInterface * @throws NotDatabasesException + * @throws NotFoundExceptionInterface * @throws Exception */ + #[DataProvider('invalideFilterDataProvider')] public function testBrowseWithInvalideParameter(array $queryParams): void { $this->request = $this->makeEmpty(Request::class, [ @@ -263,9 +245,10 @@ class WorkingHoursControllerTest extends Unit } /** + * @throws ContainerExceptionInterface * @throws NotDatabasesException - * @throws HttpBadRequestException - * @throws HttpNotFoundException + * @throws NotFoundExceptionInterface + * @throws Exception */ public function testUpdateExistRecord(): void { @@ -273,13 +256,15 @@ class WorkingHoursControllerTest extends Unit $response = $controller->update($this->request, $this->response, [ 'id' => self::EXISTING_DATE, ]); - $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); } /** + * @throws ContainerExceptionInterface * @throws NotDatabasesException - * @throws HttpBadRequestException - * @throws HttpNotFoundException + * @throws NotFoundExceptionInterface + * @throws Exception */ public function testUpdateNotExistRecord(): void { @@ -311,14 +296,12 @@ class WorkingHoursControllerTest extends Unit * @param string $date * @param string $time * - * @dataProvider invalidCreatDataProvider - * - * @throws HttpBadRequestException - * @throws HttpConflictRequestException - * @throws HttpInternalServerErrorException + * @throws ContainerExceptionInterface * @throws NotDatabasesException + * @throws NotFoundExceptionInterface * @throws Exception */ + #[DataProvider('invalidCreatDataProvider')] public function testCreatInvalidData(string $date, string $time): void { $this->request = $this->makeEmpty(Request::class, [ @@ -333,10 +316,9 @@ class WorkingHoursControllerTest extends Unit } /** - * @throws HttpBadRequestException + * @throws ContainerExceptionInterface * @throws NotDatabasesException - * @throws HttpInternalServerErrorException - * @throws HttpConflictRequestException + * @throws NotFoundExceptionInterface * @throws Exception */ public function testCreatNewRecord(): void @@ -349,14 +331,14 @@ class WorkingHoursControllerTest extends Unit ]); $controller = new WorkingHoursController($this->container); $response = $controller->creat($this->request, $this->response, []); - $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); } /** - * @throws HttpBadRequestException - * @throws HttpConflictRequestException - * @throws HttpInternalServerErrorException + * @throws ContainerExceptionInterface * @throws NotDatabasesException + * @throws NotFoundExceptionInterface * @throws Exception */ public function testCreatExistRecord(): void @@ -373,9 +355,10 @@ class WorkingHoursControllerTest extends Unit } /** - * @throws HttpInternalServerErrorException - * @throws HttpNotFoundException + * @throws ContainerExceptionInterface * @throws NotDatabasesException + * @throws NotFoundExceptionInterface + * @throws Exception */ public function testReadExistRecord(): void { @@ -383,13 +366,15 @@ class WorkingHoursControllerTest extends Unit $response = $controller->read($this->request, $this->response, [ 'id' => self::EXISTING_DATE, ]); - $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); } /** - * @throws HttpInternalServerErrorException - * @throws HttpNotFoundException + * @throws ContainerExceptionInterface * @throws NotDatabasesException + * @throws NotFoundExceptionInterface + * @throws Exception */ public function testReadNotExistRecord(): void { diff --git a/api/tests/unit/Controller/WorkingHoursViewControllerTest.php b/api/tests/unit/Controller/WorkingHoursViewControllerTest.php index fa2ca2f..2f5e2da 100644 --- a/api/tests/unit/Controller/WorkingHoursViewControllerTest.php +++ b/api/tests/unit/Controller/WorkingHoursViewControllerTest.php @@ -2,49 +2,30 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Controller; -use Codeception\Test\Unit; use Exception; -use PDO; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; -use Psr\Http\Message\StreamInterface; +use Psr\Container\NotFoundExceptionInterface; use Slim\Psr7\Request; -use Slim\Psr7\Response; use TorstenHettstedt\TimekeepingApi\Controller\NotDatabasesException; use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursViewController; -class WorkingHoursViewControllerTest extends Unit +class WorkingHoursViewControllerTest extends AbstractControllerTest { - protected ContainerInterface $container; - protected Request $request; - protected Response $response; - /** * @throws Exception + * @throws \PHPUnit\Framework\MockObject\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), - ]), - ]); + $this->request = $this->makeEmpty(Request::class); } /** * @throws NotDatabasesException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface * @throws Exception */ public function testConstructWithNonDatabase(): void @@ -57,32 +38,44 @@ class WorkingHoursViewControllerTest extends Unit } /** + * @throws NotDatabasesException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface * @throws Exception */ public function testBrowseMonthly(): void { $controller = new WorkingHoursViewController($this->container); $response = $controller->browseMonthly($this->request, $this->response, []); - $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); } /** + * @throws NotDatabasesException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface * @throws Exception */ public function testBrowseYearly(): void { $controller = new WorkingHoursViewController($this->container); $response = $controller->browseYearly($this->request, $this->response, []); - $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); } /** + * @throws NotDatabasesException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface * @throws Exception */ public function testBrowseWeekly(): void { $controller = new WorkingHoursViewController($this->container); $response = $controller->browseWeekly($this->request, $this->response, []); - $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); } } diff --git a/api/tests/unit/Middleware/ErrorHandlerTest.php b/api/tests/unit/Middleware/ErrorHandlerTest.php index 036fd8b..779ade7 100644 --- a/api/tests/unit/Middleware/ErrorHandlerTest.php +++ b/api/tests/unit/Middleware/ErrorHandlerTest.php @@ -5,6 +5,7 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Middleware; use Codeception\Stub\Expected; use Codeception\Test\Unit; use Exception; +use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -20,6 +21,7 @@ use TorstenHettstedt\TimekeepingApi\Middleware\ErrorHandler; class ErrorHandlerTest extends Unit { + /** @var App */ protected App $app; protected ServerRequestInterface $request; protected Exception $exception; @@ -38,6 +40,7 @@ class ErrorHandlerTest extends Unit 'write' => function (mixed $data) { $this->assertIsString($data); $this->assertJson($data); + return strlen($data); }, ]), ]), @@ -87,11 +90,10 @@ class ErrorHandlerTest extends Unit 'file' => '/path(to/file', 'getTitle' => Expected::once('The Title'), ]); - $this->logger = $this->makeEmpty(LoggerInterface::class, []); + $this->logger = $this->makeEmpty(LoggerInterface::class); $middleWare = new ErrorHandler($this->app); $middleWare($this->request, $this->exception, true, true, true, $this->logger); - } /** @@ -104,7 +106,7 @@ class ErrorHandlerTest extends Unit 'code' => 400, 'file' => '/path(to/file', ]); - $this->logger = $this->makeEmpty(LoggerInterface::class, []); + $this->logger = $this->makeEmpty(LoggerInterface::class); $middleWare = new ErrorHandler($this->app); $middleWare($this->request, $this->exception, true, true, true, $this->logger); diff --git a/api/tests/unit/Models/WorkingHoursTest.php b/api/tests/unit/Models/WorkingHoursTest.php index 200f997..b1612ff 100644 --- a/api/tests/unit/Models/WorkingHoursTest.php +++ b/api/tests/unit/Models/WorkingHoursTest.php @@ -2,6 +2,7 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Models; +use Codeception\Attribute\DataProvider; use Codeception\Test\Unit; use DateInterval; use DateTime; @@ -22,7 +23,7 @@ class WorkingHoursTest extends Unit } /** - * @return array[] + * @return array>> */ public function workingHoursProvider(): array { @@ -49,17 +50,13 @@ class WorkingHoursTest extends Unit /** * @param DateTime $date * @param DateInterval $interval - * @param array $should_json - * - * @dataProvider workingHoursProvider + * @param array $shouldJson */ - public function testWorkingHoursWithContent(DateTime $date, DateInterval $interval, array $should_json): void + #[DataProvider('workingHoursProvider')] + public function testWorkingHoursWithContent(DateTime $date, DateInterval $interval, array $shouldJson): void { - $obj = new WorkingHours(); - $obj - ->setWorkingDay($date) - ->setWorkingTime($interval); + $obj->setWorkingDay($date)->setWorkingTime($interval); $this->assertNotNull($obj->getWorkingDay()); $this->assertInstanceOf(DateTime::class, $obj->getWorkingDay()); @@ -70,7 +67,6 @@ class WorkingHoursTest extends Unit $this->assertEquals($interval, $obj->getWorkingTime()); $json = $obj->jsonSerialize(); - $this->assertIsArray($json); - $this->assertEquals($should_json, $json); + $this->assertEquals($shouldJson, $json); } } diff --git a/api/tests/unit/Models/WorkingHoursViewTest.php b/api/tests/unit/Models/WorkingHoursViewTest.php index cf0a0db..ed79216 100644 --- a/api/tests/unit/Models/WorkingHoursViewTest.php +++ b/api/tests/unit/Models/WorkingHoursViewTest.php @@ -2,6 +2,7 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Models; +use Codeception\Attribute\DataProvider; use Codeception\Test\Unit; use DateInterval; use DateTime; @@ -13,7 +14,7 @@ class WorkingHoursViewTest extends Unit { /** - * @return array[] + * @return array}> */ public function workingHoursProvider(): array { @@ -25,7 +26,11 @@ class WorkingHoursViewTest extends Unit $absence_time->invert = 1; return [ [ - $date, PeriodDesignationEnum::WEEKLY(), $work_days, $total_hours, $overtime, + $date, + PeriodDesignationEnum::WEEKLY, + $work_days, + $total_hours, + $overtime, [ 'period' => '2020#49', 'periodDesignation' => 'weekly', @@ -35,7 +40,11 @@ class WorkingHoursViewTest extends Unit ], ], [ - $date, PeriodDesignationEnum::WEEKLY(), $work_days, $total_hours, $absence_time, + $date, + PeriodDesignationEnum::WEEKLY, + $work_days, + $total_hours, + $absence_time, [ 'period' => '2020#49', 'periodDesignation' => 'weekly', @@ -45,7 +54,11 @@ class WorkingHoursViewTest extends Unit ], ], [ - $date, PeriodDesignationEnum::MONTHLY(), $work_days, $total_hours, $overtime, + $date, + PeriodDesignationEnum::MONTHLY, + $work_days, + $total_hours, + $overtime, [ 'period' => '2020-11', 'periodDesignation' => 'monthly', @@ -55,7 +68,11 @@ class WorkingHoursViewTest extends Unit ], ], [ - $date, PeriodDesignationEnum::MONTHLY(), $work_days, $total_hours, $absence_time, + $date, + PeriodDesignationEnum::MONTHLY, + $work_days, + $total_hours, + $absence_time, [ 'period' => '2020-11', 'periodDesignation' => 'monthly', @@ -65,7 +82,12 @@ class WorkingHoursViewTest extends Unit ], ], [ - $date, PeriodDesignationEnum::YEARLY(), $work_days, $total_hours, $overtime, [ + $date, + PeriodDesignationEnum::YEARLY, + $work_days, + $total_hours, + $overtime, + [ 'period' => '2020', 'periodDesignation' => 'yearly', 'workingDays' => 15, @@ -74,7 +96,12 @@ class WorkingHoursViewTest extends Unit ], ], [ - $date, PeriodDesignationEnum::YEARLY(), $work_days, $total_hours, $absence_time, [ + $date, + PeriodDesignationEnum::YEARLY, + $work_days, + $total_hours, + $absence_time, + [ 'period' => '2020', 'periodDesignation' => 'yearly', 'workingDays' => 15, @@ -86,50 +113,40 @@ class WorkingHoursViewTest extends Unit } /** - * @param DateTimeInterface $period - * @param PeriodDesignationEnum $periodDesignation - * @param int $workingDays - * @param DateInterval $totalHours - * @param DateInterval $overtime - * @param array $should_json - * - * @dataProvider workingHoursProvider + * @param DateTimeInterface $period + * @param PeriodDesignationEnum $periodDesignation + * @param int $workingDays + * @param DateInterval $totalHours + * @param DateInterval $overtime + * @param array $shouldJson */ + #[DataProvider('workingHoursProvider')] public function testWorkingHoursViewWithContent( DateTimeInterface $period, PeriodDesignationEnum $periodDesignation, int $workingDays, DateInterval $totalHours, DateInterval $overtime, - array $should_json - ): void - { - + array $shouldJson + ): void { $obj = new WorkingHoursView($period, $periodDesignation, $workingDays, $totalHours, $overtime); - $this->assertNotNull($obj->getPeriod()); $this->assertInstanceOf(DateTimeInterface::class, $obj->getPeriod()); $this->assertEquals($period, $obj->getPeriod()); - $this->assertNotNull($obj->getPeriodDesignation()); $this->assertInstanceOf(PeriodDesignationEnum::class, $obj->getPeriodDesignation()); $this->assertEquals($periodDesignation, $obj->getPeriodDesignation()); - $this->assertNotNull($obj->getWorkingDays()); - $this->assertIsInt($obj->getWorkingDays()); $this->assertEquals($workingDays, $obj->getWorkingDays()); - $this->assertNotNull($obj->getTotalHours()); $this->assertInstanceOf(DateInterval::class, $obj->getTotalHours()); $this->assertEquals($totalHours, $obj->getTotalHours()); - $this->assertNotNull($obj->getOvertime()); $this->assertInstanceOf(DateInterval::class, $obj->getOvertime()); $this->assertEquals($overtime, $obj->getOvertime()); $json = $obj->jsonSerialize(); - $this->assertIsArray($json); - $this->assertEquals($should_json, $json); + $this->assertEquals($shouldJson, $json); } } diff --git a/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php b/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php index 7080ee9..326caff 100644 --- a/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php +++ b/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php @@ -2,7 +2,7 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Repositories; -use Codeception\Example; +use Codeception\Attribute\DataProvider; use Codeception\Test\Unit; use DateInterval; use DateTime; @@ -18,18 +18,20 @@ use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursRepository; class WorkingHoursRepositoryTest extends Unit { - const EXISTING_DATE = '2020-01-28'; - const EXISTING_INTERVAL = 'PT7H20M'; - const NEW_DATE = '2020-04-01'; - const NEW_INTERVAL = 'PT7H59M'; - const RECORDS_COUNT = 61; + public const string EXISTING_DATE = '2020-01-28'; + public const string EXISTING_INTERVAL = 'PT7H20M'; + public const string NEW_DATE = '2020-04-01'; + public const string NEW_INTERVAL = 'PT7H59M'; + public const int RECORDS_COUNT = 61; protected PDO $pdoObject; protected function _before(): void { /** @noinspection SpellCheckingInspection */ - $this->pdoObject = new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass'); + $this->pdoObject = new PDO( + 'pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass' + ); parent::_before(); } @@ -40,8 +42,8 @@ class WorkingHoursRepositoryTest extends Unit { $repository = new WorkingHoursRepository($this->pdoObject); $models = $repository->findAll(); - $this->assertIsArray($models); - $this->assertContainsOnly(WorkingHours::class, $models); + + $this->assertContainsOnlyInstancesOf(WorkingHours::class, $models); $this->assertCount(self::RECORDS_COUNT, $models); } @@ -51,13 +53,13 @@ class WorkingHoursRepositoryTest extends Unit public function valideFilterParameter(): array { return [ - [null, null, 61,], - ['2020-04-01', null, 0,], - ['2020-04-01', '2020-04-30', 0,], - [null, '2020-04-30', 61,], - ['2020-03-01', null, 22,], + [null, null, 61,], + ['2020-04-01', null, 0,], + ['2020-04-01', '2020-04-30', 0,], + [null, '2020-04-30', 61,], + ['2020-03-01', null, 22,], ['2020-03-01', '2020-03-31', 22,], - ['2020-03-01', '2020-03-07', 5,], + ['2020-03-01', '2020-03-07', 5,], ]; } @@ -66,16 +68,15 @@ class WorkingHoursRepositoryTest extends Unit * @param string|null $ende * @param int $count * - * @dataProvider valideFilterParameter - * * @throws Exception */ + #[DataProvider('valideFilterParameter')] public function testFindAllWithValideParameter(?string $start, ?string $ende, int $count): void { $repository = new WorkingHoursRepository($this->pdoObject); $models = $repository->findFiltered($start, $ende); - $this->assertIsArray($models); - $this->assertContainsOnly(WorkingHours::class, $models); + + $this->assertContainsOnlyInstancesOf(WorkingHours::class, $models); $this->assertCount($count, $models); } @@ -85,23 +86,23 @@ class WorkingHoursRepositoryTest extends Unit public function invalideFilterParameter(): array { return [ - ['2020-04-31',null], - [null, '2020-04-31'], - ['2020-04-00','2020-04-31'], - ['2020-04-01','2020-04-31'], - ['2020-04-00','2020-04-30'], + ['2020-04-31', null], + [null, '2020-04-31'], + ['2020-04-00', '2020-04-31'], + ['2020-04-01', '2020-04-31'], + ['2020-04-00', '2020-04-30'], // - ['2020-04', null], - [null, '2020-04'], - ['2020-04', '2020-04'], - ['2020-04-01','2020-04'], - ['2020-04', '2020-04-30'], + ['2020-04', null], + [null, '2020-04'], + ['2020-04', '2020-04'], + ['2020-04-01', '2020-04'], + ['2020-04', '2020-04-30'], // - ['2020', null], - [null, '2020'], - ['2020', '2020'], - ['2020-04-01','2020'], - ['2020', '2020-04-30'], + ['2020', null], + [null, '2020'], + ['2020', '2020'], + ['2020-04-01', '2020'], + ['2020', '2020-04-30'], ]; } @@ -109,10 +110,9 @@ class WorkingHoursRepositoryTest extends Unit * @param string|null $start * @param string|null $ende * - * @dataProvider invalideFilterParameter - * * @throws Exception */ + #[DataProvider('invalideFilterParameter')] public function testFindAllWithInvalideParameter(?string $start, ?string $ende): void { $repository = new WorkingHoursRepository($this->pdoObject); @@ -127,7 +127,6 @@ class WorkingHoursRepositoryTest extends Unit { $repository = new WorkingHoursRepository($this->pdoObject); $model = $repository->findByKey(self::EXISTING_DATE); - $this->assertInstanceOf(WorkingHours::class, $model); $this->assertEquals(new DateTime(self::EXISTING_DATE), $model->getWorkingDay()); $this->assertEquals(new DateInterval(self::EXISTING_INTERVAL), $model->getWorkingTime()); } @@ -163,7 +162,6 @@ class WorkingHoursRepositoryTest extends Unit ]); $repository->insert($model); $model = $repository->findByKey(self::NEW_DATE); - $this->assertInstanceOf(WorkingHours::class, $model); $this->assertEquals(new DateTime(self::NEW_DATE), $model->getWorkingDay()); $this->assertEquals(new DateInterval(self::NEW_INTERVAL), $model->getWorkingTime()); $this->assertCount(self::RECORDS_COUNT + 1, $repository->findAll()); diff --git a/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php b/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php index 9297162..14ebb54 100644 --- a/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php +++ b/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php @@ -2,12 +2,11 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Repositories; +use Codeception\Attribute\DataProvider; use Codeception\Test\Unit; use DateTime; use DateTimeZone; -use Exception; -use PDO; -use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView; +use PDO;use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView; use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException; use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursWeeklyViewRepository; use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursMonthlyViewRepository; @@ -25,7 +24,7 @@ class WorkingHoursViewRepositoryTest extends Unit } /** - * @return array[] + * @return array */ public function listObjectProvider(): array { @@ -37,20 +36,19 @@ class WorkingHoursViewRepositoryTest extends Unit } /** - * @return array[] - * @throws Exception + * @return array */ 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'))], + [WorkingHoursYearlyViewRepository::class, '2020', 61, date_create('2020-01-01', new DateTimeZone('UCT'))], + [WorkingHoursMonthlyViewRepository::class, '2020 February', 20, date_create('2020-02-01', new DateTimeZone('UCT'))], + [WorkingHoursWeeklyViewRepository::class, '2020#03', 5, date_create('2020-01-13', new DateTimeZone('UCT'))], ]; } /** - * @return array[] + * @return array */ public function notExistingObjectProvider(): array { @@ -61,49 +59,30 @@ class WorkingHoursViewRepositoryTest extends Unit ]; } - /** - * @param string $period_class - * @param int $count_records - * - * @dataProvider listObjectProvider - */ - public function testFindAll(string $period_class, int $count_records): void + #[DataProvider(('listObjectProvider'))] + public function testFindAll(string $periodClass, int $countRecords): void { - $repository = new $period_class($this->pdoObject); + $repository = new $periodClass($this->pdoObject); $models = $repository->findAll(); $this->assertIsArray($models); - $this->assertContainsOnly(WorkingHoursView::class, $models); - $this->assertCount($count_records, $models); + $this->assertContainsOnlyInstancesOf(WorkingHoursView::class, $models); + $this->assertCount($countRecords, $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 + #[DataProvider(('existingObjectProvider'))] + public function testExistingFindByKey(string $periodClass, string $search, int $workingDays, DateTime $period): void { - $repository = new $period_class($this->pdoObject); + $repository = new $periodClass($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 + #[DataProvider(('notExistingObjectProvider'))] + public function testNotExistingFindByKey(string $periodClass, string $search): void { - $repository = new $period_class($this->pdoObject); + $repository = new $periodClass($this->pdoObject); $this->expectException(RepositoryRecordNotFoundException::class); $repository->findByKey($search); } diff --git a/compose.yml b/compose.yml index 786849c..41d45cd 100644 --- a/compose.yml +++ b/compose.yml @@ -1,32 +1,35 @@ volumes: - logs: - driver: local + logs: + driver: local services: - api: - build: ./api/ - environment: - docker: "true" - ports: - - 8090:80 - volumes: - - ./api:/var/www - - logs:/var/www/logs - test-api: - build: ./api/ - environment: - docker: "true" - DATABASES_HOST: psql.torsten-hettstedt.net - DATABASES_NAME: testdb - DATABASES_USER: bruce - DATABASES_PASS: mypass - ports: - - 8091:80 - volumes: - - ./api:/var/www - - logs:/var/www/logs - ui: - build: ./ui/ - ports: - - 80:80 - depends_on: - - api \ No newline at end of file + api: + build: ./api/ + environment: + docker: "true" + ports: + - "8090:80" + volumes: + - ./api:/var/www + - logs:/var/www/logs + test-api: + build: ./api/ + environment: + docker: "true" + DATABASES_HOST: psql.torsten-hettstedt.net + DATABASES_NAME: testdb + DATABASES_USER: bruce + DATABASES_PASS: mypass + ports: + - "8091:80" + volumes: + - ./api:/var/www + - logs:/var/www/logs + ui: + build: ./ui/ + env_file: + - ui/.env + ports: + - "8080:3000" + depends_on: + - api + - test-api \ No newline at end of file diff --git a/deploy.sh b/deploy.sh index b65b6c1..be6c037 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash create_css_files() { - cd ui/public || exit 2 - lessc --source-map less/print.less print.css - lessc --source-map less/global.less global.css + cd ui || exit 2 + lessc --source-map less/print.less static/print.css + lessc --source-map less/global.less static/global.css cd ../.. } @@ -27,6 +27,13 @@ build_ui() { cd .. } +codeception_install() { + pwd + cd api/ || exit 2 + test -e ./tests/unit.suite.yml || ./vendor/bin/codecept build + cd .. +} + test_unit() { pwd cd api/ || exit 2 @@ -44,4 +51,4 @@ composer_run() { docker compose up -d } -create_css_files && install_ui_dependence && install_api_dependence && build_ui && test_unit && composer_run && test_api \ No newline at end of file +create_css_files && install_ui_dependence && install_api_dependence && build_ui && codeception_install && test_unit && composer_run && test_api \ No newline at end of file diff --git a/ui/.dockerignore b/ui/.dockerignore new file mode 100644 index 0000000..76add87 --- /dev/null +++ b/ui/.dockerignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/ui/Dockerfile b/ui/Dockerfile index 2ab11d1..c1ae596 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -1,3 +1,16 @@ -FROM httpd:2.4 +FROM node:22-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build +RUN npm prune --production -COPY ./public/ /usr/local/apache2/htdocs/ \ No newline at end of file +FROM node:22-alpine +WORKDIR /app +COPY --from=builder /app/build build/ +COPY --from=builder /app/node_modules node_modules/ +COPY package.json . +EXPOSE 3000 +ENV NODE_ENV=production +CMD [ "node", "build" ] \ No newline at end of file diff --git a/ui/public/less/global.less b/ui/less/global.less similarity index 100% rename from ui/public/less/global.less rename to ui/less/global.less diff --git a/ui/public/less/lesslib/basics.less b/ui/less/lesslib/basics.less similarity index 100% rename from ui/public/less/lesslib/basics.less rename to ui/less/lesslib/basics.less diff --git a/ui/public/less/lesslib/colors.less b/ui/less/lesslib/colors.less similarity index 100% rename from ui/public/less/lesslib/colors.less rename to ui/less/lesslib/colors.less diff --git a/ui/public/less/lesslib/fonts.less b/ui/less/lesslib/fonts.less similarity index 100% rename from ui/public/less/lesslib/fonts.less rename to ui/less/lesslib/fonts.less diff --git a/ui/public/less/lesslib/layout.less b/ui/less/lesslib/layout.less similarity index 63% rename from ui/public/less/lesslib/layout.less rename to ui/less/lesslib/layout.less index d0bd78a..0c280e1 100644 --- a/ui/public/less/lesslib/layout.less +++ b/ui/less/lesslib/layout.less @@ -5,50 +5,66 @@ 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; + div { + > 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; } - 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); + + > 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; } - } - &.selected, &.selected:hover { - background: lighten(@mainColor,10%,relativ); + a { - color: lighten(@mainColor,66%,relativ); + 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); + } } } } @@ -63,15 +79,19 @@ main { 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 { @@ -79,7 +99,9 @@ main { border-bottom: @mainColor solid thin; } } - }; + } + ; + tbody { td.absence-time { color: @redColor; @@ -87,15 +109,23 @@ main { text-decoration-style: dashed; } } + tfoot { + * { + border: initial; + } + } } + form { @formLabelWidth: 10rem; @formItemWidth: 40rem; + nav.menu { width: @formItemWidth + @formLabelWidth + 4rem; border-color: @mainColor; border-bottom-style: solid; margin: 1rem auto; + span { border-color: @mainColor; border-style: solid solid none; @@ -103,73 +133,89 @@ main { display: inline-block; font-weight: bolder; margin-right: 1rem; + &:first-child { margin-left: 1.5rem; } + &:hover { - background-color: darken(@mainColorLight,10%,relativ); + background-color: darken(@mainColorLight, 10%, relativ); } + &.active { - background-color: darken(@mainColorLight,20%,relativ); + background-color: darken(@mainColorLight, 20%, relativ); } } } + fieldset { width: @formItemWidth + @formLabelWidth + 2rem; margin: 1rem auto; - border-style: solid ; + border-style: solid; border-color: @mainColor; padding: @sectionMargin; + &.tasten { border: none; background-color: @mainColorLight; } + legend { margin: @sectionMargin; margin-left: @sectionMargin * 2; } + div { - & > label { + & > 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-style: solid; border-width: thin; } } } } + button { - .button(@mainColor, spin(lighten(@mainColor,66%,relativ),@subColor)); + .button(@mainColor, spin(lighten(@mainColor, 66%, relativ), @subColor)); + &.sub-button { - .button(lighten(@mainColor,20%,relativ), spin(@mainColor,@subColor)); + .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 ; + + background: @mainColorLight; + color: @mainColor; font-style: italic; font-size: 85%; } + nav li { padding: 0.5rem; } + border-bottom-style: solid; } } diff --git a/ui/public/less/lesslib/media_queries.less b/ui/less/lesslib/media_queries.less similarity index 98% rename from ui/public/less/lesslib/media_queries.less rename to ui/less/lesslib/media_queries.less index 0b57d01..78d7375 100644 --- a/ui/public/less/lesslib/media_queries.less +++ b/ui/less/lesslib/media_queries.less @@ -5,7 +5,7 @@ font-size: 10pt; } header, footer, main, nav, aside { - body > & { + body div > & { width: 19cm; margin: auto; .border-box(); @@ -15,7 +15,7 @@ padding: 0.5rem; ; } - body { + body div { & > header { font-size: 200%; font-weight: bolder; @@ -71,7 +71,7 @@ @media screen and (min-width: 831px) { header, footer, main, nav, aside { - body > & { + body div > & { width: @breiteMainBereich; margin: auto; .border-box(); @@ -80,7 +80,7 @@ header, footer{ padding: 0.5rem; } - body { + body div { & > header { font-size: 200%; font-weight: bolder; @@ -139,7 +139,7 @@ } @media screen and (max-width: 830px) { - html body { + html body div { @boxHeight: 5rem; & > header { height: @boxHeight; @@ -267,7 +267,7 @@ } } @media screen and (max-width: 720px) { - html body { + html body div { @boxHeight: 2.5rem; & > header { height: @boxHeight; diff --git a/ui/public/less/lesslib/mixins.framework.less b/ui/less/lesslib/mixins.framework.less similarity index 100% rename from ui/public/less/lesslib/mixins.framework.less rename to ui/less/lesslib/mixins.framework.less diff --git a/ui/public/less/lesslib/mixins.less b/ui/less/lesslib/mixins.less similarity index 100% rename from ui/public/less/lesslib/mixins.less rename to ui/less/lesslib/mixins.less diff --git a/ui/public/less/lesslib/print/basics.less b/ui/less/lesslib/print/basics.less similarity index 99% rename from ui/public/less/lesslib/print/basics.less rename to ui/less/lesslib/print/basics.less index 63acfce..f464f04 100644 --- a/ui/public/less/lesslib/print/basics.less +++ b/ui/less/lesslib/print/basics.less @@ -2,7 +2,7 @@ html { font-size: 100%; } -body { +body div { color: #000; font-size: 1em; font-family: OpenSans, "Lucida Grande", "Lucida Sans Unicode", Verdana, Helvetica, Arial, sans-serif; diff --git a/ui/public/less/lesslib/print/colors.less b/ui/less/lesslib/print/colors.less similarity index 100% rename from ui/public/less/lesslib/print/colors.less rename to ui/less/lesslib/print/colors.less diff --git a/ui/public/less/lesslib/print/layout.less b/ui/less/lesslib/print/layout.less similarity index 98% rename from ui/public/less/lesslib/print/layout.less rename to ui/less/lesslib/print/layout.less index 0864ddf..864ebe6 100644 --- a/ui/public/less/lesslib/print/layout.less +++ b/ui/less/lesslib/print/layout.less @@ -1,5 +1,5 @@ html { - body { + body div { > header, > footer { background: none; font-family: serif; @@ -59,7 +59,7 @@ main { } } } - tbody { + tbody div { th { font-weight: bold; text-align: center; diff --git a/ui/public/less/print.less b/ui/less/print.less similarity index 100% rename from ui/public/less/print.less rename to ui/less/print.less diff --git a/ui/package.json b/ui/package.json index 567b320..6814751 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,31 +1,38 @@ { - "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", - "svelte-paginate": "^0.1.0" - } + "private": true, + "name": "svelte-demo", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "dependencies": { + "ganalytics": "^3.1.2", + "navaid": "^1.0.2", + "open-iconic": "^1.1.1", + "papaparse": "^5.5.3" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^6.0.0", + "@sveltejs/kit": "^2.16.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@sveltejs/adapter-node": "^5.2.12", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.0.0", + "vite": "^6.2.6", + "dotenv": "^8.2.0", + "wx-svelte-grid": "^2.1.5", + "@iconify-icons/oi": "^1.1.0", + "@iconify/svelte": "^5.0.0" + }, + "exports": { + ".": { + "svelte": "./puplic/index.js" + } + } } diff --git a/ui/public/index.html b/ui/public/index.html deleted file mode 100644 index a48646e..0000000 --- a/ui/public/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - Svelte app - - - - - - - - - - - - diff --git a/ui/rollup.config.js b/ui/rollup.config.js deleted file mode 100644 index 7b165f5..0000000 --- a/ui/rollup.config.js +++ /dev/null @@ -1,51 +0,0 @@ -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 - } -}; diff --git a/ui/src/app.html b/ui/src/app.html new file mode 100644 index 0000000..025cb9c --- /dev/null +++ b/ui/src/app.html @@ -0,0 +1,23 @@ + + + + + + + + Svelte app + + + + + %sveltekit.head% + + + + + +
+ %sveltekit.body% +
+ + diff --git a/ui/src/components/App.svelte b/ui/src/components/App.svelte deleted file mode 100644 index 0577043..0000000 --- a/ui/src/components/App.svelte +++ /dev/null @@ -1,57 +0,0 @@ -
- Arbeitszeiten -
-