Refactor elimination feature and improve backoffice usability

This commit introduces a refactored EliminationFactory for better modularity, updates the elimination preparation process, and adds functionality to view eliminations. Backoffice templates and forms have been reorganized, minor translations were corrected, and additional assets like styles and flashes were included for enhanced user experience.
This commit is contained in:
2025-05-30 20:38:20 +02:00
parent e0350c8c31
commit d3e5cb0569
45 changed files with 1569 additions and 978 deletions

View File

@@ -33,7 +33,9 @@ jobs:
*.cache-from=type=gha,scope=refs/heads/main *.cache-from=type=gha,scope=refs/heads/main
*.cache-to=type=gha,scope=${{github.ref}},mode=max *.cache-to=type=gha,scope=${{github.ref}},mode=max
- name: Start services - name: Start services
run: docker compose up --wait --no-build run: docker compose up php --wait --no-build
- name: Lint Twig templates
run: php bin/console lint:twig --format=github templates
- name: Coding Style - name: Coding Style
run: docker compose exec -T php vendor/bin/php-cs-fixer check --diff --show-progress=none run: docker compose exec -T php vendor/bin/php-cs-fixer check --diff --show-progress=none
- name: Twig Coding Style - name: Twig Coding Style

5
.gitignore vendored
View File

@@ -114,3 +114,8 @@ phpstan.neon
.phpunit.result.cache .phpunit.result.cache
/phpunit.xml /phpunit.xml
###< symfony/phpunit-bridge ### ###< symfony/phpunit-bridge ###
###> symfony/asset-mapper ###
/public/assets/
/assets/vendor/
###< symfony/asset-mapper ###

View File

@@ -159,6 +159,11 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/phpoffice/phpspreadsheet" /> <excludeFolder url="file://$MODULE_DIR$/vendor/phpoffice/phpspreadsheet" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" /> <excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-client" />
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/simple-cache" /> <excludeFolder url="file://$MODULE_DIR$/vendor/psr/simple-cache" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/asset-mapper" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/http-client-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfonycasts/sass-bundle" />
<excludeFolder url="file://$MODULE_DIR$/vendor/twig/intl-extra" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />

305
.idea/php.xml generated
View File

@@ -1,5 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="LaravelPint">
<laravel_pint_settings>
<laravel_pint_by_interpreter asDefaultInterpreter="true" interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8">
<option name="timeout" value="30000" />
</laravel_pint_by_interpreter>
</laravel_pint_settings>
</component>
<component name="MessDetector">
<phpmd_settings>
<phpmd_by_interpreter asDefaultInterpreter="true" interpreter_id="96512cb2-7b9e-4e1d-bfa2-bf7f3be424c8" timeout="30000" />
</phpmd_settings>
</component>
<component name="MessDetectorOptionsConfiguration"> <component name="MessDetectorOptionsConfiguration">
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</component> </component>
@@ -29,159 +41,164 @@
</component> </component>
<component name="PhpIncludePathManager"> <component name="PhpIncludePathManager">
<include_path> <include_path>
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/string" />
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
<path value="$PROJECT_DIR$/vendor/runtime/frankenphp-symfony" />
<path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
<path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" /> <path value="$PROJECT_DIR$/vendor/psr/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
<path value="$PROJECT_DIR$/vendor/psr/cache" /> <path value="$PROJECT_DIR$/vendor/psr/cache" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<path value="$PROJECT_DIR$/vendor/psr/log" />
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
<path value="$PROJECT_DIR$/vendor/psr/container" /> <path value="$PROJECT_DIR$/vendor/psr/container" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" /> <path value="$PROJECT_DIR$/vendor/psr/http-message" />
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" /> <path value="$PROJECT_DIR$/vendor/psr/log" />
<path value="$PROJECT_DIR$/vendor/symfony/config" /> <path value="$PROJECT_DIR$/vendor/psr/http-client" />
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/console" />
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/process" />
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
<path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan" />
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/vendor/rector/rector" />
<path value="$PROJECT_DIR$/vendor/react/event-loop" />
<path value="$PROJECT_DIR$/vendor/react/cache" />
<path value="$PROJECT_DIR$/vendor/react/stream" />
<path value="$PROJECT_DIR$/vendor/clue/ndjson-react" />
<path value="$PROJECT_DIR$/vendor/react/promise" />
<path value="$PROJECT_DIR$/vendor/react/dns" />
<path value="$PROJECT_DIR$/vendor/friendsofphp/php-cs-fixer" />
<path value="$PROJECT_DIR$/vendor/react/socket" />
<path value="$PROJECT_DIR$/vendor/react/child-process" />
<path value="$PROJECT_DIR$/vendor/evenement/evenement" />
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
<path value="$PROJECT_DIR$/vendor/fidry/cpu-core-counter" />
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
<path value="$PROJECT_DIR$/vendor/twig/twig" />
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
<path value="$PROJECT_DIR$/vendor/psr/clock" /> <path value="$PROJECT_DIR$/vendor/psr/clock" />
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" /> <path value="$PROJECT_DIR$/vendor/psr/simple-cache" />
<path value="$PROJECT_DIR$/vendor/symfony/type-info" /> <path value="$PROJECT_DIR$/vendor/clue/ndjson-react" />
<path value="$PROJECT_DIR$/vendor/symfony/property-access" /> <path value="$PROJECT_DIR$/vendor/psr/http-factory" />
<path value="$PROJECT_DIR$/vendor/symfony/property-info" /> <path value="$PROJECT_DIR$/vendor/twig/intl-extra" />
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" /> <path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/clock" /> <path value="$PROJECT_DIR$/vendor/twig/twig" />
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" /> <path value="$PROJECT_DIR$/vendor/twig/html-extra" />
<path value="$PROJECT_DIR$/vendor/symfony/security-core" /> <path value="$PROJECT_DIR$/vendor/nikic/php-parser" />
<path value="$PROJECT_DIR$/vendor/symfony/security-http" /> <path value="$PROJECT_DIR$/vendor/fidry/cpu-core-counter" />
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" /> <path value="$PROJECT_DIR$/vendor/react/cache" />
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" /> <path value="$PROJECT_DIR$/vendor/react/event-loop" />
<path value="$PROJECT_DIR$/vendor/phpstan/extension-installer" /> <path value="$PROJECT_DIR$/vendor/react/promise" />
<path value="$PROJECT_DIR$/vendor/react/stream" />
<path value="$PROJECT_DIR$/vendor/react/socket" />
<path value="$PROJECT_DIR$/vendor/react/dns" />
<path value="$PROJECT_DIR$/vendor/jean85/pretty-package-versions" />
<path value="$PROJECT_DIR$/vendor/react/child-process" />
<path value="$PROJECT_DIR$/vendor/sentry/sentry-symfony" />
<path value="$PROJECT_DIR$/vendor/rector/rector" />
<path value="$PROJECT_DIR$/vendor/staabm/side-effects-detector" />
<path value="$PROJECT_DIR$/vendor/sentry/sentry" />
<path value="$PROJECT_DIR$/vendor/myclabs/deep-copy" />
<path value="$PROJECT_DIR$/vendor/egulias/email-validator" />
<path value="$PROJECT_DIR$/vendor/phar-io/version" />
<path value="$PROJECT_DIR$/vendor/phar-io/manifest" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-doctrine" /> <path value="$PROJECT_DIR$/vendor/phpstan/phpstan-doctrine" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-phpunit" /> <path value="$PROJECT_DIR$/vendor/phpstan/phpstan-phpunit" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpstan-symfony" /> <path value="$PROJECT_DIR$/vendor/phpstan/phpstan-symfony" />
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
<path value="$PROJECT_DIR$/vendor/thecodingmachine/phpstan-safe-rule" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-icu" />
<path value="$PROJECT_DIR$/vendor/symfony/form" />
<path value="$PROJECT_DIR$/vendor/twig/html-extra" />
<path value="$PROJECT_DIR$/vendor/twig/extra-bundle" />
<path value="$PROJECT_DIR$/vendor/easycorp/easyadmin-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/mime" />
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
<path value="$PROJECT_DIR$/vendor/symfony/ux-twig-component" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" />
<path value="$PROJECT_DIR$/vendor/symfony/intl" />
<path value="$PROJECT_DIR$/vendor/symfony/translation" />
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
<path value="$PROJECT_DIR$/vendor/vincentlanglet/twig-cs-fixer" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php84" />
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
<path value="$PROJECT_DIR$/vendor/egulias/email-validator" />
<path value="$PROJECT_DIR$/vendor/symfony/mailer" />
<path value="$PROJECT_DIR$/vendor/symfonycasts/verify-email-bundle" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
<path value="$PROJECT_DIR$/vendor/jean85/pretty-package-versions" />
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
<path value="$PROJECT_DIR$/vendor/sentry/sentry-symfony" />
<path value="$PROJECT_DIR$/vendor/sentry/sentry" />
<path value="$PROJECT_DIR$/vendor/symfony/psr-http-message-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
<path value="$PROJECT_DIR$/vendor/symfony/phpunit-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
<path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" /> <path value="$PROJECT_DIR$/vendor/phpstan/phpdoc-parser" />
<path value="$PROJECT_DIR$/vendor/markbaker/complex" /> <path value="$PROJECT_DIR$/vendor/phpstan/extension-installer" />
<path value="$PROJECT_DIR$/vendor/markbaker/matrix" /> <path value="$PROJECT_DIR$/vendor/phpunit/phpunit" />
<path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" /> <path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/vendor/psr/http-client" /> <path value="$PROJECT_DIR$/vendor/phpunit/php-code-coverage" />
<path value="$PROJECT_DIR$/vendor/psr/simple-cache" /> <path value="$PROJECT_DIR$/vendor/phpunit/php-text-template" />
<path value="$PROJECT_DIR$/vendor/maennchen/zipstream-php" /> <path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-invoker" />
<path value="$PROJECT_DIR$/vendor/symfony/framework-bundle" />
<path value="$PROJECT_DIR$/vendor/runtime/frankenphp-symfony" />
<path value="$PROJECT_DIR$/vendor/symfony/form" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-icu" />
<path value="$PROJECT_DIR$/vendor/symfony/dotenv" />
<path value="$PROJECT_DIR$/vendor/symfony/serializer" />
<path value="$PROJECT_DIR$/vendor/symfony/cache" />
<path value="$PROJECT_DIR$/vendor/symfony/validator" />
<path value="$PROJECT_DIR$/vendor/symfony/browser-kit" />
<path value="$PROJECT_DIR$/vendor/symfony/asset" />
<path value="$PROJECT_DIR$/vendor/symfony/intl" />
<path value="$PROJECT_DIR$/vendor/symfony/security-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/translation" />
<path value="$PROJECT_DIR$/vendor/symfony/security-http" />
<path value="$PROJECT_DIR$/vendor/symfony/config" />
<path value="$PROJECT_DIR$/vendor/symfony/asset-mapper" />
<path value="$PROJECT_DIR$/vendor/symfony/uid" />
<path value="$PROJECT_DIR$/vendor/symfony/runtime" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher" />
<path value="$PROJECT_DIR$/vendor/symfony/var-dumper" />
<path value="$PROJECT_DIR$/vendor/symfony/phpunit-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/filesystem" />
<path value="$PROJECT_DIR$/vendor/symfony/mailer" />
<path value="$PROJECT_DIR$/vendor/symfony/security-csrf" />
<path value="$PROJECT_DIR$/vendor/symfony/options-resolver" />
<path value="$PROJECT_DIR$/vendor/symfony/maker-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-idn" />
<path value="$PROJECT_DIR$/vendor/symfony/web-profiler-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/css-selector" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<path value="$PROJECT_DIR$/vendor/symfony/dom-crawler" />
<path value="$PROJECT_DIR$/vendor/symfony/event-dispatcher-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/twig-bundle" />
<path value="$PROJECT_DIR$/vendor/symfony/property-access" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php84" />
<path value="$PROJECT_DIR$/vendor/symfony/finder" />
<path value="$PROJECT_DIR$/vendor/symfony/stopwatch" />
<path value="$PROJECT_DIR$/vendor/symfony/http-client" />
<path value="$PROJECT_DIR$/vendor/symfony/http-kernel" />
<path value="$PROJECT_DIR$/vendor/symfony/psr-http-message-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/mime" />
<path value="$PROJECT_DIR$/vendor/symfony/yaml" /> <path value="$PROJECT_DIR$/vendor/symfony/yaml" />
<path value="$PROJECT_DIR$/vendor/symfony/security-core" />
<path value="$PROJECT_DIR$/vendor/symfony/string" />
<path value="$PROJECT_DIR$/vendor/symfony/routing" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/http-client-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/service-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/type-info" />
<path value="$PROJECT_DIR$/vendor/symfony/property-info" />
<path value="$PROJECT_DIR$/vendor/symfony/doctrine-bridge" />
<path value="$PROJECT_DIR$/vendor/symfony/http-foundation" />
<path value="$PROJECT_DIR$/vendor/symfony/flex" />
<path value="$PROJECT_DIR$/vendor/symfony/var-exporter" />
<path value="$PROJECT_DIR$/vendor/symfony/cache-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/error-handler" />
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
<path value="$PROJECT_DIR$/vendor/symfony/dependency-injection" />
<path value="$PROJECT_DIR$/vendor/symfony/process" />
<path value="$PROJECT_DIR$/vendor/symfony/password-hasher" />
<path value="$PROJECT_DIR$/vendor/symfony/ux-twig-component" />
<path value="$PROJECT_DIR$/vendor/symfony/console" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-migrations-bundle" />
<path value="$PROJECT_DIR$/vendor/doctrine/data-fixtures" />
<path value="$PROJECT_DIR$/vendor/doctrine/migrations" />
<path value="$PROJECT_DIR$/vendor/doctrine/dbal" />
<path value="$PROJECT_DIR$/vendor/doctrine/lexer" />
<path value="$PROJECT_DIR$/vendor/doctrine/persistence" />
<path value="$PROJECT_DIR$/vendor/doctrine/inflector" />
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-fixtures-bundle" />
<path value="$PROJECT_DIR$/vendor/doctrine/event-manager" />
<path value="$PROJECT_DIR$/vendor/doctrine/orm" />
<path value="$PROJECT_DIR$/vendor/doctrine/sql-formatter" />
<path value="$PROJECT_DIR$/vendor/doctrine/collections" />
<path value="$PROJECT_DIR$/vendor/doctrine/doctrine-bundle" />
<path value="$PROJECT_DIR$/vendor/doctrine/deprecations" />
<path value="$PROJECT_DIR$/vendor/evenement/evenement" />
<path value="$PROJECT_DIR$/vendor/easycorp/easyadmin-bundle" />
<path value="$PROJECT_DIR$/vendor/markbaker/complex" />
<path value="$PROJECT_DIR$/vendor/maennchen/zipstream-php" />
<path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" />
<path value="$PROJECT_DIR$/vendor/markbaker/matrix" />
<path value="$PROJECT_DIR$/vendor/sebastian/comparator" />
<path value="$PROJECT_DIR$/vendor/ralouphie/getallheaders" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/vendor/sebastian/version" />
<path value="$PROJECT_DIR$/vendor/sebastian/exporter" />
<path value="$PROJECT_DIR$/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/vendor/guzzlehttp/psr7" />
<path value="$PROJECT_DIR$/vendor/webmozart/assert" />
<path value="$PROJECT_DIR$/vendor/friendsofphp/php-cs-fixer" />
<path value="$PROJECT_DIR$/vendor/masterminds/html5" />
<path value="$PROJECT_DIR$/vendor/symfonycasts/sass-bundle" />
<path value="$PROJECT_DIR$/vendor/symfonycasts/verify-email-bundle" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/type-resolver" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-common" />
<path value="$PROJECT_DIR$/vendor/vincentlanglet/twig-cs-fixer" />
<path value="$PROJECT_DIR$/vendor/phpdocumentor/reflection-docblock" />
<path value="$PROJECT_DIR$/vendor/thecodingmachine/phpstan-safe-rule" />
<path value="$PROJECT_DIR$/vendor/thecodingmachine/safe" />
<path value="$PROJECT_DIR$/vendor/composer" />
</include_path> </include_path>
</component> </component>
<component name="PhpInterpreters"> <component name="PhpInterpreters">

View File

@@ -99,4 +99,7 @@ RUN set -eux; \
composer dump-autoload --classmap-authoritative --no-dev; \ composer dump-autoload --classmap-authoritative --no-dev; \
composer dump-env prod; \ composer dump-env prod; \
composer run-script --no-dev post-install-cmd; \ composer run-script --no-dev post-install-cmd; \
chmod +x bin/console; sync; chmod +x bin/console; \
bin/console sass:build \
bin/console asset-map:compile --no-debug --quiet --no-ansi; \
sync;

4
assets/backoffice.js Normal file
View File

@@ -0,0 +1,4 @@
import 'bootstrap/dist/css/bootstrap.min.css'
import * as bootstrap from 'bootstrap'
import './styles/app.scss';

View File

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 164 KiB

12
assets/styles/quiz.scss Normal file
View File

@@ -0,0 +1,12 @@
html, body {
height: 100%;
background-image: url("../img/background.png");
background-position: center center;
background-repeat: no-repeat;
background-color: black;
color: white;
display: grid;
align-items: center;
justify-self: center;
}

View File

@@ -31,6 +31,17 @@ services:
- target: 443 - target: 443
published: ${HTTP3_PORT:-443} published: ${HTTP3_PORT:-443}
protocol: udp protocol: udp
sass:
image: ${IMAGES_PREFIX:-}app-php
volumes:
- ./:/app
entrypoint: ''
depends_on:
- php
command:
- bin/console
- sass:build
- --watch
###> symfony/mercure-bundle ### ###> symfony/mercure-bundle ###
###< symfony/mercure-bundle ### ###< symfony/mercure-bundle ###

View File

@@ -11,18 +11,19 @@
"ext-iconv": "*", "ext-iconv": "*",
"doctrine/dbal": "^4.2.3", "doctrine/dbal": "^4.2.3",
"doctrine/doctrine-bundle": "^2.14.0", "doctrine/doctrine-bundle": "^2.14.0",
"doctrine/doctrine-migrations-bundle": "^3.4.1", "doctrine/doctrine-migrations-bundle": "^3.4.2",
"doctrine/orm": "^3.3.2", "doctrine/orm": "^3.3.3",
"easycorp/easyadmin-bundle": "^4.24.6", "easycorp/easyadmin-bundle": "^4.24.7",
"phpdocumentor/reflection-docblock": "^5.6", "phpdocumentor/reflection-docblock": "^5.6.2",
"phpoffice/phpspreadsheet": "*", "phpoffice/phpspreadsheet": "^4.2.0",
"phpstan/phpdoc-parser": "^2.1", "phpstan/phpdoc-parser": "^2.1",
"runtime/frankenphp-symfony": "^0.2.0", "runtime/frankenphp-symfony": "^0.2.0",
"sentry/sentry-symfony": "^5.2", "sentry/sentry-symfony": "^5.2",
"symfony/asset": "7.2.*", "symfony/asset": "7.2.*",
"symfony/asset-mapper": "7.2.*",
"symfony/console": "7.2.*", "symfony/console": "7.2.*",
"symfony/dotenv": "7.2.*", "symfony/dotenv": "7.2.*",
"symfony/flex": "^2.5.0", "symfony/flex": "^2.7.0",
"symfony/form": "7.2.*", "symfony/form": "7.2.*",
"symfony/framework-bundle": "7.2.*", "symfony/framework-bundle": "7.2.*",
"symfony/mailer": "7.2.*", "symfony/mailer": "7.2.*",
@@ -35,28 +36,32 @@
"symfony/twig-bundle": "7.2.*", "symfony/twig-bundle": "7.2.*",
"symfony/uid": "7.2.*", "symfony/uid": "7.2.*",
"symfony/yaml": "7.2.*", "symfony/yaml": "7.2.*",
"symfonycasts/sass-bundle": "^0.8.2",
"symfonycasts/verify-email-bundle": "^1.17.3", "symfonycasts/verify-email-bundle": "^1.17.3",
"thecodingmachine/safe": "^3.1.0" "thecodingmachine/safe": "^3.3.0",
"twig/extra-bundle": "^3.21",
"twig/intl-extra": "^3.21",
"twig/twig": "^3.21.1"
}, },
"require-dev": { "require-dev": {
"doctrine/doctrine-fixtures-bundle": "^4.1", "doctrine/doctrine-fixtures-bundle": "^4.1",
"friendsofphp/php-cs-fixer": "^3.75.0", "friendsofphp/php-cs-fixer": "^3.75.0",
"phpstan/extension-installer": "^1.4.3", "phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan": "^2.1.12", "phpstan/phpstan": "^2.1.17",
"phpstan/phpstan-doctrine": "^2.0.2", "phpstan/phpstan-doctrine": "^2.0.3",
"phpstan/phpstan-phpunit": "^2.0.6", "phpstan/phpstan-phpunit": "^2.0.6",
"phpstan/phpstan-symfony": "^2.0.4", "phpstan/phpstan-symfony": "^2.0.6",
"phpunit/phpunit": "^12.1.3", "phpunit/phpunit": "^12.1.6",
"rector/rector": "^2.0.12", "rector/rector": "^2.0.16",
"roave/security-advisories": "dev-latest", "roave/security-advisories": "dev-latest",
"symfony/browser-kit": "7.2.*", "symfony/browser-kit": "7.2.*",
"symfony/css-selector": "7.2.*", "symfony/css-selector": "7.2.*",
"symfony/maker-bundle": "^1.62.1", "symfony/maker-bundle": "^1.63.0",
"symfony/phpunit-bridge": "^7.2", "symfony/phpunit-bridge": "7.2.*",
"symfony/stopwatch": "7.2.*", "symfony/stopwatch": "7.2.*",
"symfony/web-profiler-bundle": "7.2.*", "symfony/web-profiler-bundle": "7.2.*",
"thecodingmachine/phpstan-safe-rule": "^1.4", "thecodingmachine/phpstan-safe-rule": "^1.4.1",
"vincentlanglet/twig-cs-fixer": "^3.5.1" "vincentlanglet/twig-cs-fixer": "^3.7.1"
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {
@@ -94,7 +99,8 @@
"scripts": { "scripts": {
"auto-scripts": { "auto-scripts": {
"cache:clear": "symfony-cmd", "cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd" "assets:install %PUBLIC_DIR%": "symfony-cmd",
"importmap:install": "symfony-cmd"
}, },
"post-install-cmd": [ "post-install-cmd": [
"@auto-scripts" "@auto-scripts"

1331
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@ use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle; use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
use Symfony\UX\TwigComponent\TwigComponentBundle; use Symfony\UX\TwigComponent\TwigComponentBundle;
use SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle; use SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle;
use Symfonycasts\SassBundle\SymfonycastsSassBundle;
use Twig\Extra\TwigExtraBundle\TwigExtraBundle; use Twig\Extra\TwigExtraBundle\TwigExtraBundle;
return [ return [
@@ -30,4 +31,5 @@ return [
DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
SymfonyCastsVerifyEmailBundle::class => ['all' => true], SymfonyCastsVerifyEmailBundle::class => ['all' => true],
SentryBundle::class => ['prod' => true], SentryBundle::class => ['prod' => true],
SymfonycastsSassBundle::class => ['all' => true],
]; ];

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Controller\Backoffice;
use App\Controller\AbstractController;
use App\Entity\Season;
use App\Entity\User;
use App\Form\CreateSeasonFormType;
use App\Repository\SeasonRepository;
use App\Service\QuizSpreadsheetService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[AsController]
#[IsGranted('ROLE_USER')]
final class BackofficeController extends AbstractController
{
public function __construct(
private readonly SeasonRepository $seasonRepository,
private readonly Security $security,
) {}
#[Route('/backoffice/', name: 'app_backoffice_index')]
public function index(): Response
{
$user = $this->getUser();
\assert($user instanceof User);
$seasons = $this->security->isGranted('ROLE_ADMIN')
? $this->seasonRepository->findAll()
: $this->seasonRepository->getSeasonsForUser($user);
return $this->render('backoffice/index.html.twig', [
'seasons' => $seasons,
]);
}
#[Route('/backoffice/add', name: 'app_backoffice_season_add', priority: 10)]
public function addSeason(Request $request, EntityManagerInterface $em): Response
{
$season = new Season();
$form = $this->createForm(CreateSeasonFormType::class, $season);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $this->getUser();
\assert($user instanceof User);
$season->addOwner($user);
$season->generateSeasonCode();
$em->persist($season);
$em->flush();
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
}
return $this->render('backoffice/season_add.html.twig', ['form' => $form]);
}
#[Route('/backoffice/template', name: 'app_backoffice_template', priority: 10)]
public function getTemplate(QuizSpreadsheetService $excel): Response
{
$response = new StreamedResponse($excel->generateTemplate());
$response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
$response->headers->set('Content-Disposition', 'attachment; filename="template.xlsx"');
return $response;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Controller\Backoffice;
use App\Controller\AbstractController;
use App\Entity\Quiz;
use App\Entity\Season;
use App\Repository\CandidateRepository;
use App\Security\Voter\SeasonVoter;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[AsController]
#[IsGranted('ROLE_USER')]
class QuizController extends AbstractController
{
public function __construct(
private readonly CandidateRepository $candidateRepository,
) {}
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}', name: 'app_backoffice_quiz')]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function index(Season $season, Quiz $quiz): Response
{
return $this->render('backoffice/quiz.html.twig', [
'season' => $season,
'quiz' => $quiz,
'result' => $this->candidateRepository->getScores($quiz),
]);
}
#[Route('/backoffice/season/{seasonCode}/quiz/{quiz}/enable', name: 'app_backoffice_enable')]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function enableQuiz(Season $season, ?Quiz $quiz, EntityManagerInterface $em): Response
{
$season->setActiveQuiz($quiz);
$em->flush();
if ($quiz instanceof Quiz) {
return $this->redirectToRoute('app_backoffice_quiz', ['seasonCode' => $season->getSeasonCode(), 'quiz' => $quiz->getId()]);
}
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace App\Controller\Backoffice;
use App\Controller\AbstractController;
use App\Entity\Candidate;
use App\Entity\Quiz;
use App\Entity\Season;
use App\Enum\FlashType;
use App\Form\AddCandidatesFormType;
use App\Form\UploadQuizFormType;
use App\Security\Voter\SeasonVoter;
use App\Service\QuizSpreadsheetService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsController]
#[IsGranted('ROLE_USER')]
class SeasonController extends AbstractController
{
public function __construct(private readonly TranslatorInterface $translator, private EntityManagerInterface $em,
) {}
#[Route('/backoffice/season/{seasonCode}', name: 'app_backoffice_season')]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function index(Season $season): Response
{
return $this->render('backoffice/season.html.twig', [
'season' => $season,
]);
}
#[Route('/backoffice/season/{seasonCode}/add_candidate', name: 'app_backoffice_add_candidates', priority: 10)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function addCandidates(Season $season, Request $request): Response
{
$form = $this->createForm(AddCandidatesFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$candidates = $form->get('candidates')->getData();
foreach (explode("\r\n", (string) $candidates) as $candidate) {
$season->addCandidate(new Candidate($candidate));
}
$this->em->flush();
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
}
return $this->render('backoffice/season_add_candidates.html.twig', ['form' => $form]);
}
#[Route('/backoffice/season/{seasonCode}/add', name: 'app_backoffice_quiz_add', priority: 10)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function addQuiz(Request $request, Season $season, QuizSpreadsheetService $quizSpreadsheet): Response
{
$quiz = new Quiz();
$form = $this->createForm(UploadQuizFormType::class, $quiz);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/* @var UploadedFile $sheet */
$sheet = $form->get('sheet')->getData();
$quizSpreadsheet->xlsxToQuiz($quiz, $sheet);
$quiz->setSeason($season);
$this->em->persist($quiz);
$this->em->flush();
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz Added!'));
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
}
return $this->render('/backoffice/quiz_add.html.twig', ['form' => $form, 'season' => $season]);
}
}

View File

@@ -1,167 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\Candidate;
use App\Entity\Quiz;
use App\Entity\Season;
use App\Entity\User;
use App\Enum\FlashType;
use App\Form\AddCandidatesFormType;
use App\Form\CreateSeasonFormType;
use App\Form\UploadQuizFormType;
use App\Repository\CandidateRepository;
use App\Repository\SeasonRepository;
use App\Security\Voter\SeasonVoter;
use App\Service\QuizSpreadsheetService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsController]
#[IsGranted('ROLE_USER')]
final class BackofficeController extends AbstractController
{
public function __construct(
private readonly SeasonRepository $seasonRepository,
private readonly CandidateRepository $candidateRepository,
private readonly Security $security,
private readonly TranslatorInterface $translator,
) {}
#[Route('/backoffice/', name: 'app_backoffice_index')]
public function index(): Response
{
$user = $this->getUser();
\assert($user instanceof User);
$seasons = $this->security->isGranted('ROLE_ADMIN')
? $this->seasonRepository->findAll()
: $this->seasonRepository->getSeasonsForUser($user);
return $this->render('backoffice/index.html.twig', [
'seasons' => $seasons,
]);
}
#[Route('/backoffice/add', name: 'app_backoffice_season_add', priority: 10)]
public function seasonAdd(Request $request, EntityManagerInterface $em): Response
{
$season = new Season();
$form = $this->createForm(CreateSeasonFormType::class, $season);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $this->getUser();
\assert($user instanceof User);
$season->addOwner($user);
$season->generateSeasonCode();
$em->persist($season);
$em->flush();
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
}
return $this->render('backoffice/season_add.html.twig', ['form' => $form]);
}
#[Route('/backoffice/{seasonCode}', name: 'app_backoffice_season')]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function season(Season $season): Response
{
return $this->render('backoffice/season.html.twig', [
'season' => $season,
]);
}
#[Route('/backoffice/{seasonCode}/{quiz}', name: 'app_backoffice_quiz')]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function quiz(Season $season, Quiz $quiz): Response
{
return $this->render('backoffice/quiz.html.twig', [
'season' => $season,
'quiz' => $quiz,
'result' => $this->candidateRepository->getScores($quiz),
]);
}
#[Route('/backoffice/{seasonCode}/{quiz}/enable', name: 'app_backoffice_enable')]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function enableQuiz(Season $season, ?Quiz $quiz, EntityManagerInterface $em): Response
{
$season->setActiveQuiz($quiz);
$em->flush();
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
}
#[Route('/backoffice/{seasonCode}/add_candidate', name: 'app_backoffice_add_candidates', priority: 10)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function addCandidates(Season $season, Request $request, EntityManagerInterface $em): Response
{
$form = $this->createForm(AddCandidatesFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$candidates = $form->get('candidates')->getData();
foreach (explode("\r\n", (string) $candidates) as $candidate) {
$season->addCandidate(new Candidate($candidate));
}
$em->flush();
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
}
return $this->render('backoffice/season_add_candidates.html.twig', ['form' => $form]);
}
#[Route('/backoffice/{seasonCode}/add', name: 'app_backoffice_quiz_add', priority: 10)]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
public function addQuiz(Request $request, Season $season, QuizSpreadsheetService $quizSpreadsheet, EntityManagerInterface $em): Response
{
$quiz = new Quiz();
$form = $this->createForm(UploadQuizFormType::class, $quiz);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/* @var UploadedFile $sheet */
$sheet = $form->get('sheet')->getData();
$quizSpreadsheet->xlsxToQuiz($quiz, $sheet);
$quiz->setSeason($season);
$em->persist($quiz);
$em->flush();
$this->addFlash(FlashType::Success, $this->translator->trans('Quiz Added!'));
return $this->redirectToRoute('app_backoffice_season', ['seasonCode' => $season->getSeasonCode()]);
}
return $this->render('/backoffice/quiz_add.html.twig', ['form' => $form, 'season' => $season]);
}
#[Route('/backoffice/template', name: 'app_backoffice_template', priority: 10)]
public function getTemplate(QuizSpreadsheetService $excel): Response
{
$response = new StreamedResponse($excel->generateTemplate());
$response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
$response->headers->set('Content-Disposition', 'attachment; filename="template.xlsx"');
return $response;
}
}

View File

@@ -4,17 +4,19 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use App\Enum\FlashType;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Contracts\Translation\TranslatorInterface;
#[AsController] #[AsController]
final class LoginController extends AbstractController final class LoginController extends AbstractController
{ {
#[Route(path: '/login', name: 'app_login_login')] #[Route(path: '/login', name: 'app_login_login')]
public function login(AuthenticationUtils $authenticationUtils): Response public function login(AuthenticationUtils $authenticationUtils, TranslatorInterface $translator): Response
{ {
// get the login error if there is one // get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError(); $error = $authenticationUtils->getLastAuthenticationError();
@@ -22,7 +24,11 @@ final class LoginController extends AbstractController
// last username entered by the user // last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername(); $lastUsername = $authenticationUtils->getLastUsername();
return $this->render('login/login.html.twig', [ if ($error instanceof AuthenticationException) {
$this->addFlash(FlashType::Danger, $translator->trans($error->getMessageKey(), $error->getMessageData(), 'security'));
}
return $this->render('backoffice/login/login.html.twig', [
'last_username' => $lastUsername, 'last_username' => $lastUsername,
'error' => $error, 'error' => $error,
]); ]);

View File

@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use App\Entity\Elimination;
use App\Entity\Quiz; use App\Entity\Quiz;
use App\Entity\Season; use App\Entity\Season;
use App\Factory\EliminationFactory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
@@ -13,12 +15,19 @@ use Symfony\Component\Routing\Attribute\Route;
final class PrepareEliminationController extends AbstractController final class PrepareEliminationController extends AbstractController
{ {
#[Route('/backoffice/elimination/{seasonCode}/{quiz}/prepare', name: 'app_prepare_elimination')] #[Route('/backoffice/elimination/{seasonCode}/{quiz}/prepare', name: 'app_prepare_elimination')]
public function index(Season $season, Quiz $quiz): Response public function index(Season $season, Quiz $quiz, EliminationFactory $eliminationFactory): Response
{ {
return $this->render('prepare_elimination/index.html.twig', [ $elimination = $eliminationFactory->createEliminationFromQuiz($quiz);
return $this->redirectToRoute('app_prepare_elimination_view', ['elimination' => $elimination->getId()]);
}
#[Route('/backoffice/elimination/{elimination}', name: 'app_prepare_elimination_view')]
public function viewElimination(Elimination $elimination): Response
{
return $this->render('backoffice/prepare_elimination/index.html.twig', [
'controller_name' => 'PrepareEliminationController', 'controller_name' => 'PrepareEliminationController',
'season' => $season, 'elimination' => $elimination,
'quiz' => $quiz,
]); ]);
} }
} }

View File

@@ -48,7 +48,7 @@ final class RegistrationController extends AbstractController
(new TemplatedEmail()) (new TemplatedEmail())
->to((string) $user->getEmail()) ->to((string) $user->getEmail())
->subject($this->translator->trans('Please Confirm your Email')) ->subject($this->translator->trans('Please Confirm your Email'))
->htmlTemplate('registration/confirmation_email.html.twig') ->htmlTemplate('backoffice/registration/confirmation_email.html.twig')
); );
$response = $security->login($user, 'form_login', 'main'); $response = $security->login($user, 'form_login', 'main');
@@ -57,7 +57,7 @@ final class RegistrationController extends AbstractController
return $response; return $response;
} }
return $this->render('registration/register.html.twig', [ return $this->render('backoffice/registration/register.html.twig', [
'registrationForm' => $form, 'registrationForm' => $form,
]); ]);
} }

View File

@@ -7,27 +7,37 @@ namespace App\Entity;
use App\Repository\EliminationRepository; use App\Repository\EliminationRepository;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Safe\DateTimeImmutable;
use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator; use Symfony\Bridge\Doctrine\IdGenerator\UuidGenerator;
use Symfony\Bridge\Doctrine\Types\UuidType; use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid; use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: EliminationRepository::class)] #[ORM\Entity(repositoryClass: EliminationRepository::class)]
#[ORM\HasLifecycleCallbacks]
class Elimination class Elimination
{ {
public const string SCREEN_GREEN = 'green';
public const string SCREEN_RED = 'red';
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: UuidType::NAME, unique: true)] #[ORM\Column(type: UuidType::NAME, unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')] #[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UuidGenerator::class)] #[ORM\CustomIdGenerator(class: UuidGenerator::class)]
private Uuid $id; private Uuid $id;
#[ORM\ManyToOne(inversedBy: 'eliminations')]
#[ORM\JoinColumn(nullable: false)]
private Quiz $quiz;
/** @var array<string, mixed> */ /** @var array<string, mixed> */
#[ORM\Column(type: Types::JSON)] #[ORM\Column(type: Types::JSON)]
private array $data = []; private array $data = [];
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: false)]
private \DateTimeImmutable $created;
public function __construct(
#[ORM\ManyToOne(inversedBy: 'eliminations')]
#[ORM\JoinColumn(nullable: false)]
private Quiz $quiz,
) {}
public function getId(): Uuid public function getId(): Uuid
{ {
return $this->id; return $this->id;
@@ -40,22 +50,26 @@ class Elimination
} }
/** @param array<string, mixed> $data */ /** @param array<string, mixed> $data */
public function setData(array $data): static public function setData(array $data): self
{ {
$this->data = $data; $this->data = $data;
return $this; return $this;
} }
public function setQuiz(Quiz $quiz): self
{
$this->quiz = $quiz;
return $this;
}
public function getQuiz(): Quiz public function getQuiz(): Quiz
{ {
return $this->quiz; return $this->quiz;
} }
#[ORM\PrePersist]
public function setCreatedAtValue(): void
{
$this->created = new DateTimeImmutable();
}
public function getCreated(): \DateTimeInterface
{
return $this->created;
}
} }

View File

@@ -38,11 +38,12 @@ class Quiz
#[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'quiz', orphanRemoval: true)] #[ORM\OneToMany(targetEntity: Correction::class, mappedBy: 'quiz', orphanRemoval: true)]
private Collection $corrections; private Collection $corrections;
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: false, options: ['default' => 1])]
private ?int $dropouts = null; private int $dropouts = 1;
/** @var Collection<int, Elimination> */ /** @var Collection<int, Elimination> */
#[ORM\OneToMany(targetEntity: Elimination::class, mappedBy: 'quiz', cascade: ['persist'], orphanRemoval: true)] #[ORM\OneToMany(targetEntity: Elimination::class, mappedBy: 'quiz', cascade: ['persist'], orphanRemoval: true)]
#[ORM\OrderBy(['created' => 'DESC'])]
private Collection $eliminations; private Collection $eliminations;
public function __construct() public function __construct()
@@ -113,12 +114,12 @@ class Quiz
return $this; return $this;
} }
public function getDropouts(): ?int public function getDropouts(): int
{ {
return $this->dropouts; return $this->dropouts;
} }
public function setDropouts(?int $dropouts): static public function setDropouts(int $dropouts): static
{ {
$this->dropouts = $dropouts; $this->dropouts = $dropouts;

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Factory;
use App\Entity\Elimination;
use App\Entity\Quiz;
use App\Repository\CandidateRepository;
use Doctrine\ORM\EntityManagerInterface;
final readonly class EliminationFactory
{
public function __construct(
private CandidateRepository $candidateRepository,
private EntityManagerInterface $em,
) {}
public function createEliminationFromQuiz(Quiz $quiz): Elimination
{
$elimination = new Elimination($quiz);
$this->em->persist($elimination);
$scores = $this->candidateRepository->getScores($quiz);
$simpleScores = [];
foreach (array_reverse($scores) as $i => $score) {
$simpleScores[$score['name']] = $i < $quiz->getDropouts() ? Elimination::SCREEN_RED : Elimination::SCREEN_GREEN;
}
$elimination->setData($simpleScores);
$this->em->flush();
return $elimination;
}
}

View File

@@ -19,7 +19,7 @@ class AddCandidatesFormType extends AbstractType
{ {
$builder $builder
->add('candidates', TextareaType::class, [ ->add('candidates', TextareaType::class, [
'label' => $this->translator->trans('Candidates'), 'label' => $this->translator->trans('Candidates'), 'translation_domain' => false,
]) ])
; ;
} }

View File

@@ -21,6 +21,7 @@ class CreateSeasonFormType extends AbstractType
$builder $builder
->add('name', TextType::class, [ ->add('name', TextType::class, [
'label' => $this->translator->trans('Season Name'), 'label' => $this->translator->trans('Season Name'),
'translation_domain' => false,
]) ])
; ;
} }

View File

@@ -18,7 +18,11 @@ class EnterNameType extends AbstractType
{ {
$builder $builder
->add('name', TextType::class, ->add('name', TextType::class,
['required' => true, 'label' => $this->translator->trans('Enter your name')], [
'required' => true,
'label' => $this->translator->trans('Enter your name'),
'translation_domain' => false,
],
) )
; ;
} }

View File

@@ -8,6 +8,7 @@ use App\Entity\User;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\Length;
@@ -27,11 +28,16 @@ class RegistrationFormType extends AbstractType
->add('email', EmailType::class, [ ->add('email', EmailType::class, [
'label' => $this->translator->trans('Email'), 'label' => $this->translator->trans('Email'),
'attr' => ['autocomplete' => 'email'], 'attr' => ['autocomplete' => 'email'],
'translation_domain' => false,
]) ])
->add('plainPassword', PasswordType::class, [ ->add('plainPassword', RepeatedType::class, [
'label' => $this->translator->trans('Password'), 'type' => PasswordType::class,
'invalid_message' => $this->translator->trans('The password fields must match.'),
'options' => ['attr' => ['class' => 'password-field']],
'required' => true,
'first_options' => ['label' => $this->translator->trans('Password')],
'second_options' => ['label' => $this->translator->trans('Repeat Password')],
'mapped' => false, 'mapped' => false,
'attr' => ['autocomplete' => 'new-password'],
'constraints' => [ 'constraints' => [
new NotBlank([ new NotBlank([
'message' => 'Please enter a password', 'message' => 'Please enter a password',
@@ -43,6 +49,7 @@ class RegistrationFormType extends AbstractType
'max' => 4096, 'max' => 4096,
]), ]),
], ],
'translation_domain' => false,
]) ])
; ;
} }

View File

@@ -20,7 +20,7 @@ class SelectSeasonType extends AbstractType
{ {
$builder $builder
->add('season_code', TextType::class, ->add('season_code', TextType::class,
['required' => true, 'constraints' => new Regex(pattern: "/^[A-Za-z\d]{5}$/"), 'label' => $this->translator->trans('Season Code')] ['required' => true, 'constraints' => new Regex(pattern: "/^[A-Za-z\d]{5}$/"), 'label' => $this->translator->trans('Season Code'), 'translation_domain' => false]
) )
; ;
} }

View File

@@ -23,11 +23,13 @@ class UploadQuizFormType extends AbstractType
$builder $builder
->add('name', TextType::class, [ ->add('name', TextType::class, [
'label' => $this->translator->trans('Quiz name'), 'label' => $this->translator->trans('Quiz name'),
'translation_domain' => false,
]) ])
->add('sheet', FileType::class, [ ->add('sheet', FileType::class, [
'label' => $this->translator->trans('Quiz (xlsx)'), 'label' => $this->translator->trans('Quiz (xlsx)'),
'mapped' => false, 'mapped' => false,
'required' => true, 'required' => true,
'translation_domain' => false,
'constraints' => [ 'constraints' => [
new File([ new File([
'maxSize' => '1024k', 'maxSize' => '1024k',

View File

@@ -13,11 +13,12 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\Query\Expr\Join;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
use Safe\Exceptions\UrlException; use Safe\Exceptions\UrlException;
use Symfony\Component\Uid\Uuid;
/** /**
* @extends ServiceEntityRepository<Candidate> * @extends ServiceEntityRepository<Candidate>
* *
* @phpstan-type Result array{0: Candidate, correct: int, time: \DateInterval, corrections?: float, score: float} * @phpstan-type Result array{id: Uuid, name: string, correct: int, time: \DateInterval, corrections?: float, score: float}
* @phpstan-type ResultList list<Result> * @phpstan-type ResultList list<Result>
*/ */
class CandidateRepository extends ServiceEntityRepository class CandidateRepository extends ServiceEntityRepository
@@ -56,7 +57,7 @@ class CandidateRepository extends ServiceEntityRepository
public function getScores(Quiz $quiz): array public function getScores(Quiz $quiz): array
{ {
$scoreTimeQb = $this->createQueryBuilder('c', 'c.id') $scoreTimeQb = $this->createQueryBuilder('c', 'c.id')
->select('c', 'sum(case when a.isRightAnswer = true then 1 else 0 end) as correct', 'max(ga.created) - min(ga.created) as time') ->select('c.id', 'c.name', 'sum(case when a.isRightAnswer = true then 1 else 0 end) as correct', 'max(ga.created) - min(ga.created) as time')
->join('c.givenAnswers', 'ga') ->join('c.givenAnswers', 'ga')
->join('ga.answer', 'a') ->join('ga.answer', 'a')
->where('ga.quiz = :quiz') ->where('ga.quiz = :quiz')
@@ -64,7 +65,7 @@ class CandidateRepository extends ServiceEntityRepository
->setParameter('quiz', $quiz); ->setParameter('quiz', $quiz);
$correctionsQb = $this->createQueryBuilder('c', 'c.id') $correctionsQb = $this->createQueryBuilder('c', 'c.id')
->select('c', 'cor.amount as corrections') ->select('c.id', 'cor.amount as corrections')
->innerJoin(Correction::class, 'cor', Join::WITH, 'cor.candidate = c and cor.quiz = :quiz') ->innerJoin(Correction::class, 'cor', Join::WITH, 'cor.candidate = c and cor.quiz = :quiz')
->setParameter('quiz', $quiz); ->setParameter('quiz', $quiz);
@@ -74,7 +75,7 @@ class CandidateRepository extends ServiceEntityRepository
} }
/** /**
* @param array<string, array{0: Candidate, correct: int, time: \DateInterval, corrections?: float}> $in * @param array<string, array{id: Uuid, name: string, correct: int, time: \DateInterval, corrections?: float}> $in
* *
* @return array<string, Result> * @return array<string, Result>
* */ * */

View File

@@ -52,14 +52,14 @@ class QuizSpreadsheetService
} }
/** @throws SpreadsheetDataException */ /** @throws SpreadsheetDataException */
public function xlsxToQuiz(Quiz $quiz, File $file): Quiz public function xlsxToQuiz(Quiz $quiz, File $file): void
{ {
$spreadsheet = $this->readSheet($file); $spreadsheet = $this->readSheet($file);
$sheet = $spreadsheet->getSheet($spreadsheet->getFirstSheetIndex()); $sheet = $spreadsheet->getSheet($spreadsheet->getFirstSheetIndex());
$answerLines = \array_slice($sheet->toArray(formatData: false), 1); $answerLines = \array_slice($sheet->toArray(formatData: false), 1);
return $this->fillQuizFromArray($quiz, $answerLines); $this->fillQuizFromArray($quiz, $answerLines);
} }
private function readSheet(File $file): Spreadsheet private function readSheet(File $file): Spreadsheet

View File

@@ -97,6 +97,21 @@
"config/packages/sentry.yaml" "config/packages/sentry.yaml"
] ]
}, },
"symfony/asset-mapper": {
"version": "7.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.4",
"ref": "5ad1308aa756d58f999ffbe1540d1189f5d7d14a"
},
"files": [
"assets/quiz.js",
"assets/styles/app.css",
"config/packages/asset_mapper.yaml",
"importmap.php"
]
},
"symfony/console": { "symfony/console": {
"version": "7.2", "version": "7.2",
"recipe": { "recipe": {
@@ -287,6 +302,9 @@
"config/routes/web_profiler.yaml" "config/routes/web_profiler.yaml"
] ]
}, },
"symfonycasts/sass-bundle": {
"version": "v0.8.2"
},
"symfonycasts/verify-email-bundle": { "symfonycasts/verify-email-bundle": {
"version": "v1.17.3" "version": "v1.17.3"
}, },

View File

@@ -1,49 +1,3 @@
<!DOCTYPE html> {% extends 'base.html.twig' %}
<html lang="nl" data-bs-theme="dark"> {% block importmap %}{{ importmap('backoffice') }}{% endblock %}
<head> {% block nav %}{{ include('backoffice/nav.html.twig') }}{% endblock %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<script
src="https://js-de.sentry-cdn.com/30cf438bc708c97e6f45c127bed9af96.min.js"
crossorigin="anonymous"
></script>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
<title>
{% block title %}Tijd voor de test{% endblock title %}
</title>
{% block stylesheets %}
{% endblock %}
{% block javascripts %}
{% endblock %}
</head>
<body>
{% block nav %}
{{ include('backoffice/nav.html.twig') }}
{% endblock nav %}
<main>
<div class="container">
{% for label, messages in app.flashes() %}
{% for message in messages %}
<div class="alert alert-{{ label }} alert-dismissible " role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endfor %}
{% block body %}
{% endblock body %}
</div>
</main>
</body>
</html>

View File

@@ -1,3 +1,40 @@
{% extends 'backoffice/base.html.twig' %} {% extends 'backoffice/base.html.twig' %}
{% block body %} {% block body %}
<div class="row">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ path('app_backoffice_index') }}">Home</a></li>
<li class="breadcrumb-item"><a
href="{{ path('app_backoffice_season', {seasonCode: elimination.quiz.season.seasonCode}) }}">{{ elimination.quiz.season.name }}</a>
</li>
<li class="breadcrumb-item"><a
href="{{ path('app_backoffice_quiz', {seasonCode: elimination.quiz.season.seasonCode, quiz: elimination.quiz.id}) }}">{{ elimination.quiz.name }}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Prepare Elimination</li>
</ol>
</nav>
</div>
<div class="row">
<div class="col-12 col-md-6">
<form>
{%~ for candidate, colour in elimination.data %}
<div class="row mb-3">
<label for="colour-{{ candidate|lower }}" class="col-4 col-form-label">{{ candidate }}</label>
<div class="col-4">
<select id="colour-{{ candidate|lower }}" class="form-select"
name="colour-{{ candidate|lower }}">
<option value="green"{% if colour == 'green' %} selected{% endif %}>Green</option>
<option value="red"{% if colour == 'red' %} selected{% endif %}>Red</option>
</select>
</div>
</div>
{% endfor %}
<button type="submit" class="btn btn-primary">{{ 'Save'|trans }}</button>
</form>
</div>
<div class="col-12 col-md-6">
<p>Hier kan dus weer wat uitleg komen</p>
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -14,7 +14,7 @@
<div id="questions"> <div id="questions">
<h4 class="py-2">{{ 'Questions'|trans }}</h4> <h4 class="py-2">{{ 'Questions'|trans }}</h4>
<div class="accordion"> <div class="accordion">
{% for question in quiz.questions %} {%~ for question in quiz.questions ~%}
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <h2 class="accordion-header">
<button class="accordion-button collapsed" <button class="accordion-button collapsed"
@@ -23,23 +23,23 @@
data-bs-target="#question-{{ loop.index0 }}" data-bs-target="#question-{{ loop.index0 }}"
aria-controls="question-{{ loop.index0 }}"> aria-controls="question-{{ loop.index0 }}">
{% set questionErrors = question.getErrors %} {% set questionErrors = question.getErrors %}
{% if questionErrors %} {%~ if questionErrors -%}
<span data-bs-toggle="tooltip" <span data-bs-toggle="tooltip"
title="{{ questionErrors }}" title="{{ questionErrors }}"
class="badge text-bg-danger rounded-pill me-2">!</span> class="badge text-bg-danger rounded-pill me-2">!</span>
{% endif %} {% endif %}
{{ loop.index }}. {{ question.question }} {{~ loop.index -}}. {{ question.question -}}
</button> </button>
</h2> </h2>
<div id="question-{{ loop.index0 }}" <div id="question-{{ loop.index0 }}"
class="accordion-collapse collapse"> class="accordion-collapse collapse">
<div class="accordion-body"> <div class="accordion-body">
<ul> <ul>
{% for answer in question.answers %} {%~ for answer in question.answers %}
<li {% if answer.isRightAnswer %}class="text-decoration-underline"{% endif %}>{{ answer.text }}</li> <li{% if answer.isRightAnswer %} class="text-decoration-underline"{% endif %}>{{ answer.text -}}</li>
{% else %} {%~ else %}
{{ 'There are no answers for this question'|trans }} {{ 'There are no answers for this question'|trans -}}
{% endfor %} {%~ endfor %}
</ul> </ul>
</div> </div>
</div> </div>
@@ -50,17 +50,23 @@
</div> </div>
</div> </div>
<div class="scores"> <div class="scores">
<p> <h4 class="py-2">{{ 'Score'|trans }}</h4>
<h4>{{ 'Score'|trans }}</h4>
</p>
<div class="btn-toolbar" role="toolbar"> <div class="btn-toolbar" role="toolbar">
<div class="btn-group btn-group-lg me-2"> <div class="btn-group btn-group-lg me-2">
<a class="btn btn-primary">{{ 'Start Elimination'|trans }}</a> <a class="btn btn-primary">{{ 'Start Elimination'|trans }}</a>
</div>
<div class="btn-group btn-group-lg">
<a href="{{ path('app_prepare_elimination', {seasonCode: season.seasonCode, quiz: quiz.id}) }}" <a href="{{ path('app_prepare_elimination', {seasonCode: season.seasonCode, quiz: quiz.id}) }}"
class="btn btn-secondary">{{ 'Prepare Custom Elimination'|trans }}</a> class="btn btn-secondary">{{ 'Prepare Custom Elimination'|trans }}</a>
<a class="btn btn-secondary">{{ 'Load Prepared Elimination'|trans }}</a> {%~ if not quiz.eliminations.empty %}
<button class="btn btn-secondary dropdown-toggle"
data-bs-toggle="dropdown">{{ 'Load Prepared Elimination'|trans }}</button>
<ul class="dropdown-menu">
{%~ for elimination in quiz.eliminations %}
<li><a class="dropdown-item"
href="{{ path('app_prepare_elimination_view', {elimination: elimination.id}) }}">{{ elimination.created | format_datetime() }}</a>
</li>
{%~ endfor %}
</ul>
{% endif %}
</div> </div>
</div> </div>
<p>{{ 'Number of dropouts:'|trans }} {{ quiz.dropouts }} </p> <p>{{ 'Number of dropouts:'|trans }} {{ quiz.dropouts }} </p>
@@ -75,9 +81,9 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for candidate in result %} {%~ for candidate in result ~%}
<tr class="table-{% if loop.revindex > quiz.dropouts %}success{% else %}danger{% endif %}"> <tr class="table-{% if loop.revindex > quiz.dropouts %}success{% else %}danger{% endif %}">
<td>{{ candidate.0.name }}</td> <td>{{ candidate.name }}</td>
<td>{{ candidate.correct|default('0') }}</td> <td>{{ candidate.correct|default('0') }}</td>
<td>{{ candidate.corrections|default('0') }}</td> <td>{{ candidate.corrections|default('0') }}</td>
<td>{{ candidate.score|default('x') }}</td> <td>{{ candidate.score|default('x') }}</td>
@@ -93,6 +99,7 @@
</div> </div>
{% endblock %} {% endblock %}
{% block javascripts %} {% block javascripts %}
{{ parent() }}
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]') const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')

View File

@@ -3,7 +3,7 @@
{% block body %} {% block body %}
<div class="row"> <div class="row">
<div class="col-md-6 col-12"> <div class="col-md-6 col-12">
<h2 class="py-2">{{ t('Add a quiz to %name%', {'%name%': season.name})|trans }} </h2> <h2 class="py-2">{{ t('Add a quiz to %name%',{'%name%': season.name})|trans }} </h2>
{{ form_start(form) }} {{ form_start(form) }}
{{ form_row(form.name) }} {{ form_row(form.name) }}
{{ form_row(form.sheet) }} {{ form_row(form.sheet) }}

29
templates/base.html.twig Normal file
View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="nl" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script
src="https://js-de.sentry-cdn.com/30cf438bc708c97e6f45c127bed9af96.min.js"
crossorigin="anonymous"
></script>
<title>
{% block title %}Tijd voor de test{% endblock title %}
</title>
{% block stylesheets %}{% endblock %}
{% block javascripts %}{% block importmap %}{% endblock %}{% endblock %}
</head>
<body>
{% block nav %}
{% endblock nav %}
<main>
{% block main %}
<div class="container">
{{ include('flashes.html.twig') }}
{% block body %}
{% endblock body %}
</div>
{% endblock %}
</main>
</body>
</html>

View File

@@ -0,0 +1,13 @@
{% set flashes=app.flashes() %}
{% if flashes is not empty %}
<div class="py-2">
{% for label, messages in flashes %}
{% for message in messages %}
<div class="alert alert-{{ label }} alert-dismissible " role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endfor %}
</div>
{% endif %}

View File

@@ -1,26 +0,0 @@
{% extends 'backoffice/base.html.twig' %}
{% block body %}
<div class="row">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ path('app_backoffice_index') }}">Home</a></li>
<li class="breadcrumb-item"><a
href="{{ path('app_backoffice_season', {seasonCode: season.seasonCode}) }}">{{ season.name }}</a>
</li>
<li class="breadcrumb-item"><a
href="{{ path('app_backoffice_quiz', {seasonCode: season.seasonCode, quiz: quiz.id}) }}">{{ quiz.name }}</a>
</li>
<li class="breadcrumb-item active" aria-current="page">Prepare Elimination</li>
</ol>
</nav>
</div>
<div class="row">
<div class="col-12 col-md-6">
</div>
<div class="col-12 col-md-6">
<p>Hier kan dus weer wat uitleg komen</p>
</div>
</div>
{% endblock %}

View File

@@ -1,55 +1,2 @@
<!DOCTYPE html> {% extends 'base.html.twig' %}
<html lang="nl" data-bs-theme="dark"> {% block importmap %}{{ importmap('quiz') }}{% endblock %}
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<script
src="https://js-de.sentry-cdn.com/30cf438bc708c97e6f45c127bed9af96.min.js"
crossorigin="anonymous"
></script>
<title>
{% block title %}
Tijd voor de test
{% endblock title %}
</title>
<style>
html, body {
height: 100%;
background-image: url("{{ asset('/img/background.png') }}");
background-position: center center;
background-repeat: no-repeat;
background-color: black;
color: white;
display: grid;
align-items: center;
justify-self: center;
}
</style>
</head>
<body>
<main>
<div class="container">
{% for label, messages in app.flashes() %}
{% for message in messages %}
<div class="alert alert-{{ label }} alert-dismissible " role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endfor %}
{% block body %}
{% endblock body %}
</div>
{% block script %}
{% endblock script %}
</main>
</body>
</html>

View File

@@ -17,7 +17,7 @@ Corrections: Jokers
'Download Template': 'Download sjabloon' 'Download Template': 'Download sjabloon'
Email: E-mail Email: E-mail
'Enter your name': 'Voor je naam in' 'Enter your name': 'Voor je naam in'
'Invalid season code': 'Ongeldige seizoenscode' 'Invalid season code': 'Ongeldige seizoencode'
'Load Prepared Elimination': 'Laad voorbereide eliminatie' 'Load Prepared Elimination': 'Laad voorbereide eliminatie'
Logout: Uitloggen Logout: Uitloggen
'Make active': 'Maak actief' 'Make active': 'Maak actief'
@@ -42,14 +42,17 @@ Quiz: Test
Quizzes: Tests Quizzes: Tests
Register: Registreren Register: Registreren
'Remember me': 'Onthoud mij' 'Remember me': 'Onthoud mij'
'Repeat Password': 'Herhaal wachtwoord'
Save: Opslaan
Score: Score Score: Score
Season: Seizoen Season: Seizoen
'Season Code': Seizoenscode 'Season Code': Seizoencode
'Season Name': Seizoensnaam 'Season Name': Seizoennaam
Seasons: Seizoenen Seasons: Seizoenen
'Sign in': 'Log in' 'Sign in': 'Log in'
'Start Elimination': 'Start eliminatie' 'Start Elimination': 'Start eliminatie'
Submit: Verstuur Submit: Verstuur
'The password fields must match.': 'De wachtwoorden moeten overeen komen.'
'There are no answers for this question': 'Er zijn geen antwoorden voor deze vraag' 'There are no answers for this question': 'Er zijn geen antwoorden voor deze vraag'
Time: Tijd Time: Tijd
'Your Seasons': 'Jouw seizoenen' 'Your Seasons': 'Jouw seizoenen'