From ac995b1b83313761b78dbe8a8217db11f1af12f5 Mon Sep 17 00:00:00 2001 From: Tim Lappe Date: Mon, 28 Apr 2025 07:42:42 +0200 Subject: [PATCH] Added edit calendar events and fixed timezone --- .vscode/settings.json | 3 +- backend/.env | 2 +- backend/composer.json | 7 +- backend/composer.lock | 349 +++++++++++++++++- backend/config/bundles.php | 2 + backend/config/packages/monolog.yaml | 62 ++++ backend/config/packages/web_profiler.yaml | 11 + backend/config/routes/web_profiler.yaml | 8 + backend/config/services.yaml | 1 + .../Controller/Event/GetEventsController.php | 3 +- .../Event/PersistEventController.php | 35 ++ backend/src/Application/DTO/EventDraftDTO.php | 48 ++- .../src/Application/DTO/PersistEventDTO.php | 24 ++ .../src/Domain/Event/GenerateDraftHandler.php | 23 +- backend/src/Domain/Event/PersistEvent.php | 19 + .../src/Domain/Event/PersistEventHandler.php | 28 ++ backend/src/Domain/Model/EventDraft.php | 58 +-- .../Domain/Model/Persisted/PersistedEvent.php | 15 + .../EventSubscriber/TimezoneSubscriber.php | 33 ++ .../Repository/EventRepository.php | 6 + backend/symfony.lock | 25 ++ docker-compose.yml | 8 +- docker/Dockerfile | 8 +- docker/nginx/default.conf | 5 + frontend/src/lib/api/endpoints.ts | 13 +- frontend/src/lib/api/useApi.ts | 23 +- frontend/src/pages/edit-event/EditEvent.tsx | 17 +- 27 files changed, 738 insertions(+), 98 deletions(-) create mode 100644 backend/config/packages/monolog.yaml create mode 100644 backend/config/packages/web_profiler.yaml create mode 100644 backend/config/routes/web_profiler.yaml create mode 100644 backend/src/Application/Controller/Event/PersistEventController.php create mode 100644 backend/src/Application/DTO/PersistEventDTO.php create mode 100644 backend/src/Domain/Event/PersistEvent.php create mode 100644 backend/src/Domain/Event/PersistEventHandler.php create mode 100644 backend/src/Infrastructure/EventSubscriber/TimezoneSubscriber.php diff --git a/.vscode/settings.json b/.vscode/settings.json index be8fa1d..cf7d82c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,6 @@ "phpstan.configFile": "backend/phpstan.dist.neon", "phpstan.checkValidity": true, "phpstan.showTypeOnHover": false, - "phpstan.showProgress": true + "phpstan.showProgress": true, + "php.version": "8.4" } \ No newline at end of file diff --git a/backend/.env b/backend/.env index bcfb9ad..cf0dcf8 100644 --- a/backend/.env +++ b/backend/.env @@ -26,5 +26,5 @@ APP_SECRET=71bf50bfb778d456b3a376ff60d5dcd8 # DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db" # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4" # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" -DATABASE_URL="postgresql://postgres:postgres@calendi-postgres:5432/postgres?serverVersion=16&charset=utf8" +DATABASE_URL="postgresql://postgres:postgres@calendi-postgres.test:5432/postgres?serverVersion=16&charset=utf8" ###< doctrine/doctrine-bundle ### diff --git a/backend/composer.json b/backend/composer.json index d913c4b..973a28b 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -4,7 +4,7 @@ "minimum-stability": "stable", "prefer-stable": true, "require": { - "php": ">=8.1", + "php": ">=8.4", "ext-ctype": "*", "ext-iconv": "*", "doctrine/annotations": "^2.0", @@ -20,6 +20,7 @@ "symfony/flex": "^2", "symfony/framework-bundle": "6.4.*", "symfony/http-client": "6.4.*", + "symfony/monolog-bundle": "^3.10", "symfony/property-access": "6.4.*", "symfony/property-info": "6.4.*", "symfony/runtime": "6.4.*", @@ -83,6 +84,8 @@ "doctrine/doctrine-fixtures-bundle": "^4.1", "phpstan/phpstan": "^2.1", "phpstan/phpstan-symfony": "^2.0", - "symfony/maker-bundle": "^1.62" + "symfony/maker-bundle": "^1.62", + "symfony/stopwatch": "6.4.*", + "symfony/web-profiler-bundle": "6.4.*" } } diff --git a/backend/composer.lock b/backend/composer.lock index 2aa2743..faba828 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f41287711c3c1d476ebbca47f5b529b5", + "content-hash": "7ec99e86c547c32beef698aea0e9e346", "packages": [ { "name": "doctrine/annotations", @@ -1202,6 +1202,109 @@ }, "time": "2025-01-24T11:45:48+00:00" }, + { + "name": "monolog/monolog", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2025-03-24T10:02:05+00:00" + }, { "name": "nelmio/api-doc-bundle", "version": "v5.0.1", @@ -3485,6 +3588,166 @@ ], "time": "2025-03-28T13:27:10+00:00" }, + { + "name": "symfony/monolog-bridge", + "version": "v6.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "9d14621e59f22c2b6d030d92d37ffe5ae1e60452" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/9d14621e59f22c2b6d030d92d37ffe5ae1e60452", + "reference": "9d14621e59f22c2b6d030d92d37ffe5ae1e60452", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.25.1|^2|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/security-core": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/mailer": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Monolog\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Monolog with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/monolog-bridge/tree/v6.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-14T08:49:08+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v3.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.25.1 || ^2.0 || ^3.0", + "php": ">=7.2.5", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/phpunit-bridge": "^6.3 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MonologBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony MonologBundle", + "homepage": "https://symfony.com", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/symfony/monolog-bundle/issues", + "source": "https://github.com/symfony/monolog-bundle/tree/v3.10.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-06T17:08:13+00:00" + }, { "name": "symfony/options-resolver", "version": "v6.4.16", @@ -6027,6 +6290,88 @@ } ], "time": "2025-03-10T17:11:00+00:00" + }, + { + "name": "symfony/web-profiler-bundle", + "version": "v6.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/web-profiler-bundle.git", + "reference": "7d1026a8e950d416cb5148ae88ac23db5d264839" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/7d1026a8e950d416cb5148ae88ac23db5d264839", + "reference": "7d1026a8e950d416cb5148ae88ac23db5d264839", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "conflict": { + "symfony/form": "<5.4", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4", + "symfony/twig-bundle": ">=7.0" + }, + "require-dev": { + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\WebProfilerBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a development tool that gives detailed information about the execution of any request", + "homepage": "https://symfony.com", + "keywords": [ + "dev" + ], + "support": { + "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-14T12:21:59+00:00" } ], "aliases": [], @@ -6035,7 +6380,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=8.1", + "php": ">=8.4", "ext-ctype": "*", "ext-iconv": "*" }, diff --git a/backend/config/bundles.php b/backend/config/bundles.php index c6fb3fd..fd4352c 100644 --- a/backend/config/bundles.php +++ b/backend/config/bundles.php @@ -8,4 +8,6 @@ return [ Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], ]; diff --git a/backend/config/packages/monolog.yaml b/backend/config/packages/monolog.yaml new file mode 100644 index 0000000..9db7d8a --- /dev/null +++ b/backend/config/packages/monolog.yaml @@ -0,0 +1,62 @@ +monolog: + channels: + - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists + +when@dev: + monolog: + handlers: + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + channels: ["!event"] + # uncomment to get logging in your browser + # you may have to allow bigger header sizes in your Web server configuration + #firephp: + # type: firephp + # level: info + #chromephp: + # type: chromephp + # level: info + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine", "!console"] + +when@test: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + channels: ["!event"] + nested: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + +when@prod: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + buffer_size: 50 # How many messages should be saved? Prevent memory leaks + nested: + type: stream + path: php://stderr + level: debug + formatter: monolog.formatter.json + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine"] + deprecation: + type: stream + channels: [deprecation] + path: php://stderr + formatter: monolog.formatter.json diff --git a/backend/config/packages/web_profiler.yaml b/backend/config/packages/web_profiler.yaml new file mode 100644 index 0000000..1e039b7 --- /dev/null +++ b/backend/config/packages/web_profiler.yaml @@ -0,0 +1,11 @@ +when@dev: + web_profiler: + toolbar: true + + framework: + profiler: + collect_serializer_data: true + +when@test: + framework: + profiler: { collect: false } diff --git a/backend/config/routes/web_profiler.yaml b/backend/config/routes/web_profiler.yaml new file mode 100644 index 0000000..8d85319 --- /dev/null +++ b/backend/config/routes/web_profiler.yaml @@ -0,0 +1,8 @@ +when@dev: + web_profiler_wdt: + resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' + prefix: /_wdt + + web_profiler_profiler: + resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' + prefix: /_profiler diff --git a/backend/config/services.yaml b/backend/config/services.yaml index 2d6a76f..c10583a 100644 --- a/backend/config/services.yaml +++ b/backend/config/services.yaml @@ -4,6 +4,7 @@ # Put parameters here that don't need to change on each machine where the app is deployed # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration parameters: + app.timezone: 'Europe/Berlin' services: # default configuration for services in *this* file diff --git a/backend/src/Application/Controller/Event/GetEventsController.php b/backend/src/Application/Controller/Event/GetEventsController.php index 69d1166..c3a19a6 100644 --- a/backend/src/Application/Controller/Event/GetEventsController.php +++ b/backend/src/Application/Controller/Event/GetEventsController.php @@ -15,6 +15,7 @@ use Nelmio\ApiDocBundle\Attribute\Model; use OpenApi\Attributes as OA; #[Route('/api/events', name: 'api_events_')] +#[OA\Tag(name: 'Events')] class GetEventsController extends AbstractController { public function __construct( @@ -23,7 +24,6 @@ class GetEventsController extends AbstractController } #[Route('', name: 'list', methods: ['GET'])] - #[OA\Tag(name: 'Events')] #[OA\Response( response: 200, description: 'Returns list of events', @@ -39,7 +39,6 @@ class GetEventsController extends AbstractController } #[Route('/{id}', name: 'get', methods: ['GET'])] - #[OA\Tag(name: 'Events')] #[OA\Parameter( name: 'id', description: 'Event ID', diff --git a/backend/src/Application/Controller/Event/PersistEventController.php b/backend/src/Application/Controller/Event/PersistEventController.php new file mode 100644 index 0000000..11f92ba --- /dev/null +++ b/backend/src/Application/Controller/Event/PersistEventController.php @@ -0,0 +1,35 @@ +persistEventHandler->handle($dto->toDomain()); + return $this->json(['message' => 'Event persisted']); + } + + #[Route('/{id}', name: 'update', methods: ['PUT'])] + public function update(#[MapRequestPayload] PersistEventDTO $dto, string $id): JsonResponse + { + $this->persistEventHandler->handle($dto->toDomain()->withId($id)); + return $this->json(['message' => 'Event updated']); + } +} \ No newline at end of file diff --git a/backend/src/Application/DTO/EventDraftDTO.php b/backend/src/Application/DTO/EventDraftDTO.php index 404be83..bcfd33c 100644 --- a/backend/src/Application/DTO/EventDraftDTO.php +++ b/backend/src/Application/DTO/EventDraftDTO.php @@ -5,36 +5,48 @@ declare(strict_types=1); namespace App\Application\DTO; use App\Domain\Model\EventDraft; +use DateTimeInterface; use OpenApi\Attributes as OA; #[OA\Schema] final readonly class EventDraftDTO { public function __construct( - #[OA\Property(type: 'string')] - public string $id, - #[OA\Property(type: 'string')] - public string $title, - #[OA\Property(type: 'string')] - public string $description, - #[OA\Property(type: 'string')] - public ?string $start, - #[OA\Property(type: 'string')] - public ?string $end, - #[OA\Property(type: 'boolean')] - public bool $allDay + #[OA\Property(type: 'string', nullable: true)] + public ?string $title = null, + #[OA\Property(type: 'string', nullable: true)] + public ?string $description = null, + #[OA\Property(type: 'string', nullable: true)] + public ?string $location = null, + #[OA\Property(type: 'datetime', nullable: true)] + public ?DateTimeInterface $start = null, + #[OA\Property(type: 'datetime', nullable: true)] + public ?DateTimeInterface $end = null, + #[OA\Property(type: 'boolean', nullable: true)] + public ?bool $allDay = null, ) { } + public function toDomain(): EventDraft + { + return new EventDraft( + $this->title, + $this->description, + $this->location, + $this->start, + $this->end, + $this->allDay, + ); + } public static function fromDraft(EventDraft $draft): self { return new self( - $draft->title(), - $draft->description(), - $draft->location(), - $draft->start()?->format('Y-m-d H:i:s'), - $draft->end()?->format('Y-m-d H:i:s'), - $draft->allDay() + $draft->title, + $draft->description, + $draft->location, + $draft->start, + $draft->end, + $draft->allDay, ); } } \ No newline at end of file diff --git a/backend/src/Application/DTO/PersistEventDTO.php b/backend/src/Application/DTO/PersistEventDTO.php new file mode 100644 index 0000000..625d9f1 --- /dev/null +++ b/backend/src/Application/DTO/PersistEventDTO.php @@ -0,0 +1,24 @@ +draft->toDomain(), + ); + } +} \ No newline at end of file diff --git a/backend/src/Domain/Event/GenerateDraftHandler.php b/backend/src/Domain/Event/GenerateDraftHandler.php index 99c340c..694aac7 100644 --- a/backend/src/Domain/Event/GenerateDraftHandler.php +++ b/backend/src/Domain/Event/GenerateDraftHandler.php @@ -6,12 +6,14 @@ use App\Domain\Chat\ChatProviderInterface; use App\Domain\Chat\ChatSession; use App\Domain\Model\EventDraft; use App\Domain\User\UserContextProvider; +use Psr\Log\LoggerInterface; class GenerateDraftHandler { public function __construct( private readonly ChatProviderInterface $chatProvider, private readonly UserContextProvider $userContextProvider, + private readonly LoggerInterface $logger, ) { } @@ -19,14 +21,20 @@ class GenerateDraftHandler { $userContext = $this->userContextProvider->getUserContext(); $chat = new ChatSession($this->chatProvider); - $chat->system(<<now()->format('Y-m-d H:i:s')} + - Timezone: {$userContext->now()->getTimezone()->getName()} You will only respond with the JSON object in ```json``` tags, nothing else. - PROMPT); + PROMPT; + + $chat->system($systemPrompt); + + $this->logger->info('Generating draft for input: ' . $generateDraft->input() . " System prompt: " . $systemPrompt); $chat->user($generateDraft->input()); $chat->commit(reasoning: false); diff --git a/backend/src/Domain/Event/PersistEvent.php b/backend/src/Domain/Event/PersistEvent.php new file mode 100644 index 0000000..8f201f5 --- /dev/null +++ b/backend/src/Domain/Event/PersistEvent.php @@ -0,0 +1,19 @@ +draft, $id); + } +} \ No newline at end of file diff --git a/backend/src/Domain/Event/PersistEventHandler.php b/backend/src/Domain/Event/PersistEventHandler.php new file mode 100644 index 0000000..5d95d1a --- /dev/null +++ b/backend/src/Domain/Event/PersistEventHandler.php @@ -0,0 +1,28 @@ +id !== null) { + $persistedEvent = $this->eventRepository->find($event->id); + if (!$persistedEvent) { + throw new \Exception('Event not found'); + } + } + + $this->eventRepository->save($event->draft->mergeIntoPersisted($persistedEvent)); + } +} \ No newline at end of file diff --git a/backend/src/Domain/Model/EventDraft.php b/backend/src/Domain/Model/EventDraft.php index ccef9fa..257efa5 100644 --- a/backend/src/Domain/Model/EventDraft.php +++ b/backend/src/Domain/Model/EventDraft.php @@ -2,47 +2,49 @@ namespace App\Domain\Model; +use App\Domain\Model\Persisted\PersistedEvent; use DateTimeInterface; class EventDraft { public function __construct( - private readonly string $title, - private readonly string $description, - private readonly string $location, - private readonly ?DateTimeInterface $start, - private readonly ?DateTimeInterface $end, - private readonly bool $allDay, + public readonly ?string $title, + public readonly ?string $description, + public readonly ?string $location, + public readonly ?DateTimeInterface $start, + public readonly ?DateTimeInterface $end, + public readonly ?bool $allDay, ) { } - public function title(): string + public function mergeIntoPersisted(PersistedEvent $persistedEvent = new PersistedEvent()): PersistedEvent { - return $this->title; - } + if (!$this->start instanceof \DateTimeImmutable) { + throw new \Exception('Start date must be a DateTimeImmutable'); + } - public function description(): string - { - return $this->description; - } + if (!$this->end instanceof \DateTimeImmutable) { + throw new \Exception('End date must be a DateTimeImmutable'); + } - public function location(): string - { - return $this->location; - } + if ($this->allDay === null) { + throw new \Exception('All day must be a boolean'); + } - public function start(): ?DateTimeInterface - { - return $this->start; - } + if ($this->title === null) { + throw new \Exception('Title must be a string'); + } - public function end(): ?DateTimeInterface - { - return $this->end; - } + if ($this->description === null) { + throw new \Exception('Description must be a string'); + } - public function allDay(): bool - { - return $this->allDay; + return $persistedEvent + ->setTitle($this->title) + ->setDescription($this->description) + ->setLocation($this->location) + ->setStart($this->start) + ->setEnd($this->end) + ->setAllDay($this->allDay); } } \ No newline at end of file diff --git a/backend/src/Domain/Model/Persisted/PersistedEvent.php b/backend/src/Domain/Model/Persisted/PersistedEvent.php index 2121519..44029e4 100644 --- a/backend/src/Domain/Model/Persisted/PersistedEvent.php +++ b/backend/src/Domain/Model/Persisted/PersistedEvent.php @@ -23,6 +23,9 @@ class PersistedEvent #[ORM\Column(type: 'text', nullable: true)] private ?string $description = null; + #[ORM\Column(type: 'string', length: 255, nullable: true)] + private ?string $location = null; + #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'event_from')] #[Assert\NotNull] private \DateTimeImmutable $from; @@ -128,4 +131,16 @@ class PersistedEvent return $this; } + + public function getLocation(): ?string + { + return $this->location; + } + + public function setLocation(?string $location): self + { + $this->location = $location; + + return $this; + } } \ No newline at end of file diff --git a/backend/src/Infrastructure/EventSubscriber/TimezoneSubscriber.php b/backend/src/Infrastructure/EventSubscriber/TimezoneSubscriber.php new file mode 100644 index 0000000..a21176c --- /dev/null +++ b/backend/src/Infrastructure/EventSubscriber/TimezoneSubscriber.php @@ -0,0 +1,33 @@ + ['setTimezone', 100], + ]; + } + + public function setTimezone(RequestEvent $event): void + { + if (!$event->isMainRequest()) { + return; + } + + $timezone = $this->params->get('app.timezone'); + date_default_timezone_set($timezone); + } +} \ No newline at end of file diff --git a/backend/src/Infrastructure/Repository/EventRepository.php b/backend/src/Infrastructure/Repository/EventRepository.php index f92093f..4cfe134 100644 --- a/backend/src/Infrastructure/Repository/EventRepository.php +++ b/backend/src/Infrastructure/Repository/EventRepository.php @@ -16,6 +16,12 @@ class EventRepository extends ServiceEntityRepository parent::__construct($registry, PersistedEvent::class); } + public function save(PersistedEvent $event): void + { + $this->getEntityManager()->persist($event); + $this->getEntityManager()->flush(); + } + /** * @return array */ diff --git a/backend/symfony.lock b/backend/symfony.lock index ae277e7..ac7cd88 100644 --- a/backend/symfony.lock +++ b/backend/symfony.lock @@ -121,6 +121,18 @@ "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" } }, + "symfony/monolog-bundle": { + "version": "3.10", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.7", + "ref": "aff23899c4440dd995907613c1dd709b6f59503f" + }, + "files": [ + "config/packages/monolog.yaml" + ] + }, "symfony/routing": { "version": "6.4", "recipe": { @@ -170,5 +182,18 @@ "files": [ "config/packages/validator.yaml" ] + }, + "symfony/web-profiler-bundle": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.1", + "ref": "8b51135b84f4266e3b4c8a6dc23c9d1e32e543b7" + }, + "files": [ + "config/packages/web_profiler.yaml", + "config/routes/web_profiler.yaml" + ] } } diff --git a/docker-compose.yml b/docker-compose.yml index 812b3ec..c27f8c1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ services: app: image: app:latest + hostname: calendi.test build: context: . dockerfile: docker/Dockerfile @@ -17,7 +18,7 @@ services: - proxy postgres: - hostname: calendi-postgres + hostname: calendi-postgres.test image: postgres:15 environment: POSTGRES_USER: postgres @@ -29,6 +30,11 @@ services: - ./var/postgres_data:/var/lib/postgresql/data networks: - proxy + labels: + - "traefik.enable=true" + - "traefik.http.routers.postgres.entrypoints=postgres" + - "traefik.http.routers.postgres.rule=Host(`calendi-postgres.test`)" + - "traefik.http.services.postgres.loadbalancer.server.port=5432" networks: proxy: diff --git a/docker/Dockerfile b/docker/Dockerfile index d161843..9f129ee 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -10,7 +10,13 @@ RUN apt-get update && apt-get install -y \ apt-transport-https \ software-properties-common \ nginx \ - unzip + unzip \ + tzdata + +# Set timezone to Germany +RUN ln -fs /usr/share/zoneinfo/Europe/Berlin /etc/localtime && \ + dpkg-reconfigure -f noninteractive tzdata && \ + echo "Europe/Berlin" > /etc/timezone # Add PHP repository RUN wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg \ diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf index eeccb2f..d1be784 100644 --- a/docker/nginx/default.conf +++ b/docker/nginx/default.conf @@ -9,6 +9,11 @@ server { try_files $uri $uri/ /index.php$is_args$args; } + # PHP Backend API + location /_profiler { + try_files $uri $uri/ /index.php$is_args$args; + } + location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; diff --git a/frontend/src/lib/api/endpoints.ts b/frontend/src/lib/api/endpoints.ts index 16f7ae9..3864520 100644 --- a/frontend/src/lib/api/endpoints.ts +++ b/frontend/src/lib/api/endpoints.ts @@ -26,19 +26,16 @@ export type Event = { allDay: boolean; }; -export type CreateEventRequest = { - title: string; - description?: string; - start: string; - end: string; - allDay?: boolean; +export type PersistEventRequest = { + draft: EventDraft; + id?: string; }; // Event endpoints export const getEvents = () => get('/api/events'); export const getEvent = (id: string) => get(`/api/events/${id}`); -export const createEvent = (data: CreateEventRequest) => post('/api/events', data); -export const updateEvent = (id: string, data: Partial) => put(`/api/events/${id}`, data); +export const persistEvent = (data: PersistEventRequest) => post('/api/events', data); +export const updateEvent = (id: string, data: Partial) => put(`/api/events/${id}`, data); export const deleteEvent = (id: string) => del(`/api/events/${id}`); // Event draft types diff --git a/frontend/src/lib/api/useApi.ts b/frontend/src/lib/api/useApi.ts index 4b0f6bf..05c1fff 100644 --- a/frontend/src/lib/api/useApi.ts +++ b/frontend/src/lib/api/useApi.ts @@ -43,25 +43,4 @@ export function useApi( ); return [execute, state]; -} - -// Example usage: -// -// function UserList() { -// const [fetchUsers, { data: users, loading, error }] = useApi(getUsers, []); -// -// useEffect(() => { -// fetchUsers(); -// }, [fetchUsers]); -// -// if (loading) return
Loading...
; -// if (error) return
Error: {error}
; -// -// return ( -//
    -// {users?.map(user => ( -//
  • {user.name}
  • -// ))} -//
-// ); -// } \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/pages/edit-event/EditEvent.tsx b/frontend/src/pages/edit-event/EditEvent.tsx index 217b917..517eb2e 100644 --- a/frontend/src/pages/edit-event/EditEvent.tsx +++ b/frontend/src/pages/edit-event/EditEvent.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { EventDraft, createEvent } from '../../lib/api/endpoints'; +import { EventDraft, persistEvent } from '../../lib/api/endpoints'; import LoadingSpinner from '../../components/ui/LoadingSpinner'; import './EditEvent.css'; @@ -85,14 +85,17 @@ const EditEvent: React.FC = () => { setLoading(true); const eventData = { - title, - description, - start: startDate ? formatForServer(startDate, startTime || '00:00') : new Date().toISOString(), - end: endDate ? formatForServer(endDate, endTime || '00:00') : new Date().toISOString(), - allDay + draft: { + id: id || '', + title, + description, + start: startDate ? formatForServer(startDate, startTime || '00:00') : new Date().toISOString(), + end: endDate ? formatForServer(endDate, endTime || '00:00') : new Date().toISOString(), + allDay + } }; - const savedEvent = await createEvent(eventData); + const savedEvent = await persistEvent(eventData); console.log('Event created:', savedEvent); // Clear the draft from sessionStorage