├── .dockerignore ├── .editorconfig ├── .env ├── .env.panther ├── .env.test ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── blank-issue.md │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ └── ci.yml ├── .gitignore ├── .php_cs.dist ├── .prettierrc.js ├── .stylelintrc.json ├── LICENSE ├── Makefile ├── README.md ├── assets ├── css │ ├── app-organization.scss │ ├── app.scss │ ├── availability-form.scss │ ├── availability-table.scss │ ├── login.scss │ ├── planning.scss │ ├── user.scss │ └── variables.scss ├── img │ ├── login-background-shape.png │ ├── login-background.jpg │ └── user-homepage.jpg └── js │ ├── _delete-item-modal.js │ ├── _helpers.js │ ├── _planning-missions.js │ ├── _planning-update.js │ ├── _planning.js │ ├── _routing.js │ ├── app.js │ ├── asset-type-form.js │ ├── availabilitable-list.js │ ├── availability-form.js │ ├── availability-table.js │ ├── forecast.js │ ├── form-choice-with-other.js │ ├── fos_js_routes.json │ ├── mission-type-form.js │ ├── missions.js │ └── planning.js ├── behat.yml.dist ├── bin ├── console ├── node-tools ├── post-install-dev.sh ├── post-install-test.sh ├── tools └── update-db.sh ├── codecov.yml ├── composer.json ├── composer.lock ├── config ├── bootstrap.php ├── bundles.php ├── packages │ ├── assets.yaml │ ├── cache.yaml │ ├── dev │ │ ├── debug.yaml │ │ ├── hautelook_alice.yaml │ │ ├── nelmio_alice.yaml │ │ └── web_profiler.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── framework.yaml │ ├── misd_phone_number.yaml │ ├── monolog.yaml │ ├── panther │ │ └── config.yaml │ ├── prod │ │ ├── doctrine.yaml │ │ ├── routing.yaml │ │ └── webpack_encore.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── sensio_framework_extra.yaml │ ├── test │ │ ├── dama_doctrine_test_bundle.yaml │ │ ├── framework.yaml │ │ ├── hautelook_alice.yaml │ │ ├── monolog.yaml │ │ ├── nelmio_alice.yaml │ │ ├── twig.yaml │ │ ├── validator.yaml │ │ ├── web_profiler.yaml │ │ └── webpack_encore.yaml │ ├── translation.yaml │ ├── twig.yaml │ ├── validator.yaml │ └── webpack_encore.yaml ├── parameters.yaml ├── routes.yaml ├── routes │ ├── annotations.yaml │ ├── dev │ │ ├── framework.yaml │ │ └── web_profiler.yaml │ └── fos_js_routing.yaml ├── services.yaml ├── services_dev.yaml ├── services_test.yaml └── translations │ └── .gitkeep ├── docker-compose.override.ci.yml.dist ├── docker-compose.override.yml.dist ├── docker-compose.yml ├── docker ├── nginx │ ├── Dockerfile │ └── files │ │ ├── bin │ │ └── entrypoint │ │ └── etc │ │ └── nginx │ │ ├── conf.d │ │ ├── default.conf │ │ └── upstream.conf │ │ └── nginx.conf ├── php-flex │ ├── Dockerfile │ └── files │ │ ├── bin │ │ └── entrypoint │ │ └── usr │ │ └── local │ │ └── etc │ │ ├── php-fpm.conf │ │ └── php │ │ └── php.ini └── traefik │ └── traefik.toml ├── docs ├── CONTRIBUTING.md ├── features-fr.md ├── img │ ├── assets-list.png │ ├── define-availability.png │ ├── edit-account.png │ ├── login.png │ ├── planning-mockup.png │ ├── planning.png │ └── users-list.png ├── technical.md └── usecase-fr.md ├── features ├── organization │ ├── assets.feature │ ├── children.feature │ ├── forecast.feature │ ├── login.feature │ ├── mission_type.feature │ ├── planning.feature │ ├── search.feature │ └── users.feature └── user │ ├── availabilities.feature │ ├── edit.feature │ ├── login.feature │ └── register.feature ├── fixtures ├── assetAvailabilities.yaml ├── assetTypes.yml ├── assets.yaml ├── mission_types.yaml ├── organizations.yaml ├── userAvailabilities.yaml └── users.yaml ├── package.json ├── phpcs.xml.dist ├── phpstan.neon ├── phpunit.xml.dist ├── public ├── favicon.ico ├── index.php ├── ping.php └── robots.txt ├── src ├── Command │ └── LoadOrganizationsCommand.php ├── Controller │ ├── Organization │ │ ├── AbstractOrganizationController.php │ │ ├── AssetType │ │ │ ├── AssetTypeEditController.php │ │ │ └── AssetTypeListController.php │ │ ├── Children │ │ │ ├── EditController.php │ │ │ ├── ListController.php │ │ │ └── NewController.php │ │ ├── CommissionableAsset │ │ │ ├── AssetAddController.php │ │ │ ├── AssetDeleteController.php │ │ │ ├── AssetEditController.php │ │ │ ├── AssetShowModalController.php │ │ │ ├── AssetsListController.php │ │ │ ├── AvailabilityController.php │ │ │ ├── AvailabilityMissionsFindController.php │ │ │ └── PreAddAssetController.php │ │ ├── DashboardController.php │ │ ├── Forecast │ │ │ └── PlanningForecastController.php │ │ ├── IndexController.php │ │ ├── Mission │ │ │ ├── AddUserAjaxController.php │ │ │ ├── MissionController.php │ │ │ ├── MissionModalController.php │ │ │ └── MissionsFindByFiltersController.php │ │ ├── MissionType │ │ │ ├── MissionTypeController.php │ │ │ └── MissionTypeDeleteController.php │ │ ├── Planning │ │ │ ├── PlanningCheckLastUpdateController.php │ │ │ ├── PlanningController.php │ │ │ └── PlanningUpdateController.php │ │ ├── SearchController.php │ │ ├── Security │ │ │ ├── LoginController.php │ │ │ └── LogoutController.php │ │ └── User │ │ │ ├── AddToMissionModalController.php │ │ │ ├── UserDeleteController.php │ │ │ ├── UserEditController.php │ │ │ ├── UserListController.php │ │ │ ├── UserMissionsListController.php │ │ │ └── UserShowModalController.php │ └── User │ │ ├── Account │ │ ├── CreateAccountController.php │ │ ├── EditAccountController.php │ │ └── IndexController.php │ │ ├── Availability │ │ ├── ManageAvailabilityController.php │ │ ├── MissionModalController.php │ │ ├── MissionsFindController.php │ │ └── UserAvailabityControllerTrait.php │ │ └── Security │ │ ├── LoginController.php │ │ └── LogoutController.php ├── DataFixtures │ ├── ApplicationFixtures.php │ ├── Factory │ │ └── AvailabilitableResourceFactory.php │ ├── Faker │ │ └── Provider │ │ │ ├── MissionTypeProvider.php │ │ │ └── UserProvider.php │ ├── SlotAvailabilityGuesser.php │ └── SlotBookingGuesser.php ├── Domain │ ├── AvailabilitiesDomain.php │ ├── AvailabilitiesHelper.php │ ├── AvailabilityDomain.php │ ├── DatePeriodCalculator.php │ ├── MissionDomain.php │ ├── MissionTypeForecastDomain.php │ ├── PlanningDomain.php │ ├── PlanningUpdateDomain.php │ └── SkillSetDomain.php ├── Entity │ ├── AssetType.php │ ├── AvailabilitableInterface.php │ ├── AvailabilitableTrait.php │ ├── AvailabilityInterface.php │ ├── CommissionableAsset.php │ ├── CommissionableAssetAvailability.php │ ├── Mission.php │ ├── MissionType.php │ ├── Organization.php │ ├── User.php │ ├── UserAvailability.php │ ├── UserPasswordInterface.php │ └── UserSerializableInterface.php ├── EntityListener │ ├── AddDependantSkillsEntityListener.php │ └── UserPasswordEntityListener.php ├── EventListener │ ├── CommandLoggerListener.php │ ├── RequestLoggerListener.php │ └── RouteLoggerListener.php ├── Exception │ └── ConstraintViolationListException.php ├── Form │ ├── Factory │ │ └── OrganizationSelectorFormFactory.php │ └── Type │ │ ├── AssetTypePropertyType.php │ │ ├── AssetTypeType.php │ │ ├── AvailabilitiesDomainType.php │ │ ├── AvailabilityDomainType.php │ │ ├── ChoiceWithOtherType.php │ │ ├── CommissionableAssetType.php │ │ ├── DynamicPropertiesType.php │ │ ├── MissionType.php │ │ ├── MissionTypeAssetTypesType.php │ │ ├── MissionTypeType.php │ │ ├── MissionTypeUserSkillsType.php │ │ ├── MissionsSearchType.php │ │ ├── OrganizationEntityType.php │ │ ├── OrganizationSelectorType.php │ │ ├── OrganizationType.php │ │ ├── PlanningDynamicFiltersType.php │ │ ├── PlanningForecastType.php │ │ ├── PlanningSearchType.php │ │ ├── PreAddAssetType.php │ │ ├── UserLoginType.php │ │ └── UserType.php ├── Kernel.php ├── Migrations │ ├── Factory │ │ └── MigrationFactoryDecorator.php │ ├── Version20200321163915.php │ ├── Version20200321175315.php │ ├── Version20200321212921.php │ ├── Version20200321221102.php │ ├── Version20200322115752.php │ ├── Version20200322141349.php │ ├── Version20200322144012.php │ ├── Version20200322162455.php │ ├── Version20200323154736.php │ ├── Version20200324193917.php │ ├── Version20200324213836.php │ ├── Version20200326034332.php │ ├── Version20200330155339.php │ ├── Version20200403195104.php │ ├── Version20200405080155.php │ ├── Version20200411165144.php │ ├── Version20200414132415.php │ ├── Version20200415083630.php │ ├── Version20200415085822.php │ ├── Version20200418152205.php │ ├── Version20200420210639.php │ ├── Version20200420214426.php │ ├── Version20200422103425.php │ ├── Version20200426171023.php │ ├── Version20200427220959.php │ ├── Version20200503083535.php │ └── Version20200512102054.php ├── Monolog │ ├── Handler │ │ └── DockerHandler.php │ └── Processor │ │ ├── ChangeLevelProcessor.php │ │ ├── CommandProcessor.php │ │ ├── ContextNormalizerProcessor.php │ │ ├── ErrorProcessor.php │ │ ├── ExceptionProcessor.php │ │ └── RequestIdProcessor.php ├── ParamConverter │ ├── AssetParamConverter.php │ ├── OrganizationParamConverter.php │ └── UserParamConverter.php ├── Repository │ ├── AssetTypeRepository.php │ ├── AvailabilitableRepositoryInterface.php │ ├── AvailabilityQueryTrait.php │ ├── AvailabilityRepositoryInterface.php │ ├── AvailabilityRepositoryTrait.php │ ├── CommissionableAssetAvailabilityRepository.php │ ├── CommissionableAssetRepository.php │ ├── MissionRepository.php │ ├── MissionTypeRepository.php │ ├── OrganizationRepository.php │ ├── SearchableRepositoryInterface.php │ ├── UserAvailabilityRepository.php │ └── UserRepository.php ├── Security │ ├── OrganizationLoginFormAuthenticator.php │ ├── UserAutomaticLoginHandler.php │ ├── UserLoginFormAuthenticator.php │ └── Voter │ │ ├── CommissionableAssetVoter.php │ │ ├── OrganizationVoter.php │ │ └── UserVoter.php └── Twig │ ├── Cache │ ├── PlanningFilesystemAdapter.php │ └── RequestGenerator.php │ └── Extension │ ├── DynamicPropertyExtension.php │ ├── EntitiesExtension.php │ ├── OrganizationExtension.php │ ├── PlanningExtension.php │ ├── TwigTextExtension.php │ └── UserExtension.php ├── symfony.lock ├── templates ├── _ajax_modal.html.twig ├── _footer.html.twig ├── _navbar.html.twig ├── availability │ └── _table.html.twig ├── base.html.twig ├── misc │ ├── flash-messages.html.twig │ └── form_theme.html.twig ├── organization │ ├── _delete_modal.html.twig │ ├── _help.html.twig │ ├── assetType │ │ ├── edit.html.twig │ │ └── list.html.twig │ ├── base.html.twig │ ├── commissionable_asset │ │ ├── _list.html.twig │ │ ├── _show.html.twig │ │ ├── availability.html.twig │ │ ├── form.html.twig │ │ ├── list.html.twig │ │ ├── preAdd.html.twig │ │ └── show-modal-content.html.twig │ ├── edit.html.twig │ ├── forecast │ │ ├── _results.html.twig │ │ ├── _search_type.html.twig │ │ └── forecast.html.twig │ ├── home.html.twig │ ├── list.html.twig │ ├── login.html.twig │ ├── mission │ │ ├── _form.html.twig │ │ ├── _list.html.twig │ │ ├── _list_full.html.twig │ │ ├── _search_type.html.twig │ │ ├── _show.html.twig │ │ ├── add-to-mission-modal-content.html.twig │ │ ├── edit.html.twig │ │ ├── index.html.twig │ │ ├── list_full.html.twig │ │ ├── new.html.twig │ │ ├── show-modal-content.html.twig │ │ └── show.html.twig │ ├── mission_type │ │ ├── _form.html.twig │ │ ├── edit.html.twig │ │ ├── index.html.twig │ │ └── new.html.twig │ ├── planning │ │ ├── _availabilities_assets.html.twig │ │ ├── _availabilities_base.html.twig │ │ ├── _availabilities_users.html.twig │ │ ├── _results.html.twig │ │ ├── _search_type.html.twig │ │ └── planning.html.twig │ ├── search.html.twig │ └── user │ │ ├── _list.html.twig │ │ ├── _show.html.twig │ │ ├── edit.html.twig │ │ ├── list.html.twig │ │ ├── missions_list.html.twig │ │ └── show-modal-content.html.twig └── user │ ├── _home-calendar.html.twig │ ├── _introduction.html.twig │ ├── _user_form.html.twig │ ├── account-form.html.twig │ ├── availability.html.twig │ ├── index.html.twig │ └── login.html.twig ├── tests ├── Behat │ ├── CoverageContext.php │ ├── DatabaseContext.php │ ├── FixturesContext.php │ ├── OrganizationPlanningContext.php │ ├── SecurityContext.php │ ├── TraversingContext.php │ └── UserPlanningContext.php ├── Domain │ ├── AvailabilitiesDomainTest.php │ ├── DatePeriodCalculatorTest.php │ └── SkillSetDomainTest.php ├── Entity │ ├── OrganizationTest.php │ └── UserTest.php ├── ObjectManager.php ├── Security │ └── Voter │ │ ├── CommissionableAssetVoterTest.php │ │ └── OrganizationVoterTest.php └── Twig │ ├── Cache │ └── RequestGeneratorTest.php │ └── Extension │ └── DynamicPropertyExtensionTest.php ├── translations ├── messages.fr.yaml ├── security.fr.yaml ├── security_organization.fr.yml └── validators.fr.yaml ├── webpack.config.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | **/._* 3 | **/.dockerignore 4 | **/.DS_Store 5 | **/.git/ 6 | **/.idea/ 7 | **/.gitattributes 8 | **/.gitignore 9 | **/.gitmodules 10 | **/Dockerfile* 11 | **/Thumbs.db 12 | **/.cache/ 13 | **/.gradle/ 14 | **/node_modules/ 15 | **/vendor/ 16 | **/dist/ 17 | 18 | /var 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_size = 4 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.neon] 15 | indent_style = tab 16 | 17 | [*.{js,css,scss,json}] 18 | indent_size = 2 19 | 20 | [*.{yaml,yml}] 21 | indent_size = 2 22 | trim_trailing_whitespace = false 23 | 24 | [{.github,config}/**/*.{yaml,yml}] 25 | indent_size = 4 26 | 27 | [.gitmodules] 28 | indent_style = tab 29 | 30 | [.php_cs{,.dist}] 31 | indent_size = 4 32 | 33 | [behat.yml{,.dist}] 34 | indent_size = 4 35 | 36 | [phpstan.neon{,.dist}] 37 | indent_style = tab 38 | indent_size = 4 39 | 40 | [phpunit.xml{,.dist}] 41 | indent_size = 4 42 | 43 | [Makefile] 44 | indent_style = tab 45 | -------------------------------------------------------------------------------- /.env.panther: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgresql://resop:postgrespwd@postgres/resop-test?serverVersion=11&charset=utf8 2 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=disabled 5 | DATABASE_URL=postgresql://resop:postgrespwd@postgres/resop-test?serverVersion=11&charset=utf8 6 | PANTHER_CHROME_ARGUMENTS="--headless --no-sandbox" 7 | PANTHER_APP_ENV=panther 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | extends: [ 4 | 'plugin:prettier/recommended', 5 | 'eslint:recommended', 6 | ], 7 | env: { 8 | amd: true, 9 | browser: true, 10 | }, 11 | rules: { 12 | 'max-len': ['error', { 13 | code: 250, 14 | ignoreUrls: true, 15 | ignoreComments: false, 16 | ignoreRegExpLiterals: true, 17 | ignoreStrings: false, 18 | ignoreTemplateLiterals: false, 19 | }], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/blank-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Blank issue 3 | about: Anything else 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /helpme.log 2 | /docker-compose.override.yml 3 | /.idea 4 | /.cache 5 | /.vscode 6 | /.eslintcache 7 | /.stylelintcache 8 | /coverage.xml 9 | 10 | ###> symfony/framework-bundle ### 11 | /.env.local 12 | /.env.local.php 13 | /.env.*.local 14 | /config/secrets/prod/prod.decrypt.private.php 15 | /config/config.* 16 | /config/translations/* 17 | !config/translations/.gitkeep 18 | /public/bundles/ 19 | /var/ 20 | /vendor/ 21 | ###< symfony/framework-bundle ### 22 | 23 | ###> friendsofphp/php-cs-fixer ### 24 | /.php_cs 25 | /.php_cs.cache 26 | ###< friendsofphp/php-cs-fixer ### 27 | 28 | ###> symfony/phpunit-bridge ### 29 | .phpunit 30 | .phpunit.result.cache 31 | /phpunit.xml 32 | ###< symfony/phpunit-bridge ### 33 | 34 | ###> phpunit/phpunit ### 35 | /phpunit.xml 36 | .phpunit.result.cache 37 | ###< phpunit/phpunit ### 38 | 39 | ###> symfony/webpack-encore-bundle ### 40 | /node_modules/ 41 | /public/build/ 42 | npm-debug.log 43 | yarn-error.log 44 | ###< symfony/webpack-encore-bundle ### 45 | 46 | ###> squizlabs/php_codesniffer ### 47 | /.phpcs-cache 48 | /phpcs.xml 49 | ###< squizlabs/php_codesniffer ### 50 | 51 | ###> friends-of-behat/symfony-extension ### 52 | /behat.yml 53 | ###< friends-of-behat/symfony-extension ### 54 | -------------------------------------------------------------------------------- /.php_cs.dist: -------------------------------------------------------------------------------- 1 | in([__DIR__.'/src', __DIR__.'/tests']) 5 | ->exclude('var') 6 | ; 7 | 8 | return PhpCsFixer\Config::create() 9 | ->setRiskyAllowed(true) 10 | ->setCacheFile(__DIR__.'/.php_cs.cache') 11 | ->setRules([ 12 | '@Symfony' => true, 13 | 'array_syntax' => ['syntax' => 'short'], 14 | 'declare_strict_types' => true, 15 | 'braces' => ['allow_single_line_closure' => true], 16 | 'native_constant_invocation' => true, 17 | 'native_function_invocation' => [ 18 | 'include' => ['@compiler_optimized'], 19 | 'scope' => 'namespaced', 20 | ], 21 | 'phpdoc_summary' => false, 22 | 'no_superfluous_phpdoc_tags' => true, 23 | 'general_phpdoc_annotation_remove' => ['author'], 24 | 'no_unreachable_default_argument_value' => true, 25 | 'no_unused_imports' => true, 26 | 'no_useless_else' => true, 27 | 'no_useless_return' => true, 28 | 'strict_comparison' => true, 29 | 'strict_param' => true, 30 | 'void_return' => true, 31 | ]) 32 | ->setFinder($finder) 33 | ; 34 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 250, 6 | tabWidth: 2 7 | }; 8 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard" 4 | ], 5 | "plugins": [ 6 | "stylelint-scss" 7 | ], 8 | "rules": { 9 | "at-rule-no-unknown": [ 10 | true, 11 | { 12 | "ignoreAtRules": [ 13 | "include", "extend" 14 | ] 15 | } 16 | ], 17 | "scss/at-rule-no-unknown": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /assets/css/app-organization.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/bootstrap"; 2 | 3 | .navbar { 4 | .input-group-search { 5 | background: $light; 6 | 7 | .input-group-prepend { 8 | .input-group-text { 9 | background-color: transparent; 10 | border: none; 11 | } 12 | } 13 | 14 | .form-control { 15 | background-color: transparent; 16 | border: none; 17 | } 18 | 19 | .input-group-append { 20 | background-color: transparent; 21 | 22 | button { 23 | border: none; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /assets/css/availability-form.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | table.availability-form-table { 4 | position: relative; 5 | 6 | label.day-title { 7 | padding: 0; 8 | margin: 0; 9 | cursor: pointer; 10 | } 11 | 12 | td.hour-title { 13 | min-width: 80px; 14 | } 15 | 16 | td.slot-box { 17 | min-width: 85px; 18 | 19 | &:hover:not(.disabled) { 20 | background-color: #eee; 21 | } 22 | 23 | &.checked { 24 | background-color: $colorAvailable; 25 | 26 | &:hover { 27 | background-color: $colorAvailableHover; 28 | } 29 | } 30 | 31 | &.booked { 32 | background-color: $colorBooked !important; 33 | } 34 | 35 | &.mission { 36 | outline: 5px solid $colorMission; 37 | outline-offset: -5px; 38 | } 39 | } 40 | 41 | button.select-all { 42 | position: absolute; 43 | right: 0; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /assets/css/availability-table.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | table.availability-table { 4 | text-align: center; 5 | 6 | td.slot-box { 7 | vertical-align: middle; 8 | cursor: pointer; 9 | 10 | input[type='checkbox'] { 11 | cursor: pointer; 12 | } 13 | 14 | &:hover:not(.disabled):not(.checked) { 15 | opacity: 0.8; 16 | } 17 | 18 | &.disabled { 19 | cursor: default; 20 | } 21 | } 22 | 23 | &.shift-pressed { 24 | td.prev-clicked { 25 | opacity: 1; 26 | box-shadow: inset 0 0 0 4px $red; 27 | } 28 | } 29 | } 30 | 31 | .availability-legend-container, 32 | .planning-legend-container { 33 | margin-bottom: 0; 34 | 35 | .availability-legend { 36 | height: 15px; 37 | width: 30px; 38 | display: inline-block; 39 | margin-right: 15px; 40 | } 41 | 42 | &.availability-legend-container { 43 | .availability-legend.unknown { 44 | background-color: white; 45 | border: 1px solid #eee; 46 | } 47 | } 48 | } 49 | 50 | .planning-actions-container, 51 | .availability-legend-container, 52 | table.availability-table { 53 | .unknown { 54 | background-color: $colorUnknown; 55 | color: white; 56 | } 57 | 58 | .available { 59 | background-color: $colorAvailable; 60 | color: white; 61 | } 62 | 63 | .booked { 64 | background-color: $colorBooked; 65 | color: white; 66 | } 67 | 68 | .engaged { 69 | background-color: $colorMission; 70 | color: $colorMissionText; 71 | 72 | &.btn-outline-light { 73 | background-color: white; 74 | border-color: $colorMission; 75 | color: $colorMissionOutlineText; 76 | } 77 | } 78 | 79 | .locked { 80 | background-color: $colorLocked; 81 | color: white; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /assets/css/variables.scss: -------------------------------------------------------------------------------- 1 | $red: crimson; 2 | 3 | $colorUnknown: #ff767f; 4 | $colorAvailableUser: #24c603; 5 | $colorAvailable: #76bd79; 6 | $colorAvailableHover: #72a870; 7 | $colorBookedUser: #009dcc; 8 | $colorBooked: #67a2d3; 9 | $colorLocked: #7a7a7a; 10 | $colorMission: #f9d948; 11 | $colorMissionUser: #f75c03; 12 | $colorMissionText: #333; 13 | $colorMissionOutlineText: #a7912e; 14 | 15 | $footerColor: #f7f7f7; 16 | 17 | $font-family-base: 'Open Sans', 'Roboto', sans-serif !important; 18 | -------------------------------------------------------------------------------- /assets/img/login-background-shape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crf-devs/resop/cf3a8239a29c40da4d9f0cc603975626d5106077/assets/img/login-background-shape.png -------------------------------------------------------------------------------- /assets/img/login-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crf-devs/resop/cf3a8239a29c40da4d9f0cc603975626d5106077/assets/img/login-background.jpg -------------------------------------------------------------------------------- /assets/img/user-homepage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crf-devs/resop/cf3a8239a29c40da4d9f0cc603975626d5106077/assets/img/user-homepage.jpg -------------------------------------------------------------------------------- /assets/js/_delete-item-modal.js: -------------------------------------------------------------------------------- 1 | const $ = require('jquery'); 2 | require('bootstrap'); 3 | 4 | $(document).ready(function () { 5 | const $modal = $('#delete-item-modal'); 6 | if (!$modal.length) { 7 | return; 8 | } 9 | 10 | $('.trigger-delete').on('click', function (e) { 11 | e.preventDefault(); 12 | const $button = $(this); 13 | 14 | $('[data-role="name"]', $modal).text($button.data('display-name')); 15 | $('[data-role="message"]', $modal).text($button.data('message')); 16 | $('button[data-role="submit"]', $modal).data('url', $button.data('href')); 17 | 18 | $modal.modal('show'); 19 | }); 20 | 21 | $('button[data-role="submit"]', $modal).on('click', function () { 22 | window.location = $(this).data('url'); 23 | $(this).closest('.modal').modal('hide'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /assets/js/_planning.js: -------------------------------------------------------------------------------- 1 | const $ = require('jquery'); 2 | 3 | function removePopovers($planning) { 4 | $planning.find('td[data-toggle="popover"]').popover('dispose').data('toggle', false); 5 | } 6 | 7 | export function addPopovers($planning) { 8 | removePopovers($planning); 9 | $planning.find('td[data-comment], td.mission').each((key, el) => toggleBoxPopover($(el))); 10 | } 11 | 12 | export function toggleBoxPopover($box) { 13 | $box.popover('dispose').data('toggle', false); 14 | 15 | let title = ''; 16 | const texts = []; 17 | 18 | if ($box.data('comment')) { 19 | texts.push('Commentaire ' + $('').text($box.data('comment'))[0].outerHTML); 20 | } 21 | 22 | if ($box.data('mission-text')) { 23 | texts.push($box.data('mission-text')); 24 | } 25 | 26 | if (!texts.length) { 27 | return; 28 | } 29 | 30 | $box 31 | .data('toggle', 'popover') 32 | .popover({ 33 | placement: 'auto', 34 | title: title, 35 | trigger: 'hover', 36 | delay: { show: 200, hide: 100 }, 37 | content: function () { 38 | return texts.join('
'); 39 | }, 40 | html: true, 41 | sanitizeFn: function (content) { 42 | return content; 43 | }, 44 | }) 45 | .on('hide.bs.popover', function () { 46 | // Keep the popover displayed while the mouse is on it 47 | 48 | const $popover = $('.popover:hover'); 49 | 50 | if (!$popover.length) { 51 | return true; 52 | } 53 | 54 | $popover.one('mouseleave', function () { 55 | $popover.popover('hide'); 56 | }); 57 | 58 | return false; 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /assets/js/_routing.js: -------------------------------------------------------------------------------- 1 | import Routing from 'fos_router'; 2 | 3 | const routes = require('./fos_js_routes.json'); 4 | Routing.setRoutingData(routes); 5 | 6 | export default Routing; 7 | -------------------------------------------------------------------------------- /assets/js/asset-type-form.js: -------------------------------------------------------------------------------- 1 | const $ = require('jquery'); 2 | 3 | function addNewWidget($list) { 4 | let counter = $list.data('widget-counter') || $list.children().length; 5 | let newWidget = $list.attr('data-prototype'); 6 | newWidget = newWidget.replace(/__name__/g, counter); 7 | 8 | counter++; 9 | $list.data('widget-counter', counter); 10 | 11 | const $newWidget = $(newWidget); 12 | $newWidget.find('legend').remove(); 13 | $newWidget.find('.hide-on-create').closest('.form-group').hide(); 14 | addWidgetDeleteLink($newWidget); 15 | 16 | $newWidget.appendTo($list); 17 | } 18 | 19 | function addWidgetDeleteLink($item) { 20 | var $removeFormButton = $(''); 21 | $item.append($removeFormButton); 22 | 23 | $removeFormButton.on('click', function () { 24 | $item.remove(); 25 | }); 26 | } 27 | 28 | $(document).ready(function () { 29 | const $assetTypeForm = $('form#edit-asset-type-form'); 30 | let persistedKeys = $assetTypeForm.data('persisted-keys').split(','); 31 | 32 | $('.add-collection-widget').each(function () { 33 | const $list = $($(this).attr('data-list-selector')); 34 | $(this).on('click', () => addNewWidget($list)); 35 | }); 36 | 37 | $('div#asset_type_properties fieldset').each(function () { 38 | const currentKey = $(this).find('input.key-input').val(); 39 | if (persistedKeys.indexOf(currentKey) >= 0) { 40 | $(this).find('.disable-on-edit').prop('disabled', true); 41 | persistedKeys = persistedKeys.filter((value) => value !== currentKey); 42 | } else { 43 | addWidgetDeleteLink($(this)); 44 | } 45 | }); 46 | 47 | $assetTypeForm.on('submit', function () { 48 | $(this).find('.disable-on-edit:disabled').prop('disabled', false); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /assets/js/availabilitable-list.js: -------------------------------------------------------------------------------- 1 | const $ = require('jquery'); 2 | 3 | $(document).ready(function () { 4 | $('form[name="organization_selector"] select').on('change', function () { 5 | window.location = window.location.pathname + '?organizationId=' + $(this).val(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /assets/js/forecast.js: -------------------------------------------------------------------------------- 1 | import { initDatesRange } from './_helpers'; 2 | 3 | const $ = require('jquery'); 4 | 5 | $(document).ready(function () { 6 | initDatesRange($('#availableRange'), $('#availableFrom'), $('#availableTo'), true); 7 | }); 8 | -------------------------------------------------------------------------------- /assets/js/form-choice-with-other.js: -------------------------------------------------------------------------------- 1 | const $ = require('jquery'); 2 | 3 | $(document).ready(function () { 4 | var $selectedChoice = $('.js-choice-with-other .form-check-input:checked'); 5 | var $choiceWithOther = $('.js-choice-with-other'); 6 | if ($selectedChoice.length > 0) { 7 | $choiceWithOther 8 | .find('.form-control') 9 | .parent() 10 | .toggle('-' === $selectedChoice.val()); 11 | } 12 | $('.js-choice-with-other .form-check-input').change(function () { 13 | if ('-' === $(this).val()) { 14 | $choiceWithOther.find('.form-control').parent().show(); 15 | $choiceWithOther.find('.form-control').focus(); 16 | } else { 17 | $choiceWithOther.find('.form-control').val('').parent().hide(); 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /assets/js/fos_js_routes.json: -------------------------------------------------------------------------------- 1 | {"base_url":"","routes":{"app_user_availability_mission_modal":{"tokens":[["text","\/modal"],["variable","\/","\\d+","id",true],["text","\/user\/availability\/missions"]],"defaults":[],"requirements":{"id":"\\d+"},"hosttokens":[],"methods":["GET"],"schemes":[]},"app_organization_mission_modal":{"tokens":[["text","\/modal"],["variable","\/","\\d+","id",true],["text","\/missions"],["variable","\/","\\d+","organization",true],["text","\/organizations"]],"defaults":[],"requirements":{"id":"\\d+","organization":"\\d+"},"hosttokens":[],"methods":["GET"],"schemes":[]},"app_organization_mission_find_by_filters":{"tokens":[["text","\/missions\/find"],["variable","\/","\\d+","organization",true],["text","\/organizations"]],"defaults":[],"requirements":{"organization":"\\d+"},"hosttokens":[],"methods":["GET"],"schemes":[]}},"prefix":"","host":"localhost","port":"","scheme":"http","locale":[]} -------------------------------------------------------------------------------- /assets/js/mission-type-form.js: -------------------------------------------------------------------------------- 1 | const $ = require('jquery'); 2 | 3 | function addNewWidget($list) { 4 | let counter = $list.data('widget-counter') || $list.children().length; 5 | let newWidget = $list.attr('data-prototype'); 6 | newWidget = newWidget.replace(/__name__/g, counter); 7 | 8 | counter++; 9 | $list.data('widget-counter', counter); 10 | 11 | const $newWidget = $(newWidget); 12 | $newWidget.find('legend').remove(); 13 | addWidgetDeleteLink($newWidget); 14 | 15 | $newWidget.appendTo($list); 16 | } 17 | 18 | function addWidgetDeleteLink($item) { 19 | var $removeFormButton = $(''); 20 | $item.append($removeFormButton); 21 | 22 | $removeFormButton.on('click', function () { 23 | $item.remove(); 24 | }); 25 | } 26 | 27 | $(document).ready(function () { 28 | $('.add-collection-widget').each(function () { 29 | const $list = $($(this).attr('data-list-selector')); 30 | 31 | $(this).on('click', function () { 32 | addNewWidget($list); 33 | }); 34 | 35 | $list.children().each(function () { 36 | addWidgetDeleteLink($(this)); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /assets/js/missions.js: -------------------------------------------------------------------------------- 1 | import { initDatesRange } from './_helpers'; 2 | 3 | const $ = require('jquery'); 4 | 5 | $(document).ready(function () { 6 | initDatesRange($('#fromToRange'), $('#mission_startTime'), $('#mission_endTime'), true); 7 | initDatesRange($('#fromTo'), $('#from'), $('#to')); 8 | }); 9 | -------------------------------------------------------------------------------- /behat.yml.dist: -------------------------------------------------------------------------------- 1 | default: 2 | suites: 3 | default: 4 | contexts: 5 | - App\Tests\Behat\CoverageContext 6 | - App\Tests\Behat\DatabaseContext 7 | - App\Tests\Behat\FixturesContext 8 | - App\Tests\Behat\OrganizationPlanningContext 9 | - App\Tests\Behat\SecurityContext 10 | - App\Tests\Behat\TraversingContext 11 | - App\Tests\Behat\UserPlanningContext 12 | - Behat\MinkExtension\Context\MinkContext 13 | - PantherExtension\Context\PantherContext 14 | - PantherExtension\Context\WaitContext: 15 | extensions: 16 | PantherExtension\Extension\PantherExtension: ~ 17 | Behat\MinkExtension: 18 | browser_name: chrome 19 | default_session: symfony 20 | javascript_session: javascript 21 | sessions: 22 | javascript: 23 | panther: 24 | driver: 'chrome' 25 | symfony: 26 | symfony: ~ 27 | FriendsOfBehat\SymfonyExtension: 28 | kernel: 29 | environment: test 30 | debug: true 31 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(['--env', '-e'], null, true)) { 23 | putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); 24 | } 25 | 26 | if ($input->hasParameterOption('--no-debug', true)) { 27 | putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); 28 | } 29 | 30 | require dirname(__DIR__).'/config/bootstrap.php'; 31 | 32 | if ($_SERVER['APP_DEBUG']) { 33 | umask(0000); 34 | 35 | if (class_exists(Debug::class)) { 36 | Debug::enable(); 37 | } 38 | } 39 | 40 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 41 | $application = new Application($kernel); 42 | $application->run($input); 43 | -------------------------------------------------------------------------------- /bin/node-tools: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | BASEDIR="$( cd "$( dirname "$0" )" && pwd )" 4 | DOCKER_COMPOSE=${DOCKER_COMPOSE:-docker-compose} 5 | DISABLE_TTY=${DISABLE_TTY:-0} # When running a php command inside a docker container with tty, the exit code will always be 129. Use DISABLE_TTY=1 . 6 | DOCKER_HOST=${DOCKER_HOST:-/var/run/docker.sock} 7 | 8 | cd $(dirname ${BASEDIR}) 9 | 10 | if [ $# -eq 0 ]; then 11 | set -- "sh" 12 | fi 13 | 14 | if echo ${DOCKER_HOST} | grep unix:///run/user/ > /dev/null 2>&1; then 15 | # Docker rootless 16 | ${DOCKER_COMPOSE} run --rm -e "HOME=/tmp" node "$@" 17 | elif [ -t 1 -a 1 -ne "${DISABLE_TTY}" ]; then 18 | ${DOCKER_COMPOSE} run --rm -u $(id -u):$(id -g) -e "HOME=/tmp" node "$@" 19 | else 20 | # CI and other runners 21 | ${DOCKER_COMPOSE} run --rm -u node:node -T -e "HOME=/tmp" node "$@" 22 | fi 23 | -------------------------------------------------------------------------------- /bin/post-install-dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -ex 3 | 4 | bin/console doctrine:database:create -n --if-not-exists 5 | bin/console doctrine:schema:drop -n -f --full-database || true 6 | 7 | bin/update-db.sh 8 | 9 | bin/console doctrine:fixtures:load -n || true 10 | -------------------------------------------------------------------------------- /bin/post-install-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -ex 3 | 4 | export APP_ENV=test 5 | 6 | bin/console doctrine:database:create -n --if-not-exists 7 | bin/console doctrine:schema:drop -n -f --full-database 8 | bin/console doctrine:migrations:migrate -n 9 | 10 | bin/console hautelook:fixtures:load -n 11 | -------------------------------------------------------------------------------- /bin/tools: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | BASEDIR="$( cd "$( dirname "$0" )" && pwd )" 4 | DOCKER_COMPOSE=${DOCKER_COMPOSE:-docker-compose} 5 | DISABLE_TTY=${DISABLE_TTY:-0} # When running a php command inside a docker container with tty, the exit code will always be 129. Use DISABLE_TTY=1 . 6 | DOCKER_HOST=${DOCKER_HOST:-/var/run/docker.sock} 7 | 8 | cd $(dirname ${BASEDIR}) 9 | 10 | if [ $# -eq 0 ]; then 11 | set -- "sh" 12 | fi 13 | 14 | if echo ${DOCKER_HOST} | grep unix:///run/user/ > /dev/null 2>&1; then 15 | # Docker rootless 16 | ${DOCKER_COMPOSE} run --rm tools "$@" 17 | elif [ -t 1 -a 1 -ne "${DISABLE_TTY}" ]; then 18 | # Common run 19 | ${DOCKER_COMPOSE} run --rm -u $(id -u):$(id -g) tools "$@" 20 | else 21 | # CI and other runners 22 | ${DOCKER_COMPOSE} run --rm -u www-data:www-data -T tools "$@" 23 | fi 24 | -------------------------------------------------------------------------------- /bin/update-db.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -ex 3 | 4 | bin/console doctrine:mapping:info || true 5 | 6 | bin/console doctrine:migrations:migrate -n --allow-no-migration || true 7 | 8 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | threshold: 10% 8 | -------------------------------------------------------------------------------- /config/bootstrap.php: -------------------------------------------------------------------------------- 1 | =1.2) 19 | if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV']) { 20 | foreach ($env as $k => $v) { 21 | $_ENV[$k] = $_ENV[$k] ?? (isset($_SERVER[$k]) && 0 !== strpos($k, 'HTTP_') ? $_SERVER[$k] : $v); 22 | } 23 | } elseif (!class_exists(Dotenv::class)) { 24 | throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); 25 | } else { 26 | // load all the .env files 27 | (new Dotenv())->loadEnv(dirname(__DIR__).'/.env'); 28 | } 29 | 30 | $_SERVER += $_ENV; 31 | $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; 32 | $_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; 33 | $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; 34 | 35 | -------------------------------------------------------------------------------- /config/packages/assets.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | assets: 3 | json_manifest_path: '%kernel.project_dir%/public/build/manifest.json' 4 | -------------------------------------------------------------------------------- /config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | cache.adapter.planning: 3 | class: App\Twig\Cache\PlanningFilesystemAdapter 4 | parent: cache.adapter.filesystem 5 | 6 | framework: 7 | cache: 8 | # Unique name of your app: used to compute stable namespaces for cache keys. 9 | #prefix_seed: your_vendor_name/app_name 10 | 11 | # The "app" cache stores to the filesystem by default. 12 | # The data in this cache should persist between deploys. 13 | # Other options include: 14 | 15 | # Redis 16 | #app: cache.adapter.redis 17 | #default_redis_provider: redis://localhost 18 | 19 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 20 | app: cache.adapter.apcu 21 | 22 | # Namespaced pools use the above "app" backend by default 23 | pools: 24 | cache.twig: 25 | adapter: cache.adapter.planning 26 | -------------------------------------------------------------------------------- /config/packages/dev/debug.yaml: -------------------------------------------------------------------------------- 1 | debug: 2 | # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. 3 | # See the "server:dump" command to start a new server. 4 | dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" 5 | -------------------------------------------------------------------------------- /config/packages/dev/hautelook_alice.yaml: -------------------------------------------------------------------------------- 1 | hautelook_alice: 2 | fixtures_path: fixtures 3 | -------------------------------------------------------------------------------- /config/packages/dev/nelmio_alice.yaml: -------------------------------------------------------------------------------- 1 | nelmio_alice: 2 | functions_blacklist: 3 | - 'current' 4 | - 'shuffle' 5 | - 'date' 6 | - 'time' 7 | - 'file' 8 | - 'md5' 9 | - 'sha1' 10 | -------------------------------------------------------------------------------- /config/packages/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: true 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { only_exceptions: false } 7 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | url: '%env(resolve:DATABASE_URL)%' 4 | server_version: '11' 5 | types: 6 | text[]: MartinGeorgiev\Doctrine\DBAL\Types\TextArray 7 | # Some other types are available in MartinGeorgiev\Doctrine 8 | mapping_types: 9 | text[]: text[] 10 | _text: text[] 11 | orm: 12 | auto_generate_proxy_classes: true 13 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware 14 | auto_mapping: true 15 | dql: 16 | string_functions: 17 | ALL_OF: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\All 18 | ANY_OF: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Any 19 | IN_ARRAY: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\InArray 20 | CONTAINS: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Contains 21 | ARRAY: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Arr 22 | JSON_GET_FIELD_AS_TEXT: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsText 23 | mappings: 24 | App: 25 | is_bundle: false 26 | type: annotation 27 | dir: '%kernel.project_dir%/src/Entity' 28 | prefix: 'App\Entity' 29 | alias: App 30 | -------------------------------------------------------------------------------- /config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | migrations_paths: 3 | 'DoctrineMigrations': 'src/Migrations' 4 | 5 | services: 6 | 'Doctrine\Migrations\Version\MigrationFactory': 'App\Migrations\Factory\MigrationFactoryDecorator' 7 | 8 | storage: 9 | table_storage: 10 | table_name: 'migration_versions' 11 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: '%env(APP_SECRET)%' 3 | #csrf_protection: true 4 | #http_method_override: true 5 | 6 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 7 | # Remove or comment this section to explicitly disable session support. 8 | session: 9 | handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler 10 | cookie_secure: auto 11 | cookie_samesite: lax 12 | 13 | #esi: true 14 | #fragments: true 15 | php_errors: 16 | log: true 17 | -------------------------------------------------------------------------------- /config/packages/misd_phone_number.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | types: 4 | phone_number: Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType 5 | -------------------------------------------------------------------------------- /config/packages/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | channels: ["php_error"] 3 | handlers: 4 | main: 5 | type: service 6 | id: App\Monolog\Handler\DockerHandler 7 | channels: ["!event", "!doctrine"] 8 | -------------------------------------------------------------------------------- /config/packages/panther/config.yaml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: ../test/* } 3 | 4 | framework: 5 | session: 6 | storage_id: session.storage.native 7 | handler_id: 'file://%kernel.project_dir%/var/sessions' 8 | profiler: { collect: false } 9 | -------------------------------------------------------------------------------- /config/packages/prod/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | orm: 3 | auto_generate_proxy_classes: false 4 | metadata_cache_driver: 5 | type: pool 6 | pool: doctrine.system_cache_pool 7 | query_cache_driver: 8 | type: pool 9 | pool: doctrine.system_cache_pool 10 | result_cache_driver: 11 | type: pool 12 | pool: doctrine.result_cache_pool 13 | 14 | framework: 15 | cache: 16 | pools: 17 | doctrine.result_cache_pool: 18 | adapter: cache.app 19 | doctrine.system_cache_pool: 20 | adapter: cache.system 21 | -------------------------------------------------------------------------------- /config/packages/prod/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: null 4 | -------------------------------------------------------------------------------- /config/packages/prod/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | #webpack_encore: 2 | # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) 3 | # Available in version 1.2 4 | #cache: true 5 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | utf8: true 4 | -------------------------------------------------------------------------------- /config/packages/sensio_framework_extra.yaml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | router: 3 | annotations: false 4 | -------------------------------------------------------------------------------- /config/packages/test/dama_doctrine_test_bundle.yaml: -------------------------------------------------------------------------------- 1 | dama_doctrine_test: 2 | enable_static_connection: true 3 | enable_static_meta_data_cache: true 4 | enable_static_query_cache: true 5 | -------------------------------------------------------------------------------- /config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_id: session.storage.mock_file 5 | handler_id: null 6 | -------------------------------------------------------------------------------- /config/packages/test/hautelook_alice.yaml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: ../dev/hautelook_alice.yaml } 3 | -------------------------------------------------------------------------------- /config/packages/test/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_http_codes: [404, 405] 8 | channels: ["!event"] 9 | nested: 10 | type: stream 11 | path: "%kernel.logs_dir%/%kernel.environment%.log" 12 | level: debug 13 | -------------------------------------------------------------------------------- /config/packages/test/nelmio_alice.yaml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: ../dev/nelmio_alice.yaml } 3 | -------------------------------------------------------------------------------- /config/packages/test/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | strict_variables: true 3 | -------------------------------------------------------------------------------- /config/packages/test/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | not_compromised_password: false 4 | -------------------------------------------------------------------------------- /config/packages/test/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: false 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { collect: false } 7 | -------------------------------------------------------------------------------- /config/packages/test/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | #webpack_encore: 2 | # strict_mode: false 3 | -------------------------------------------------------------------------------- /config/packages/translation.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | default_locale: '%app.locale%' 3 | translator: 4 | default_path: '%kernel.project_dir%/config/translations' 5 | fallbacks: 6 | - '%app.locale%' 7 | paths: 8 | - '%kernel.project_dir%/translations' 9 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | form_themes: ['bootstrap_4_layout.html.twig', 'misc/form_theme.html.twig'] 4 | globals: 5 | build_tag: '%env(IMAGE_BUILD_TAG)%' 6 | slot_interval: '%app.slot_interval%' 7 | user_properties: '%app.user_properties%' 8 | -------------------------------------------------------------------------------- /config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | email_validation_mode: html5 4 | 5 | # Enables validator auto-mapping support. 6 | # For instance, basic validation constraints will be inferred from Doctrine's metadata. 7 | #auto_mapping: 8 | # App\Entity\: [] 9 | -------------------------------------------------------------------------------- /config/packages/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | webpack_encore: 2 | # The path where Encore is building the assets - i.e. Encore.setOutputPath() 3 | output_path: '%kernel.project_dir%/public/build' 4 | # If multiple builds are defined (as shown below), you can disable the default build: 5 | # output_path: false 6 | 7 | # if using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials') 8 | # crossorigin: 'anonymous' 9 | 10 | # preload all rendered script and link tags automatically via the http2 Link header 11 | # preload: true 12 | 13 | # Throw an exception if the entrypoints.json file is missing or an entry is missing from the data 14 | # strict_mode: false 15 | 16 | # if you have multiple builds: 17 | # builds: 18 | # pass "frontend" as the 3rg arg to the Twig functions 19 | # {{ encore_entry_script_tags('entry1', null, 'frontend') }} 20 | 21 | # frontend: '%kernel.project_dir%/public/frontend/build' 22 | 23 | # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) 24 | # Put in config/packages/prod/webpack_encore.yaml 25 | # cache: true 26 | -------------------------------------------------------------------------------- /config/routes/annotations.yaml: -------------------------------------------------------------------------------- 1 | controllers: 2 | resource: ../../src/Controller/ 3 | type: annotation 4 | -------------------------------------------------------------------------------- /config/routes/dev/framework.yaml: -------------------------------------------------------------------------------- 1 | _errors: 2 | resource: '@FrameworkBundle/Resources/config/routing/errors.xml' 3 | prefix: /_error 4 | -------------------------------------------------------------------------------- /config/routes/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler_wdt: 2 | resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' 3 | prefix: /_wdt 4 | 5 | web_profiler_profiler: 6 | resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' 7 | prefix: /_profiler 8 | -------------------------------------------------------------------------------- /config/routes/fos_js_routing.yaml: -------------------------------------------------------------------------------- 1 | fos_js_routing: 2 | resource: "@FOSJsRoutingBundle/Resources/config/routing/routing-sf4.xml" 3 | -------------------------------------------------------------------------------- /config/services_dev.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | # Comment the 2 lines bellow if you want to test the planning twig cache in dev env 3 | Twig\CacheExtension\CacheStrategy\BlackholeCacheStrategy: ~ 4 | Twig\CacheExtension\CacheStrategyInterface: '@Twig\CacheExtension\CacheStrategy\BlackholeCacheStrategy' 5 | -------------------------------------------------------------------------------- /config/services_test.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autowire: true 4 | autoconfigure: true 5 | 6 | App\Tests\Behat\: 7 | resource: '../tests/Behat/*' 8 | 9 | App\Tests\Behat\FixturesContext: 10 | $aliceFixturesLoader: '@hautelook_alice.loader' 11 | 12 | App\Tests\Behat\TraversingContext: 13 | $projectDir: '%kernel.project_dir%' 14 | -------------------------------------------------------------------------------- /config/translations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crf-devs/resop/cf3a8239a29c40da4d9f0cc603975626d5106077/config/translations/.gitkeep -------------------------------------------------------------------------------- /docker-compose.override.ci.yml.dist: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | postgres: 5 | tmpfs: 6 | - /var/lib/postgresql/data:rw 7 | 8 | nginx: 9 | build: 10 | cache_from: 11 | - ${CI_REGISTRY_IMAGE}/nginx:latest 12 | 13 | fpm: 14 | build: 15 | cache_from: 16 | - ${CI_REGISTRY_IMAGE}/fpm:latest 17 | - ${CI_REGISTRY_IMAGE}/php-flex-dev:latest 18 | 19 | tools: 20 | build: 21 | cache_from: 22 | - ${CI_REGISTRY_IMAGE}/php-flex-dev:latest 23 | environment: 24 | - APP_ENV=test 25 | -------------------------------------------------------------------------------- /docker-compose.override.yml.dist: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | volumes: 4 | postgres-data: ~ 5 | 6 | services: 7 | 8 | # traefik: 9 | # ports: 10 | # - '80:80' # Uncomment to use the 80 port on your host instead of the 7500 one. 11 | # volumes: 12 | # - /run/user/1000/docker.sock:/var/run/docker.sock # Uncomment for docker rootless 13 | 14 | # blackfire: 15 | # image: blackfire/blackfire 16 | # environment: 17 | # - BLACKFIRE_SERVER_ID=none 18 | # - BLACKFIRE_SERVER_TOKEN=none 19 | 20 | # fpm: # Uncomment for MacOS 21 | # volumes: # Uncomment for MacOS 22 | # - ./:/srv:cached # Uncomment for MacOS 23 | # environment: 24 | # - BLACKFIRE_CLIENT_ID=none 25 | # - BLACKFIRE_CLIENT_TOKEN=none 26 | 27 | # tools: # Uncomment for MacOS 28 | # volumes: # Uncomment for MacOS 29 | # - ./:/srv:cached # Uncomment for MacOS 30 | 31 | # node: # Uncomment for MacOS 32 | # volumes: # Uncomment for MacOS 33 | # - ./:/srv:cached # Uncomment for MacOS 34 | # - ./.cache/node:/tmp/.cache/ # Uncomment if you want to cache yarn tmp files 35 | 36 | postgres: 37 | volumes: 38 | - postgres-data:/var/lib/postgresql/data:rw 39 | # ports: 40 | # - '5432:5432' # Uncomment if you need to access the DB from your host 41 | -------------------------------------------------------------------------------- /docker/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.9 AS withoutsources 2 | 3 | RUN apk add --update --no-cache \ 4 | nginx \ 5 | curl \ 6 | && rm -rf /var/cache/apk/* && rm -rf /tmp/* 7 | 8 | COPY ./docker/nginx/files/. / 9 | 10 | RUN echo "daemon off;" >> /etc/nginx/nginx.conf 11 | 12 | ENV FPM_ENDPOINT "fpm:9000" 13 | 14 | WORKDIR /srv 15 | 16 | ENTRYPOINT ["/bin/entrypoint"] 17 | CMD ["nginx"] 18 | 19 | EXPOSE 80 20 | 21 | HEALTHCHECK --interval=5s --timeout=5s --start-period=5s --retries=3 CMD curl -s http://0.0.0.0/health 1>/dev/null || exit 1 22 | STOPSIGNAL SIGQUIT 23 | 24 | # ================================================ 25 | 26 | FROM withoutsources AS withsources 27 | 28 | COPY ./public/. /srv/public 29 | 30 | CMD ["nginx"] 31 | -------------------------------------------------------------------------------- /docker/nginx/files/bin/entrypoint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -e 3 | 4 | if [ -n "${FPM_ENDPOINT}" ]; then 5 | echo "Using ${FPM_ENDPOINT} as php-fpm endpoint" 6 | echo "upstream php-upstream { server ${FPM_ENDPOINT}; }" > /etc/nginx/conf.d/upstream.conf 7 | fi 8 | 9 | nginx -t || true 10 | 11 | exec "$@" 12 | -------------------------------------------------------------------------------- /docker/nginx/files/etc/nginx/conf.d/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen [::]:80 default_server ipv6only=on; 4 | 5 | server_name _; 6 | root /srv/public; 7 | 8 | location / { 9 | try_files $uri /index.php$is_args$args; 10 | } 11 | 12 | location ~* ping\.php$ { 13 | access_log off; 14 | error_log off; 15 | } 16 | 17 | location ~ \.php$ { 18 | fastcgi_pass php-upstream; 19 | fastcgi_split_path_info ^(.+\.php)(/.*)$; 20 | include fastcgi_params; 21 | add_header X-Request-Id $reqid; 22 | fastcgi_param HTTP_X_REQUEST_ID $reqid; 23 | fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; 24 | fastcgi_param DOCUMENT_ROOT $realpath_root; 25 | fastcgi_param HTTPS off; 26 | fastcgi_pass_request_headers on; 27 | } 28 | 29 | location /health { 30 | access_log off; 31 | error_log off; 32 | return 200 'ok'; 33 | add_header Content-Type text/plain; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docker/nginx/files/etc/nginx/conf.d/upstream.conf: -------------------------------------------------------------------------------- 1 | upstream php-upstream { server fpm:9000; } 2 | -------------------------------------------------------------------------------- /docker/php-flex/files/usr/local/etc/php-fpm.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | pid = /var/run/php-fpm.pid 3 | error_log = /dev/stderr 4 | log_limit = 10240 5 | log_buffering = yes 6 | process_control_timeout = 15s 7 | 8 | [symfony] 9 | user = www-data 10 | group = www-data 11 | 12 | listen = 0.0.0.0:9000 13 | 14 | pm = dynamic 15 | pm.max_children = 100 16 | pm.start_servers = 3 17 | pm.min_spare_servers = 2 18 | pm.max_spare_servers = 20 19 | pm.max_requests = 1000 20 | 21 | ;access.log = /dev/stderr 22 | 23 | clear_env = no 24 | 25 | catch_workers_output = yes 26 | decorate_workers_output = no 27 | 28 | php_flag[display_errors] = on 29 | php_admin_value[error_log] = /dev/stderr 30 | php_admin_flag[log_errors] = on 31 | -------------------------------------------------------------------------------- /docker/php-flex/files/usr/local/etc/php/php.ini: -------------------------------------------------------------------------------- 1 | memory_limit = 2G 2 | log_errors = On 3 | expose_php = Off 4 | 5 | apc.enable_cli = 1 6 | date.timezone = UTC 7 | session.auto_start = Off 8 | short_open_tag = Off 9 | 10 | ;opcache.preload=/srv/var/cache/prod/srcApp_KernelProdContainer.preload.php # TODO We need https://github.com/pull/34757 to be in the used release 11 | ;opcache.validate_timestamps=0 # TODO Add a apc_clear_cache() or opcache_reset() call via the web server in order to flush cache 12 | ;opcache.file_cache='/tmp/opcache' # TODO Remove when using preload 13 | opcache.preload_user=www-data 14 | opcache.enable_cli=1 15 | opcache.interned_strings_buffer = 16 16 | opcache.max_accelerated_files = 20000 17 | opcache.memory_consumption = 256 18 | opcache.file_update_protection=0 19 | 20 | realpath_cache_size = 4096K 21 | realpath_cache_ttl = 600 22 | -------------------------------------------------------------------------------- /docker/traefik/traefik.toml: -------------------------------------------------------------------------------- 1 | debug = false 2 | logLevel = "INFO" 3 | 4 | defaultEntryPoints = ["http", "https", "dashboard"] 5 | 6 | [api] 7 | entryPoint = "dashboard" 8 | dashboard = true 9 | debug = true 10 | 11 | [entryPoints] 12 | [entryPoints.dashboard] 13 | address = ":9000" 14 | [entryPoints.http] 15 | address = ":80" 16 | [entryPoints.https] 17 | address = ":443" 18 | [entryPoints.https.tls] 19 | [[entryPoints.https.tls.certificates]] 20 | certFile = "/etc/ssl/local.crt" 21 | keyFile = "/etc/ssl/local.key" 22 | 23 | [docker] 24 | domain = "vcap.me" 25 | watch = true 26 | exposedbydefault = false 27 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## A good first issue ? 4 | 5 | You can easily start with any [`good first issue` tagged issue](https://github.com/crf-devs/resop/labels/good%20first%20issue)! 6 | 7 | ## Workflow 8 | 9 | We are using a standard github workflow in this project: 10 | 11 | 1. [Find an issue](https://github.com/crf-devs/resop/issues) you want to work on, and discuss about it on Github or on Discord 12 | 1. [Create a fork](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) of the repository 13 | 1. [Create your new branch](https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging) 14 | 1. Work on it :-) 15 | 1. Run `make fix-cs` in order to clean you code 16 | 1. Run `make test` and fix everything if any 17 | 1. [Push your branch](https://git-scm.com/docs/git-push) 18 | 1. [Create your pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request), 19 | you can add `Closes #xxx` in its description in order to link it with an existing issue 20 | 1. While you are still working on you PR, you can add a `WIP: ` prefix to its name 21 | -------------------------------------------------------------------------------- /docs/img/assets-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crf-devs/resop/cf3a8239a29c40da4d9f0cc603975626d5106077/docs/img/assets-list.png -------------------------------------------------------------------------------- /docs/img/define-availability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crf-devs/resop/cf3a8239a29c40da4d9f0cc603975626d5106077/docs/img/define-availability.png -------------------------------------------------------------------------------- /docs/img/edit-account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crf-devs/resop/cf3a8239a29c40da4d9f0cc603975626d5106077/docs/img/edit-account.png -------------------------------------------------------------------------------- /docs/img/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crf-devs/resop/cf3a8239a29c40da4d9f0cc603975626d5106077/docs/img/login.png -------------------------------------------------------------------------------- /docs/img/planning-mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crf-devs/resop/cf3a8239a29c40da4d9f0cc603975626d5106077/docs/img/planning-mockup.png -------------------------------------------------------------------------------- /docs/img/planning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crf-devs/resop/cf3a8239a29c40da4d9f0cc603975626d5106077/docs/img/planning.png -------------------------------------------------------------------------------- /docs/img/users-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crf-devs/resop/cf3a8239a29c40da4d9f0cc603975626d5106077/docs/img/users-list.png -------------------------------------------------------------------------------- /docs/usecase-fr.md: -------------------------------------------------------------------------------- 1 | 2 | # Illustration du projet 3 | 4 | ## COVID-19, la Croix-Rouge Française à Paris utilise ResOp pour organiser sa réponse à la crise sanitaire 5 | 6 | Dans le cadre de la pandémie du Covid-19, la Croix-Rouge Française se mobilise aux côtés des pouvoirs publics 7 | pour apporter son soutien aux personnes malades et auprès des plus fragiles. 8 | 9 | A Paris, la Croix-Rouge s’organise autour de 18 structures locales (les Unités Locales ou UL) regroupant un total 10 | plus de X bénévoles et véhicules, chacun avec des compétences et caractéristiques propres. 11 | 12 | Pour mettre en place rapidement et de manière globale sa réponse à la crise, la Croix-Rouge Française de Paris utilise 13 | ResOp qui donne la visibilité nécessaire sur les équipes de bénévoles et les ressources opérationnelles pouvant être 14 | mobilisées au sein de l’ensemble des Unités Locales. 15 | 16 | A l’heure actuelle ce sont plus de 700 bénévoles aux compétences diverses qui renseignent leurs disponibilités dans l’outil. 17 | ResOp rend ainsi possible l’affectation quotidienne de plus de 120 intervenants sur des missions de secours 18 | (renfort SAMU, trains médicalisés…) ou de soutien à la population (Croix-Rouge chez vous, maraudes…). 19 | -------------------------------------------------------------------------------- /features/organization/login.feature: -------------------------------------------------------------------------------- 1 | @login 2 | Feature: 3 | In order to manage my actions, 4 | As an organization, 5 | I must be able to log in. 6 | 7 | Scenario: As anonymous, I cannot access to the homepage 8 | When I go to "/organizations" 9 | Then I should be on "/organizations/login" 10 | 11 | Scenario Outline: As a registered organization, I can log in 12 | Given I am on "/organizations/login" 13 | When I select "" from "identifier" 14 | And I fill in "password" with "covid19" 15 | And I press "Je me connecte" 16 | Then the response status code should be 200 17 | And I should be on "/organizations/" 18 | And I should see "" 19 | Examples: 20 | | identifier | name | id | 21 | | DT75 | DT75 | 201 | 22 | | UL 01-02 | DT75 - UL 01-02 | 203 | 23 | 24 | Scenario: As an authenticated organization, I can log out 25 | Given I am authenticated as "UL 01-02" 26 | And I am on "/organizations/" 27 | When I follow "Déconnexion" 28 | Then I should be on "/organizations/login" 29 | -------------------------------------------------------------------------------- /fixtures/assetAvailabilities.yaml: -------------------------------------------------------------------------------- 1 | App\Entity\CommissionableAssetAvailability: 2 | # VPSP 75012 is locked tuesday next week from 6:00 to 9:59 3 | CommissionableAssetAvailability.vpsp_75012.next-week.tuesday.{6..8,2}: 4 | __factory: 5 | '@App\DataFixtures\Factory\AvailabilitableResourceFactory::create': 6 | - '<{app.slot_interval}>' 7 | - '@Asset.UL-01-02.VPSP.2' 8 | - 'tuesday next week :00' 9 | - 'locked' 10 | 11 | # VPSP 75012 is available tuesday next week from 10:00 to 13:59 12 | CommissionableAssetAvailability.vpsp_75012.next-week.tuesday.{10..12,2}: 13 | __factory: 14 | '@App\DataFixtures\Factory\AvailabilitableResourceFactory::create': 15 | - '<{app.slot_interval}>' 16 | - '@Asset.UL-01-02.VPSP.2' 17 | - 'tuesday next week :00' 18 | - 'available' 19 | 20 | # VPSP 75012 is booked tuesday next week from 14:00 to 17:59 21 | CommissionableAssetAvailability.vpsp_75012.next-week.tuesday.{14..16,2}: 22 | __factory: 23 | '@App\DataFixtures\Factory\AvailabilitableResourceFactory::create': 24 | - '<{app.slot_interval}>' 25 | - '@Asset.UL-01-02.VPSP.2' 26 | - 'tuesday next week :00' 27 | - 'booked' 28 | -------------------------------------------------------------------------------- /fixtures/assetTypes.yml: -------------------------------------------------------------------------------- 1 | App\Entity\AssetType: 2 | AssetType.DT75.VL: 3 | id: 301 4 | organization: '@Organization.DT75' 5 | name: VL 6 | AssetType.DT75.VPSP: 7 | id: 302 8 | organization: '@Organization.DT75' 9 | name: VPSP 10 | AssetType.DT77.VL: 11 | id: 303 12 | organization: '@Organization.DT77' 13 | name: VL 14 | AssetType.DT77.VPSP: 15 | id: 304 16 | organization: '@Organization.DT77' 17 | name: VPSP 18 | -------------------------------------------------------------------------------- /fixtures/assets.yaml: -------------------------------------------------------------------------------- 1 | App\Entity\CommissionableAsset: 2 | Asset.DT75.VPSP.{2..4,2}: 3 | id: 7599 4 | organization: '@Organization.DT75' 5 | assetType: '@AssetType.DT75.VPSP' 6 | name: '7599' 7 | Asset.DT75.VL.{6..8,2}: 8 | id: 7599 9 | organization: '@Organization.DT75' 10 | assetType: '@AssetType.DT75.VL' 11 | name: '7599' 12 | Asset.DT77.VPSP.{2..4,2}: 13 | id: 7799 14 | organization: '@Organization.DT77' 15 | assetType: '@AssetType.DT77.VPSP' 16 | name: '7799' 17 | Asset.DT77.VL.{6..8,2}: 18 | id: 7799 19 | organization: '@Organization.DT77' 20 | assetType: '@AssetType.DT77.VL' 21 | name: '7799' 22 | Asset.UL-01-02.VPSP.{2..4,2}: 23 | id: 7501 24 | organization: '@Organization.UL-01-02' 25 | assetType: '@AssetType.DT75.VPSP' 26 | name: '7501' 27 | Asset.UL-01-02.VL.{6..8,2}: 28 | id: 7501 29 | organization: '@Organization.UL-01-02' 30 | assetType: '@AssetType.DT75.VL' 31 | name: '7501' 32 | Asset.UL-DE-BRIE-ET-CHANTEREINE.VPSP.{2..4,2}: 33 | id: 7710 34 | organization: '@Organization.UL-DE-BRIE-ET-CHANTEREINE' 35 | assetType: '@AssetType.DT77.VPSP' 36 | name: '7710' 37 | Asset.UL-DE-BRIE-ET-CHANTEREINE.VL.{6..8,2}: 38 | id: 7710 39 | organization: '@Organization.UL-DE-BRIE-ET-CHANTEREINE' 40 | assetType: '@AssetType.DT77.VL' 41 | name: '7710' 42 | -------------------------------------------------------------------------------- /fixtures/mission_types.yaml: -------------------------------------------------------------------------------- 1 | App\Entity\MissionType: 2 | MissionType.DT75.{1..2}: 3 | id: 75 4 | name: Mission type DT75 5 | organization: '@Organization.DT75' 6 | minimumAvailableHours: 2 7 | userSkillsRequirement: [{"skill":"psc1","number":3},{"skill":"infirmier","number":2}] 8 | assetTypesRequirement: 9 | MissionType.DT77.{1..2}: 10 | id: 77 11 | name: Mission type DT77 12 | organization: '@Organization.DT77' 13 | minimumAvailableHours: 4 14 | userSkillsRequirement: 15 | assetTypesRequirement: 16 | -------------------------------------------------------------------------------- /fixtures/organizations.yaml: -------------------------------------------------------------------------------- 1 | App\Entity\Organization: 2 | Organization.DT75: 3 | id: 201 4 | name: DT75 5 | plainPassword: covid19 6 | Organization.DT77: 7 | id: 202 8 | name: DT77 9 | plainPassword: covid19 10 | Organization.UL-01-02: 11 | id: 203 12 | name: UL 01-02 13 | plainPassword: covid19 14 | parent: '@Organization.DT75' 15 | Organization.UL-DE-BRIE-ET-CHANTEREINE: 16 | id: 204 17 | name: UL DE BRIE ET CHANTEREINE 18 | plainPassword: covid19 19 | parent: '@Organization.DT77' 20 | -------------------------------------------------------------------------------- /fixtures/users.yaml: -------------------------------------------------------------------------------- 1 | App\Entity\User: 2 | User.john_doe: 3 | id: 101 4 | firstName: John 5 | lastName: DOE 6 | organization: '@Organization.DT75' 7 | identificationNumber: 990001A 8 | emailAddress (unique): john.doe@resop.com 9 | phoneNumber: '' 10 | birthday: '1990-01-01' 11 | skillSet: '' 12 | properties: {"occupation": "Pharmacien", "organizationOccupation": "Secouriste", "vulnerable": true, "fullyEquipped": true, "drivingLicence": true} 13 | User.jane_doe: 14 | id: 102 15 | firstName: Jane 16 | lastName: DOE 17 | organization: '@Organization.UL-01-02' 18 | identificationNumber: 990002A 19 | emailAddress (unique): jane.doe@resop.com 20 | phoneNumber: '' 21 | birthday: '1990-01-01' 22 | skillSet: '' 23 | properties: {"occupation": "", "organizationOccupation": "Secouriste", "vulnerable": , "fullyEquipped": , "drivingLicence": } 24 | User.chuck_norris: 25 | id: 103 26 | firstName: Chuck 27 | lastName: NORRIS 28 | organization: '@Organization.UL-DE-BRIE-ET-CHANTEREINE' 29 | identificationNumber: 990003A 30 | emailAddress (unique): chuck.norris@resop.com 31 | phoneNumber: '' 32 | birthday: '1990-01-01' 33 | skillSet: '' 34 | properties: {"occupation": "", "organizationOccupation": "Secouriste", "vulnerable": , "fullyEquipped": , "drivingLicence": } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@symfony/webpack-encore": "^0.31.0", 4 | "FOSJsRoutingBundle": "FriendsOfSymfony/FOSJsRoutingBundle#2.5.4", 5 | "babel-eslint": "^10.1.0", 6 | "bootstrap": "^4.4.1", 7 | "bootstrap-select": "^1.13.12", 8 | "browser-update": "^3.3.16", 9 | "core-js": "^3.0.0", 10 | "daterangepicker": "^3.0.5", 11 | "eslint": "^7.12.1", 12 | "eslint-config-prettier": "^6.10.1", 13 | "eslint-plugin-prettier": "^3.1.2", 14 | "font-awesome": "^4.7.0", 15 | "jquery": "^3.5.1", 16 | "moment": "2.29.1", 17 | "node-sass": "^5.0.0", 18 | "popper.js": "^1.16.1", 19 | "prettier": "^2.0.2", 20 | "regenerator-runtime": "^0.13.2", 21 | "sass-loader": "^10.0.5", 22 | "stylelint": "^13.2.1", 23 | "stylelint-config-standard": "^20.0.0", 24 | "stylelint-scss": "^3.16.0", 25 | "webpack-notifier": "^1.6.0" 26 | }, 27 | "resolutions": { 28 | "moment": "2.24.0" 29 | }, 30 | "license": "GPL-3.0", 31 | "private": false, 32 | "scripts": { 33 | "dev-server": "encore dev-server", 34 | "dev": "encore dev", 35 | "watch": "encore dev --watch", 36 | "build": "encore production --progress", 37 | "lint": "npx eslint 'assets/js/**/*.js' --cache --max-warnings=0", 38 | "lint:fix": "npx eslint 'assets/js/**/*.js' --cache --max-warnings=0 --fix", 39 | "lint:css": "npx stylelint 'assets/css/**/*.{css,scss}' --cache --max-warnings=0", 40 | "lint:css:fix": "npx stylelint 'assets/css/**/*.{css,scss}' --cache --max-warnings=0 --fix" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | src/ 24 | tests/ 25 | 26 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/phpstan/phpstan-symfony/extension.neon 3 | - vendor/phpstan/phpstan-doctrine/extension.neon 4 | 5 | parameters: 6 | tmpDir: var/cache/phpstan 7 | excludes_analyse: 8 | - config/ 9 | - src/Migrations/ 10 | - public/ 11 | - features/ 12 | - var/ 13 | - node_modules/ 14 | - vendor/ 15 | symfony: 16 | container_xml_path: %rootDir%/../../../var/cache/test/App_KernelTestDebugContainer.xml 17 | doctrine: 18 | repositoryClass: Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository 19 | objectManagerLoader: tests/ObjectManager.php 20 | inferPrivatePropertyTypeFromConstructor: true 21 | checkMissingIterableValueType: false 22 | checkGenericClassInNonGenericObjectType: false 23 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | src 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | tests 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crf-devs/resop/cf3a8239a29c40da4d9f0cc603975626d5106077/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handle($request); 28 | $response->send(); 29 | $kernel->terminate($request, $response); 30 | -------------------------------------------------------------------------------- /public/ping.php: -------------------------------------------------------------------------------- 1 | getUser(); 21 | $organization = $parameters['organization'] ?? null; 22 | $parameters = array_merge($parameters, ['organization' => $user->getId()]); 23 | if (null !== $organization && $user->getId() !== $organization) { 24 | $parameters['organizationId'] = $organization; 25 | } 26 | } 27 | 28 | return parent::generateUrl($route, $parameters, $referenceType); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Controller/Organization/AssetType/AssetTypeListController.php: -------------------------------------------------------------------------------- 1 | assetTypeRepository = $assetTypeRepository; 25 | } 26 | 27 | public function __invoke(): Response 28 | { 29 | /** @var Organization $organization */ 30 | $organization = $this->getUser(); 31 | 32 | return $this->render('organization/assetType/list.html.twig', [ 33 | 'assetTypes' => $this->assetTypeRepository->findByOrganization($organization), 34 | ]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Controller/Organization/Children/EditController.php: -------------------------------------------------------------------------------- 1 | }/edit", name="app_organization_edit", methods={"GET", "POST"}) 18 | * @IsGranted(OrganizationVoter::CAN_MANAGE, subject="object") 19 | */ 20 | class EditController extends AbstractOrganizationController 21 | { 22 | public function __invoke(Request $request, Organization $object): Response 23 | { 24 | $form = $this->createForm(OrganizationType::class, $object); 25 | $form->handleRequest($request); 26 | 27 | if ($form->isSubmitted() && $form->isValid()) { 28 | $flashMessage = 'La structure a été mise à jour avec succès.'; 29 | 30 | $entityManager = $this->getDoctrine()->getManager(); 31 | $entityManager->persist($object); 32 | $entityManager->flush(); 33 | 34 | $this->addFlash('success', $flashMessage); 35 | 36 | return $this->redirectToRoute('app_organization_list'); 37 | } 38 | 39 | return $this->render( 40 | 'organization/edit.html.twig', 41 | [ 42 | 'organization' => $object, 43 | 'form' => $form->createView(), 44 | ] 45 | )->setStatusCode($form->isSubmitted() ? Response::HTTP_BAD_REQUEST : Response::HTTP_OK); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Controller/Organization/Children/ListController.php: -------------------------------------------------------------------------------- 1 | organizationRepository = $organizationRepository; 24 | } 25 | 26 | public function __invoke(): Response 27 | { 28 | $organization = $this->getUser(); 29 | if (!$organization instanceof Organization || !$organization->isParent()) { 30 | throw new AccessDeniedException(); 31 | } 32 | 33 | $organizations = $this->organizationRepository->findBy(['parent' => $organization], ['name' => 'ASC']); 34 | 35 | return $this->render('organization/list.html.twig', [ 36 | 'organizations' => $organizations, 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Controller/Organization/CommissionableAsset/AssetShowModalController.php: -------------------------------------------------------------------------------- 1 | }/modal", name="app_organization_asset_show_modal", methods={"GET", "POST"}) 14 | */ 15 | class AssetShowModalController extends AbstractController 16 | { 17 | public function __invoke(CommissionableAsset $asset): Response 18 | { 19 | return $this->render('organization/commissionable_asset/show-modal-content.html.twig', ['asset' => $asset]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Controller/Organization/CommissionableAsset/PreAddAssetController.php: -------------------------------------------------------------------------------- 1 | createForm( 24 | PreAddAssetType::class, 25 | ['organizationId' => $organization->getId()], 26 | ['organization' => $organization] 27 | )->createView(); 28 | 29 | return $this->render('organization/commissionable_asset/preAdd.html.twig', [ 30 | 'form' => $form, 31 | 'organization' => $organization, 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Controller/Organization/DashboardController.php: -------------------------------------------------------------------------------- 1 | }", name="app_organization_dashboard", methods={"GET"}) 15 | */ 16 | final class DashboardController extends AbstractController 17 | { 18 | public function __invoke(): Response 19 | { 20 | return $this->render('organization/home.html.twig'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Controller/Organization/Forecast/PlanningForecastController.php: -------------------------------------------------------------------------------- 1 | planningDomain = $planningDomain; 28 | $this->missionTypeForecastDomain = $missionTypeForecastDomain; 29 | } 30 | 31 | public function __invoke(Request $request): Response 32 | { 33 | $form = $this->planningDomain->generateForm(PlanningForecastType::class); 34 | $filters = $this->planningDomain->generateFilters($form); 35 | 36 | return $this->render('organization/forecast/forecast.html.twig', [ 37 | 'filters' => $filters, 38 | 'form' => $form->createView(), 39 | 'forecast' => $this->missionTypeForecastDomain->calculatePerMissionTypes($filters), 40 | ]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Controller/Organization/IndexController.php: -------------------------------------------------------------------------------- 1 | redirectToRoute('app_organization_dashboard'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Controller/Organization/Mission/AddUserAjaxController.php: -------------------------------------------------------------------------------- 1 | }/users/add/{userToAdd<\d+>}", name="app_organization_mission_add_user", methods={"POST"}) 16 | */ 17 | class AddUserAjaxController extends AbstractController 18 | { 19 | public function __invoke(Mission $mission, User $userToAdd): Response 20 | { 21 | if (!$mission->users->contains($userToAdd)) { 22 | $mission->users->add($userToAdd); 23 | } 24 | 25 | $this->getDoctrine()->getManager()->flush(); 26 | 27 | return new JsonResponse(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Controller/Organization/Mission/MissionModalController.php: -------------------------------------------------------------------------------- 1 | }/modal", name="app_organization_mission_modal", methods={"GET"}, options={"expose"=true}) 14 | */ 15 | class MissionModalController extends AbstractController 16 | { 17 | public function __invoke(Mission $mission): Response 18 | { 19 | return $this->render('organization/mission/show-modal-content.html.twig', [ 20 | 'mission' => $mission, 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Controller/Organization/Mission/MissionsFindByFiltersController.php: -------------------------------------------------------------------------------- 1 | planningDomain = $planningDomain; 25 | $this->missionRepository = $missionRepository; 26 | $this->serializer = $serializer; 27 | } 28 | 29 | public function __invoke(): JsonResponse 30 | { 31 | $form = $this->planningDomain->generateForm(); 32 | $filters = $this->planningDomain->generateFilters($form); 33 | 34 | $data = $this->missionRepository->findByPlanningFilters($filters, $this->planningDomain->getAvailableResources($filters, true)); 35 | 36 | // TODO Paginate 37 | return new JsonResponse($this->serializer->serialize($data, 'json', ['groups' => ['mission:ajax']]), 200, [], true); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Controller/Organization/MissionType/MissionTypeDeleteController.php: -------------------------------------------------------------------------------- 1 | translator = $translator; 27 | $this->entityManager = $entityManager; 28 | } 29 | 30 | public function __invoke(MissionType $missionType): RedirectResponse 31 | { 32 | $this->entityManager->remove($missionType); 33 | $this->entityManager->flush(); 34 | 35 | $this->addFlash('success', 'organization.missionType.deleteSuccessMessage'); 36 | 37 | return $this->redirectToRoute('app_organization_mission_type_index'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Controller/Organization/Planning/PlanningCheckLastUpdateController.php: -------------------------------------------------------------------------------- 1 | planningDomain = $planningDomain; 22 | } 23 | 24 | public function __invoke(Request $request): JsonResponse 25 | { 26 | $form = $this->planningDomain->generateForm(); 27 | $filters = $this->planningDomain->generateFilters($form); 28 | 29 | return new JsonResponse($this->planningDomain->generateLastUpdateAndCount($filters)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Controller/Organization/SearchController.php: -------------------------------------------------------------------------------- 1 | }/search", name="app_organization_search", methods={"GET"}, requirements={"id"="\d+"}) 19 | */ 20 | final class SearchController extends AbstractOrganizationController 21 | { 22 | public function __invoke(Request $request, UserRepository $userRepository, CommissionableAssetRepository $commissionableAssetRepository, OrganizationRepository $organizationRepository): Response 23 | { 24 | /** @var Organization $organization */ 25 | $organization = $this->getUser(); 26 | 27 | /** @var string $query */ 28 | $query = preg_replace('/\s+/', ' ', trim((string) $request->query->get('query'))); 29 | if (empty($query)) { 30 | return $this->redirectToRoute('app_organization_dashboard'); 31 | } 32 | 33 | return $this->render('organization/search.html.twig', [ 34 | 'query' => $query, 35 | 'users' => $userRepository->search($organization, $query), 36 | 'assets' => $commissionableAssetRepository->search($organization, $query), 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Controller/Organization/Security/LogoutController.php: -------------------------------------------------------------------------------- 1 | }/missions/add/modal", name="app_organization_user_add_to_mission_modal", methods={"GET"}) 17 | */ 18 | class AddToMissionModalController extends AbstractController 19 | { 20 | private PlanningDomain $planningDomain; 21 | private MissionRepository $missionRepository; 22 | 23 | public function __construct(PlanningDomain $planningDomain, MissionRepository $missionRepository) 24 | { 25 | $this->planningDomain = $planningDomain; 26 | $this->missionRepository = $missionRepository; 27 | } 28 | 29 | public function __invoke(User $userToAdd): Response 30 | { 31 | $form = $this->planningDomain->generateForm(MissionsSearchType::class); 32 | $filters = $form->getData(); 33 | 34 | return $this->render('organization/mission/add-to-mission-modal-content.html.twig', [ 35 | 'userToAdd' => $userToAdd, 36 | 'filters' => $filters, 37 | 'form' => $form->createView(), 38 | 'missions' => $this->missionRepository->findByFilters($filters), 39 | ]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Controller/Organization/User/UserDeleteController.php: -------------------------------------------------------------------------------- 1 | }/delete", name="app_organization_user_delete", methods={"GET"}) 18 | * @IsGranted(UserVoter::CAN_EDIT, subject="userToDelete") 19 | */ 20 | class UserDeleteController extends AbstractOrganizationController 21 | { 22 | private UserAvailabilityRepository $userAvailabilityRepository; 23 | 24 | public function __construct(UserAvailabilityRepository $userAvailabilityRepository) 25 | { 26 | $this->userAvailabilityRepository = $userAvailabilityRepository; 27 | } 28 | 29 | public function __invoke(EntityManagerInterface $entityManager, User $userToDelete): RedirectResponse 30 | { 31 | $entityManager->beginTransaction(); 32 | $this->userAvailabilityRepository->deleteByOwner($userToDelete); 33 | $entityManager->remove($userToDelete); 34 | $entityManager->flush(); 35 | $entityManager->commit(); 36 | 37 | $this->addFlash('success', 'Le bénévole a été supprimé avec succès.'); 38 | 39 | return $this->redirectToRoute('app_organization_user_list', ['organization' => $userToDelete->getNotNullOrganization()->id]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Controller/Organization/User/UserEditController.php: -------------------------------------------------------------------------------- 1 | }/edit", name="app_organization_user_edit", methods={"GET", "POST"}) 18 | * @IsGranted(UserVoter::CAN_EDIT, subject="userToEdit") 19 | */ 20 | class UserEditController extends AbstractOrganizationController 21 | { 22 | public function __invoke(Request $request, User $userToEdit): Response 23 | { 24 | $form = $this 25 | ->createForm(UserType::class, $userToEdit, ['display_type' => UserType::DISPLAY_ORGANIZATION]) 26 | ->handleRequest($request); 27 | 28 | if ($form->isSubmitted() && $form->isValid()) { 29 | $entityManager = $this->getDoctrine()->getManager(); 30 | $entityManager->persist($userToEdit); 31 | $entityManager->flush(); 32 | 33 | $this->addFlash('success', 'Les informations ont été mises à jour avec succès.'); 34 | 35 | return $this->redirectToRoute('app_organization_user_list', ['organization' => $userToEdit->getNotNullOrganization()->id]); 36 | } 37 | 38 | return $this->render('organization/user/edit.html.twig', [ 39 | 'user' => $userToEdit, 40 | 'form' => $form->createView(), 41 | ])->setStatusCode($form->isSubmitted() ? Response::HTTP_BAD_REQUEST : Response::HTTP_OK); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Controller/Organization/User/UserMissionsListController.php: -------------------------------------------------------------------------------- 1 | }/missions", name="app_organization_user_missions_list", methods={"GET"}) 14 | */ 15 | class UserMissionsListController extends AbstractController 16 | { 17 | public function __invoke(User $item): Response 18 | { 19 | return $this->render('organization/user/missions_list.html.twig', [ 20 | 'user' => $item, 21 | 'missions' => $item->missions, 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Controller/Organization/User/UserShowModalController.php: -------------------------------------------------------------------------------- 1 | }/modal", name="app_organization_user_show_modal", methods={"GET"}) 14 | */ 15 | class UserShowModalController extends AbstractController 16 | { 17 | public function __invoke(User $userToShow): Response 18 | { 19 | return $this->render('organization/user/show-modal-content.html.twig', [ 20 | 'user' => $userToShow, 21 | ]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Controller/User/Availability/MissionModalController.php: -------------------------------------------------------------------------------- 1 | }/modal", name="app_user_availability_mission_modal", methods={"GET"}, options={"expose"=true}) 15 | * @Security("mission.users.contains(user)") 16 | */ 17 | class MissionModalController extends AbstractController 18 | { 19 | public function __invoke(Mission $mission): Response 20 | { 21 | return $this->render('organization/mission/show-modal-content.html.twig', [ 22 | 'mission' => $mission, 23 | 'showLinks' => false, 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Controller/User/Availability/MissionsFindController.php: -------------------------------------------------------------------------------- 1 | ?}/missions", name="app_user_availability_missions_week", methods={"GET"}) 18 | */ 19 | class MissionsFindController extends AbstractController 20 | { 21 | use UserAvailabityControllerTrait; 22 | 23 | private MissionRepository $missionRepository; 24 | private SerializerInterface $serializer; 25 | 26 | public function __construct(MissionRepository $missionRepository, SerializerInterface $serializer) 27 | { 28 | $this->missionRepository = $missionRepository; 29 | $this->serializer = $serializer; 30 | } 31 | 32 | public function __invoke(Request $request): JsonResponse 33 | { 34 | $user = $this->getUser(); 35 | if (!$user instanceof User) { 36 | throw $this->createAccessDeniedException(); 37 | } 38 | 39 | [$start, $end] = $this->getDatesByWeek($request->attributes->get('week')); 40 | 41 | $data = $this->missionRepository->findByPlanningFilters(['from' => $start, 'to' => $end], [[(int) $user->getId()], []]); 42 | 43 | return new JsonResponse($this->serializer->serialize($data, 'json', ['groups' => ['mission:ajax']]), 200, [], true); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Controller/User/Availability/UserAvailabityControllerTrait.php: -------------------------------------------------------------------------------- 1 | add(new \DateInterval('P7D')); 20 | 21 | return [$start, $end]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Controller/User/Security/LogoutController.php: -------------------------------------------------------------------------------- 1 | add(\DateInterval::createFromDateString($slotInterval)); 22 | 23 | if ($resource instanceof User) { 24 | return new UserAvailability(null, $resource, $startDate, $endTime, $status); 25 | } 26 | 27 | if ($resource instanceof CommissionableAsset) { 28 | return new CommissionableAssetAvailability(null, $resource, $startDate, $endTime, $status); 29 | } 30 | 31 | throw new \LogicException(sprintf('Not handled resource of type "%s"', \get_class($resource))); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/DataFixtures/Faker/Provider/MissionTypeProvider.php: -------------------------------------------------------------------------------- 1 | skillSetDomain = $skillSetDomain; 18 | } 19 | 20 | public function randomSkillRequirement(): array 21 | { 22 | return array_map( 23 | static function (string $skill) { 24 | return [ 25 | 'skill' => $skill, 26 | 'number' => Faker::numberBetween(1, 5), 27 | ]; 28 | }, 29 | Faker::randomElements($this->skillSetDomain->getSkillSetKeys(), random_int(1, 3)) 30 | ); 31 | } 32 | 33 | public function randomAssetTypeRequirement(AssetType ...$assetTypes): array 34 | { 35 | return array_map( 36 | static function (AssetType $assetType) { 37 | return [ 38 | 'type' => $assetType->id, 39 | 'number' => Faker::numberBetween(1, 5), 40 | ]; 41 | }, 42 | Faker::randomElements($assetTypes, random_int(1, 2)) 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/DataFixtures/Faker/Provider/UserProvider.php: -------------------------------------------------------------------------------- 1 | skillSetDomain = $skillSetDomain; 20 | $this->phoneNumberUtil = $phoneNumberUtil; 21 | } 22 | 23 | public function randomSkillSet(): array 24 | { 25 | return Faker::randomElements($this->skillSetDomain->getSkillSetKeys(), random_int(1, 3)); 26 | } 27 | 28 | public function phoneNumberObject(string $phoneNumber, ?string $defaultRegion = 'FR'): PhoneNumber 29 | { 30 | return $this->phoneNumberUtil->parse($phoneNumber, $defaultRegion); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Entity/AvailabilitableInterface.php: -------------------------------------------------------------------------------- 1 | initialize($id, $startTime, $endTime, $status); 35 | $this->asset = $asset; 36 | } 37 | 38 | public function getOwner(): AvailabilitableInterface 39 | { 40 | return $this->asset; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Entity/UserAvailability.php: -------------------------------------------------------------------------------- 1 | initialize($id, $startTime, $endTime, $status); 35 | $this->user = $user; 36 | } 37 | 38 | public function getOwner(): AvailabilitableInterface 39 | { 40 | return $this->user; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Entity/UserPasswordInterface.php: -------------------------------------------------------------------------------- 1 | skillSetDomain = $skillSetDomain; 17 | } 18 | 19 | public function prePersist(User $user): void 20 | { 21 | $user->skillSet = $this->skillSetDomain->getIncludedSkillsFromSkillSet($user->skillSet); 22 | } 23 | 24 | public function preUpdate(User $user): void 25 | { 26 | $this->prePersist($user); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/EntityListener/UserPasswordEntityListener.php: -------------------------------------------------------------------------------- 1 | encoder = $encoder; 17 | } 18 | 19 | public function prePersist(UserPasswordInterface $user): void 20 | { 21 | if (!$plainPassword = $user->getPlainPassword()) { 22 | return; 23 | } 24 | 25 | $user->setPassword($this->encoder->encodePassword($user, $plainPassword)); 26 | $user->eraseCredentials(); 27 | } 28 | 29 | public function preUpdate(UserPasswordInterface $user): void 30 | { 31 | $this->prePersist($user); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/EventListener/RouteLoggerListener.php: -------------------------------------------------------------------------------- 1 | 'onKernelResponse', 23 | ]; 24 | } 25 | 26 | public function onKernelResponse(ResponseEvent $event): void 27 | { 28 | $event->getResponse()->headers->set(self::ROUTE_HEADER, $event->getRequest()->attributes->get('_route', 'null')); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Exception/ConstraintViolationListException.php: -------------------------------------------------------------------------------- 1 | violations = $violations; 16 | parent::__construct('Validation error'); 17 | } 18 | 19 | public function __toString(): string 20 | { 21 | $message = ''; 22 | foreach ($this->violations as $violation) { 23 | if ('' !== $message) { 24 | $message .= "\n"; 25 | } 26 | if ($propertyPath = $violation->getPropertyPath()) { 27 | $message .= "$propertyPath: "; 28 | } 29 | 30 | $message .= $violation->getMessage(); 31 | } 32 | 33 | return $message; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Form/Factory/OrganizationSelectorFormFactory.php: -------------------------------------------------------------------------------- 1 | formFactory = $formFactory; 19 | } 20 | 21 | public function createForm(Organization $organization, Organization $loggedOrganization): FormInterface 22 | { 23 | return $this->formFactory->create( 24 | OrganizationSelectorType::class, 25 | ['organization' => $organization], 26 | ['currentOrganization' => $loggedOrganization] 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Form/Type/AssetTypeType.php: -------------------------------------------------------------------------------- 1 | add('name', null, ['label' => 'Nom']) 20 | ->add('properties', CollectionType::class, [ 21 | 'entry_type' => AssetTypePropertyType::class, 22 | 'allow_add' => true, 23 | 'allow_delete' => true, 24 | 'label' => 'organization.asset_type.properties.main_title', 25 | ]) 26 | ->add('submit', SubmitType::class) 27 | ; 28 | } 29 | 30 | public function configureOptions(OptionsResolver $resolver): void 31 | { 32 | $resolver->setDefaults([ 33 | 'error_bubbling' => true, 34 | 'data_class' => AssetType::class, 35 | ]); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Form/Type/AvailabilitiesDomainType.php: -------------------------------------------------------------------------------- 1 | add('availabilityDomains', CollectionType::class, [ 20 | 'entry_type' => AvailabilityDomainType::class, 21 | ]) 22 | ->add('submit', SubmitType::class) 23 | ; 24 | } 25 | 26 | public function configureOptions(OptionsResolver $resolver): void 27 | { 28 | $resolver 29 | ->setDefaults([ 30 | 'data_class' => AvailabilitiesDomain::class, 31 | ]) 32 | ; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Form/Type/OrganizationEntityType.php: -------------------------------------------------------------------------------- 1 | setDefaults([ 19 | 'class' => Organization::class, 20 | 'choice_label' => 'name', 21 | 'attr' => [ 22 | 'class' => 'selectpicker show-tick', 23 | 'data-live-search' => 'true', 24 | 'title' => 'user.selectOrganization', 25 | ], 26 | 'group_by' => 'parentName', 27 | 'query_builder' => static function (OrganizationRepository $repository): QueryBuilder { 28 | return $repository->createActiveOrganizationQueryBuilder(); 29 | }, 30 | ]); 31 | } 32 | 33 | public function getParent(): ?string 34 | { 35 | return EntityType::class; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Form/Type/OrganizationSelectorType.php: -------------------------------------------------------------------------------- 1 | organizationRepository = $organizationRepository; 21 | } 22 | 23 | /** 24 | * @param array{currentOrganization: Organization, route_to_redirect: string} $options 25 | */ 26 | public function buildForm(FormBuilderInterface $builder, array $options): void 27 | { 28 | if (!$options['currentOrganization']->isParent()) { 29 | return; 30 | } 31 | 32 | $builder 33 | ->add( 34 | 'organization', 35 | EntityType::class, 36 | [ 37 | 'class' => Organization::class, 38 | 'label' => 'organization.childrenSelector.label', 39 | 'query_builder' => $this->organizationRepository->findByParentQueryBuilder($options['currentOrganization']), 40 | ] 41 | ); 42 | } 43 | 44 | public function configureOptions(OptionsResolver $resolver): void 45 | { 46 | $resolver->setRequired(['currentOrganization']); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Form/Type/OrganizationType.php: -------------------------------------------------------------------------------- 1 | add('name', TextType::class, [ 20 | 'label' => 'organization.name', 21 | ]) 22 | ->add('submit', SubmitType::class, [ 23 | 'label' => 'action.submit', 24 | ]); 25 | } 26 | 27 | public function configureOptions(OptionsResolver $resolver): void 28 | { 29 | $resolver->setDefaults([ 30 | 'data_class' => Organization::class, 31 | ]); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Form/Type/PlanningDynamicFiltersType.php: -------------------------------------------------------------------------------- 1 | add( 26 | $property['key'], 27 | ChoiceType::class, 28 | [ 29 | 'required' => false, 30 | 'label' => $property['columnLabel'] ?? $property['label'], 31 | 'placeholder' => 'organization.planning.booleanFilterPlaceholder', 32 | 'choices' => [ 33 | 'common.no' => 0, 34 | 'common.yes' => 1, 35 | ], 36 | 'attr' => ['class' => 'dynamic_planning_filter ml-2'], 37 | 'row_attr' => ['class' => 'form-inline mb-2'], 38 | ] 39 | ); 40 | } 41 | } 42 | 43 | public function configureOptions(OptionsResolver $resolver): void 44 | { 45 | $resolver 46 | ->setDefaults(['config' => []]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Form/Type/PreAddAssetType.php: -------------------------------------------------------------------------------- 1 | getParentOrganization(); 22 | 23 | $builder 24 | ->add('type', EntityType::class, [ 25 | 'required' => true, 26 | 'class' => AssetType::class, 27 | 'choice_name' => 'name', 28 | 'query_builder' => fn (AssetTypeRepository $assetTypeRepository) => $assetTypeRepository->findByOrganizationQB($parentOrganization), 29 | ]) 30 | ->add('submit', SubmitType::class, ['label' => 'Continuer']) 31 | ->add('organizationId', HiddenType::class) 32 | ; 33 | } 34 | 35 | public function getBlockPrefix(): string 36 | { 37 | return ''; 38 | } 39 | 40 | public function configureOptions(OptionsResolver $resolver): void 41 | { 42 | $resolver 43 | ->setRequired(['organization']) 44 | ->addAllowedTypes('organization', Organization::class) 45 | ->setDefaults([ 46 | 'csrf_protection' => false, 47 | ]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Form/Type/UserLoginType.php: -------------------------------------------------------------------------------- 1 | add('identifier', TextType::class, [ 19 | 'label' => 'user.login', 20 | ]) 21 | ->add('birthday', BirthdayType::class, [ 22 | 'format' => 'dd MMMM yyyy', 23 | 'input' => 'string', 24 | 'label' => 'user.dob', 25 | ]); 26 | } 27 | 28 | public function configureOptions(OptionsResolver $resolver): void 29 | { 30 | $resolver->setDefaults([ 31 | 'csrf_token_id' => 'authenticate', 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Migrations/Factory/MigrationFactoryDecorator.php: -------------------------------------------------------------------------------- 1 | migrationFactory = $migrationFactory; 20 | $this->container = $container; 21 | } 22 | 23 | public function createVersion(string $migrationClassName): AbstractMigration 24 | { 25 | $instance = $this->migrationFactory->createVersion($migrationClassName); 26 | 27 | if ($instance instanceof ContainerAwareInterface) { 28 | $instance->setContainer($this->container); 29 | } 30 | 31 | return $instance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Migrations/Version20200321175315.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 15 | 16 | $this->addSql('ALTER TABLE organization ADD parent_id INT DEFAULT NULL'); 17 | $this->addSql('ALTER TABLE organization ADD CONSTRAINT FK_C1EE637C727ACA70 FOREIGN KEY (parent_id) REFERENCES organization (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); 18 | $this->addSql('CREATE INDEX IDX_C1EE637C727ACA70 ON organization (parent_id)'); 19 | } 20 | 21 | public function down(Schema $schema): void 22 | { 23 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 24 | 25 | $this->addSql('ALTER TABLE organization DROP CONSTRAINT FK_C1EE637C727ACA70'); 26 | $this->addSql('DROP INDEX IDX_C1EE637C727ACA70'); 27 | $this->addSql('ALTER TABLE organization DROP parent_id'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Migrations/Version20200322115752.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 20 | 21 | $this->addSql('ALTER TABLE users ADD birthday VARCHAR(255) NOT NULL'); 22 | } 23 | 24 | public function down(Schema $schema): void 25 | { 26 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 27 | 28 | $this->addSql('ALTER TABLE users DROP birthday'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Migrations/Version20200322141349.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 15 | 16 | $this->addSql('ALTER TABLE organization ADD password VARCHAR(255) DEFAULT NULL'); 17 | } 18 | 19 | public function down(Schema $schema): void 20 | { 21 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 22 | 23 | $this->addSql('ALTER TABLE organization DROP password'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Migrations/Version20200322144012.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 15 | 16 | $this->addSql('CREATE UNIQUE INDEX organisation_name_unique ON organization (name)'); 17 | } 18 | 19 | public function down(Schema $schema): void 20 | { 21 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 22 | 23 | $this->addSql('DROP INDEX organisation_name_unique'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Migrations/Version20200322162455.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 15 | 16 | $this->addSql('ALTER TABLE users DROP skill_set'); 17 | $this->addSql('ALTER TABLE users ADD skill_set text[] DEFAULT NULL'); 18 | $this->addSql('CREATE INDEX user_skill_set_idx ON users (skill_set)'); 19 | $this->addSql('CREATE INDEX user_vulnerable_idx ON users (vulnerable)'); 20 | $this->addSql('CREATE INDEX user_fully_equipped_idx ON users (fully_equipped)'); 21 | } 22 | 23 | public function down(Schema $schema): void 24 | { 25 | $this->throwIrreversibleMigrationException(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Migrations/Version20200324193917.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 20 | 21 | $this->addSql('ALTER TABLE commissionable_asset DROP last_commission_date'); 22 | $this->addSql('CREATE INDEX commissionable_asset_type_idx ON commissionable_asset (type)'); 23 | } 24 | 25 | public function down(Schema $schema): void 26 | { 27 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 28 | 29 | $this->addSql('DROP INDEX commissionable_asset_type_idx'); 30 | $this->addSql('ALTER TABLE commissionable_asset ADD last_commission_date TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL'); 31 | $this->addSql('COMMENT ON COLUMN commissionable_asset.last_commission_date IS \'(DC2Type:datetimetz_immutable)\''); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Migrations/Version20200324213836.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 20 | 21 | $this->addSql('CREATE INDEX commissionable_asset_name_idx ON commissionable_asset (name)'); 22 | $this->addSql('CREATE INDEX user_firstname_idx ON users (first_name)'); 23 | $this->addSql('CREATE INDEX user_lastname_idx ON users (last_name)'); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 29 | 30 | $this->addSql('DROP INDEX commissionable_asset_name_idx'); 31 | $this->addSql('DROP INDEX user_firstname_idx'); 32 | $this->addSql('DROP INDEX user_lastname_idx'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Migrations/Version20200330155339.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 15 | 16 | $this->addSql('ALTER TABLE commissionable_asset ALTER has_mobile_radio SET NOT NULL'); 17 | } 18 | 19 | public function down(Schema $schema): void 20 | { 21 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 22 | 23 | $this->addSql('ALTER TABLE commissionable_asset ALTER has_mobile_radio DROP NOT NULL'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Migrations/Version20200403195104.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 20 | 21 | $this->addSql('DROP TABLE IF EXISTS sessions'); // The "bin/console doctrine:schema:drop --full-database" command does not drop this table 22 | $this->addSql('CREATE TABLE sessions ( sess_id VARCHAR(128) NOT NULL PRIMARY KEY, sess_data BYTEA NOT NULL, sess_time INTEGER NOT NULL, sess_lifetime INTEGER NOT NULL );'); 23 | } 24 | 25 | public function down(Schema $schema): void 26 | { 27 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 28 | 29 | $this->addSql('DROP TABLE sessions'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Migrations/Version20200405080155.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 20 | 21 | $this->addSql('DROP INDEX organisation_name_unique'); 22 | $this->addSql('CREATE INDEX organization_name_idx ON organization (name)'); 23 | } 24 | 25 | public function down(Schema $schema): void 26 | { 27 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 28 | 29 | $this->addSql('DROP INDEX organization_name_idx'); 30 | $this->addSql('CREATE UNIQUE INDEX organisation_name_unique ON organization (name)'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Migrations/Version20200411165144.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 20 | 21 | $this->addSql('CREATE SEQUENCE mission_type_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); 22 | $this->addSql('CREATE TABLE mission_type (id INT NOT NULL, organization_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, user_skills_requirement JSON NOT NULL, asset_types_requirement JSON NOT NULL, PRIMARY KEY(id))'); 23 | $this->addSql('CREATE INDEX IDX_A59CFB2632C8A3DE ON mission_type (organization_id)'); 24 | $this->addSql('ALTER TABLE mission_type ADD CONSTRAINT FK_A59CFB2632C8A3DE FOREIGN KEY (organization_id) REFERENCES organization (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); 25 | } 26 | 27 | public function down(Schema $schema): void 28 | { 29 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 30 | 31 | $this->addSql('DROP SEQUENCE mission_type_id_seq CASCADE'); 32 | $this->addSql('DROP TABLE mission_type'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Migrations/Version20200414132415.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 20 | 21 | $this->addSql('ALTER TABLE commissionable_asset ADD license_plate VARCHAR(255) DEFAULT NULL, ADD comments TEXT DEFAULT NULL'); 22 | } 23 | 24 | public function down(Schema $schema): void 25 | { 26 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 27 | 28 | $this->addSql('ALTER TABLE commissionable_asset DROP license_plate, DROP comments'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Migrations/Version20200415083630.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 20 | 21 | $this->addSql('ALTER TABLE users ADD driving_licence BOOLEAN DEFAULT FALSE NOT NULL'); 22 | } 23 | 24 | public function down(Schema $schema): void 25 | { 26 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 27 | 28 | $this->addSql('ALTER TABLE users DROP driving_licence'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Migrations/Version20200418152205.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 20 | 21 | $this->addSql('ALTER TABLE user_availability ADD comment TEXT DEFAULT \'\' NOT NULL'); 22 | $this->addSql('ALTER TABLE commissionable_asset_availability ADD comment TEXT DEFAULT \'\' NOT NULL'); 23 | } 24 | 25 | public function down(Schema $schema): void 26 | { 27 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 28 | 29 | $this->addSql('ALTER TABLE user_availability DROP comment'); 30 | $this->addSql('ALTER TABLE commissionable_asset_availability DROP comment'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Migrations/Version20200420210639.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 20 | 21 | $this->addSql('ALTER TABLE mission_type ADD minimum_available_hours INT DEFAULT NULL'); 22 | } 23 | 24 | public function down(Schema $schema): void 25 | { 26 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 27 | 28 | $this->addSql('ALTER TABLE mission_type DROP minimum_available_hours'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Migrations/Version20200422103425.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 20 | 21 | $this->addSql('ALTER TABLE users ALTER phone_number TYPE VARCHAR(35)'); 22 | $this->addSql('ALTER TABLE users ALTER phone_number DROP NOT NULL'); 23 | $this->addSql('COMMENT ON COLUMN users.phone_number IS \'(DC2Type:phone_number)\''); 24 | } 25 | 26 | public function down(Schema $schema): void 27 | { 28 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 29 | 30 | $this->addSql('ALTER TABLE users ALTER phone_number TYPE VARCHAR(255)'); 31 | $this->addSql('ALTER TABLE users ALTER phone_number SET NOT NULL'); 32 | $this->addSql('COMMENT ON COLUMN users.phone_number IS NULL'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Migrations/Version20200426171023.php: -------------------------------------------------------------------------------- 1 | addSql('UPDATE users SET phone_number = CONCAT(\'+33\', SUBSTRING(phone_number,2)) WHERE phone_number LIKE \'0%\''); 20 | } 21 | 22 | public function down(Schema $schema): void 23 | { 24 | $this->addSql('UPDATE users SET phone_number = CONCAT(\'0\', SUBSTRING(phone_number,4)) WHERE phone_number LIKE \'+33%\''); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Migrations/Version20200503083535.php: -------------------------------------------------------------------------------- 1 | abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 20 | 21 | $this->addSql('ALTER TABLE mission ADD comment TEXT DEFAULT \'\' NOT NULL'); 22 | } 23 | 24 | public function down(Schema $schema): void 25 | { 26 | $this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.'); 27 | 28 | $this->addSql('ALTER TABLE mission DROP comment'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Monolog/Processor/ChangeLevelProcessor.php: -------------------------------------------------------------------------------- 1 | command = $command; 19 | $this->input = $input; 20 | } 21 | 22 | public function getCurrentCommand(): ?Command 23 | { 24 | return $this->command; 25 | } 26 | 27 | public function getInput(): ?InputInterface 28 | { 29 | return $this->input; 30 | } 31 | 32 | public function unsetCurrentCommand(): void 33 | { 34 | $this->command = null; 35 | } 36 | 37 | public function __invoke(array $record): array 38 | { 39 | if (null === $this->command || null === $this->input) { 40 | return $record; 41 | } 42 | 43 | $record['extra'] += [ 44 | 'command_name' => $this->command->getName(), 45 | 'command_arguments' => \json_encode($this->input->getArguments()), 46 | ]; 47 | 48 | return $record; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Monolog/Processor/ErrorProcessor.php: -------------------------------------------------------------------------------- 1 | = Logger::ERROR && !isset($record['context']['error'])) { 15 | $record['context']['error'] = $record['message'] ?? ''; 16 | } 17 | 18 | return $record; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Monolog/Processor/ExceptionProcessor.php: -------------------------------------------------------------------------------- 1 | includeStacktraces(); 17 | 18 | $formatedException = json_decode($jsonFormatter->format(['e' => $e]), true, 512, \JSON_THROW_ON_ERROR); 19 | $formatedException['e']['trace_string'] = json_encode($formatedException['e']['trace'] ?? [], \JSON_UNESCAPED_SLASHES + \JSON_THROW_ON_ERROR); 20 | unset($formatedException['e']['trace']); // The default trace format is not compliant with kibana 21 | 22 | $record['context']['exception'] = $formatedException['e']; 23 | } 24 | 25 | return $record; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ParamConverter/OrganizationParamConverter.php: -------------------------------------------------------------------------------- 1 | organizationRepository = $organizationRepository; 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function apply(Request $request, ParamConverter $configuration): bool 27 | { 28 | $name = $configuration->getName(); 29 | $id = $request->attributes->getInt($name); 30 | 31 | if (!empty($organizationId = $request->query->getInt('organizationId')) 32 | ) { 33 | $id = $organizationId; 34 | } 35 | 36 | $organization = $this->organizationRepository->find($id); 37 | 38 | if (null === $organization) { 39 | throw new NotFoundHttpException(sprintf('Organization with id "%d" does not exist.', $id)); 40 | } 41 | 42 | $request->attributes->set($name, $organization); 43 | 44 | return true; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function supports(ParamConverter $configuration): bool 51 | { 52 | return Organization::class === $configuration->getClass(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Repository/AvailabilitableRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | andWhere('(ua.startTime >= :start and ua.endTime <= :end) or (ua.startTime <= :start and ua.endTime >= :start) or (ua.startTime <= :end and ua.endTime >= :end)') 16 | ->setParameter('start', $from) 17 | ->setParameter('end', $to) 18 | ->getQuery() 19 | ->setHint(Query::HINT_INCLUDE_META_COLUMNS, true) 20 | ->getArrayResult(); 21 | } 22 | 23 | private function findLastUpdatesForEntities(QueryBuilder $qb): ?array 24 | { 25 | $rootAlias = $qb->getRootAliases()[0]; 26 | 27 | return $qb 28 | ->select(sprintf('MAX(COALESCE(%s.updatedAt, %s.createdAt)) as last_update, COUNT(%s) as total_count', $rootAlias, $rootAlias, $rootAlias)) 29 | ->setMaxResults(1) 30 | ->getQuery() 31 | ->getOneOrNullResult(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Repository/MissionTypeRepository.php: -------------------------------------------------------------------------------- 1 | findByOrganizationQb($organization)->getQuery()->getResult(); 29 | } 30 | 31 | public function findByOrganizationQb(Organization $organization): QueryBuilder 32 | { 33 | $qb = $this->createQueryBuilder('mt'); 34 | 35 | $qb 36 | ->join('mt.organization', 'o') 37 | ->where($qb->expr()->orX('o.id = :orga', 'o.parent = :orga')) 38 | ->setParameter('orga', $organization->parent ?: $organization) 39 | ->addOrderBy('mt.name', 'ASC'); 40 | 41 | return $qb; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Repository/SearchableRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | guardHandler = $guardHandler; 23 | $this->formAuthenticator = $formAuthenticator; 24 | } 25 | 26 | /** 27 | * @param User|UserInterface $user 28 | */ 29 | public function handleAuthentication(Request $request, UserInterface $user): Response 30 | { 31 | if (!$user instanceof User) { 32 | throw new \InvalidArgumentException(sprintf('Method %s only accepts a %s instance as its second argument.', __METHOD__, User::class)); 33 | } 34 | 35 | $response = $this->guardHandler->authenticateUserAndHandleSuccess( 36 | $user, 37 | $request, 38 | $this->formAuthenticator, 39 | 'main' 40 | ); 41 | 42 | if (!$response instanceof Response) { 43 | throw new \RuntimeException('Guard handler must return a Response object.'); 44 | } 45 | 46 | return $response; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Security/Voter/CommissionableAssetVoter.php: -------------------------------------------------------------------------------- 1 | getUser(); 29 | 30 | if (!$loggedOrganization instanceof Organization) { 31 | return false; 32 | } 33 | 34 | return $subject->organization === $loggedOrganization || $subject->organization->parent === $loggedOrganization; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Security/Voter/OrganizationVoter.php: -------------------------------------------------------------------------------- 1 | getUser(); 29 | 30 | if (!$loggedOrganization instanceof Organization) { 31 | return false; 32 | } 33 | 34 | if (self::CAN_CREATE === $attribute) { 35 | return $loggedOrganization->isParent(); 36 | } 37 | 38 | return $this->canManageOrganization($loggedOrganization, $subject); 39 | } 40 | 41 | private function canManageOrganization(Organization $loggedOrganization, Organization $organization): bool 42 | { 43 | return $loggedOrganization === $organization || $loggedOrganization === $organization->parent; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Security/Voter/UserVoter.php: -------------------------------------------------------------------------------- 1 | getUser(); 28 | 29 | if (!$loggedOrganization instanceof Organization || null === $subject->organization) { 30 | return false; 31 | } 32 | 33 | return $subject->organization === $loggedOrganization || $subject->getNotNullOrganization()->parent === $loggedOrganization; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Twig/Cache/PlanningFilesystemAdapter.php: -------------------------------------------------------------------------------- 1 | cacheDirectory = $directory.\DIRECTORY_SEPARATOR.$namespace.\DIRECTORY_SEPARATOR; 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | protected function doFetch(array $ids) 25 | { 26 | /** @var array $values */ 27 | $values = parent::doFetch($ids); 28 | foreach ($values as $id => $value) { 29 | $file = $this->getFile($id); 30 | if (!$handle = @fopen($file, 'rb')) { 31 | continue; 32 | } 33 | $values[$id] = [ 34 | "\x9D".pack('VN', (int) (0.1 + (int) fgets($handle) - 1527506807), ceil(filectime($file) / 100))."\x5F" => $value, 35 | ]; 36 | fclose($handle); 37 | } 38 | 39 | return $values; 40 | } 41 | 42 | private function getFile(string $id): string 43 | { 44 | // Use MD5 to favor speed over security, which is not an issue here 45 | $hash = str_replace('/', '-', base64_encode(hash('md5', static::class.$id, true))); 46 | $dir = $this->cacheDirectory.strtoupper($hash[0].\DIRECTORY_SEPARATOR.$hash[1].\DIRECTORY_SEPARATOR); 47 | 48 | return $dir.substr($hash, 2, 20); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Twig/Cache/RequestGenerator.php: -------------------------------------------------------------------------------- 1 | translator = $translator; 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function getFunctions(): array 25 | { 26 | return [ 27 | new TwigFunction('dynamicPropertyValue', [$this, 'dynamicPropertyValue']), 28 | ]; 29 | } 30 | 31 | /** 32 | * @param bool|string|int $value 33 | */ 34 | public function dynamicPropertyValue($value, array $propertyDefinition): string 35 | { 36 | if (\in_array($propertyDefinition['type'], [DynamicPropertiesType::TYPE_CHOICE, DynamicPropertiesType::TYPE_CHOICE_WITH_OTHER], true)) { 37 | return array_flip($propertyDefinition['choices'] ?? [])[$value] ?? $value; 38 | } 39 | 40 | if (DynamicPropertiesType::TYPE_BOOLEAN === $propertyDefinition['type']) { 41 | // false value will be sent by `default` twig filter as an empty string or an hyphen 42 | if (\in_array($value, ['', '-'], true)) { 43 | $value = false; 44 | } 45 | 46 | return $this->translator->trans(sprintf('common.%s', (bool) $value ? 'yes' : 'no')); 47 | } 48 | 49 | return (string) $value; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Twig/Extension/TwigTextExtension.php: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | -------------------------------------------------------------------------------- /templates/_footer.html.twig: -------------------------------------------------------------------------------- 1 |
2 | 10 |
11 | -------------------------------------------------------------------------------- /templates/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ 'project.name' | trans }} - {% block title %}{{ 'nav.welcome' | trans }}!{% endblock %} 8 | {% block stylesheets %} 9 | {{ encore_entry_link_tags('app') }} 10 | {% endblock %} 11 | 12 | 13 | {% block document_body %} 14 | 15 | {% include '_navbar.html.twig' %} 16 | 17 |
18 | {% block body %}{% endblock %} 19 |
20 | 21 | {% include '_footer.html.twig' %} 22 | 23 | {% include '_ajax_modal.html.twig' %} 24 | 25 | {% block javascripts %} 26 | {{ encore_entry_script_tags('app') }} 27 | {% endblock %} 28 | 29 | {% endblock %} 30 | 31 | -------------------------------------------------------------------------------- /templates/misc/flash-messages.html.twig: -------------------------------------------------------------------------------- 1 | {% for message in app.flashes('success') %} 2 | 5 | {% endfor %} 6 | {% for message in app.flashes('error') %} 7 | 10 | {% endfor %} 11 | -------------------------------------------------------------------------------- /templates/misc/form_theme.html.twig: -------------------------------------------------------------------------------- 1 | {% block dynamic_properties_widget %} 2 |
3 | {% for child in form.children %} 4 |
5 | {{ form_row(child) }} 6 |
7 | {% endfor %} 8 |
9 | {% endblock dynamic_properties_widget %} 10 | -------------------------------------------------------------------------------- /templates/organization/_delete_modal.html.twig: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /templates/organization/assetType/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block title %}{{ 'nav.section.organization' | trans }}{% endblock %} 4 | 5 | {% block javascripts %} 6 | {{ parent() }} 7 | {{ encore_entry_script_tags('asset-type-form') }} 8 | {% endblock %} 9 | 10 | {% block body %} 11 | {{ 'common.backToList' | trans }} 12 |

13 | {% if app.request.get('_route') == 'app_organization_assetType_new' %} 14 | {{ 'organization.asset_type.add_new' | trans }} - {{ app.user }} 15 | {% else %} 16 | {{ 'organization.asset_type.edit_form_title' | trans }} - {{ app.user }} 17 | {% endif %} 18 |

19 | 20 | {{ form_start(form, { attr: { id: 'edit-asset-type-form', 'data-persisted-keys': persistedKeys|join(',')}}) }} 21 | 22 | {{ form_errors(form) }} 23 |
24 | {{ form_row(form.name) }} 25 |
26 | {{ form_row(form.properties) }} 27 | 28 | 29 |
30 | 33 |
34 |
35 | 36 |
37 | {{ form_rest(form) }} 38 |
39 |
40 | {{ form_end(form) }} 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /templates/organization/assetType/list.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block title %}{{ 'nav.section.organization' | trans }}{% endblock %} 4 | 5 | {% block javascripts %} 6 | {{ parent() }} 7 | {% endblock %} 8 | 9 | {% block body %} 10 |

{{ 'organization.asset_type.main_title' | trans }} - {{ app.user }}

11 | 12 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% for assetType in assetTypes %} 28 | 29 | 30 | 33 | 34 | {% endfor %} 35 | 36 |
{{ 'common.name' | trans }}{{ 'common.actions' | trans }}
{{ assetType.name }} 31 | {{ 'action.edit' | trans }} 32 |
37 |
38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /templates/organization/commissionable_asset/_show.html.twig: -------------------------------------------------------------------------------- 1 | {% if app.user == asset.organization or app.user == asset.organization.parent %} 2 | {{ 'action.edit' | trans }} 3 | {% endif %} 4 | 5 |

{{ asset }}

6 | 7 |
8 |
{{ 'organization.default'|trans }}
9 |
{{ asset.organization }}
10 | 11 | {% for prop in asset.assetType | assetTypeProperties() %} 12 |
{{ prop.label | default('') }}
13 |
14 | {% if prop != null %} 15 | 16 | {{ dynamicPropertyValue(asset.properties[prop.key]|default('-'), prop)|truncate(75) }} 17 | 18 | {% else %} 19 | - 20 | {% endif %} 21 |
22 | {% endfor %} 23 |
24 | -------------------------------------------------------------------------------- /templates/organization/commissionable_asset/availability.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block title %}{{ 'organization.asset.availabilities' | trans ({ '%asset%' : asset }) }}{% endblock %} 4 | 5 | {% block javascripts %} 6 | {{ parent() }} 7 | {{ encore_entry_script_tags('availability-form') }} 8 | {{ encore_entry_script_tags('availability-table') }} 9 | {% endblock %} 10 | 11 | {% block body %} 12 |

{{ 'organization.asset.availabilities' | trans ({ '%asset%' : asset }) }}

13 | 14 | {% include 'availability/_table.html.twig' with { availabilityType: 'assets', availabilityId: asset.id } %} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/organization/commissionable_asset/form.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% set actionName = asset.id is not empty ? 'Modification' : 'Création' %} 4 | 5 | {% block title %}{{ 'organization.asset.createEdit' | trans ({ '%action%' : actionName }) }}{% endblock %} 6 | 7 | {% block body %} 8 | 9 | {% if asset.id %} 10 | {{ 'action.delete' | trans }} 17 | {% endif %} 18 | 19 |

{{ 'organization.asset.createEdit' | trans ({ '%action%' : actionName }) }}

20 | 21 | {{ form_start(form) }} 22 | {% if form.organization is defined %} 23 | {{ form_row(form.organization) }} 24 | {% endif %} 25 | {{ form_rest(form) }} 26 | {{ form_end(form) }} 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/organization/commissionable_asset/list.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block title %}{{ 'nav.section.organization' | trans }}{% endblock %} 4 | 5 | {% block javascripts %} 6 | {{ parent() }} 7 | {{ encore_entry_script_tags('availabilitable-list') }} 8 | {% endblock %} 9 | 10 | {% block body %} 11 |

{{ 'organization.assets' | trans }} - {{ organization }}

12 | 13 | {{ form(organization_selector_form) }} 14 | 15 | 22 | 23 | {{ include('organization/commissionable_asset/_list.html.twig') }} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/organization/commissionable_asset/preAdd.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block body %} 4 | {{ form_start(form, { method: 'GET', action: path('app_organization_asset_add', {organization: organization.id | default(app.user.id)})}) }} 5 | {{ form_rest(form) }} 6 | {{ form_end(form) }} 7 | {% endblock %} 8 | 9 | {% block title %}{{ 'organization.assetType' }}{% endblock %} 10 | 11 | -------------------------------------------------------------------------------- /templates/organization/commissionable_asset/show-modal-content.html.twig: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /templates/organization/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% if organization.id is null %} 4 | {% set formAction = path('app_organization_new') %} 5 | {% set formTitle = 'organization.add' | trans %} 6 | {% else %} 7 | {% set formAction = path('app_organization_edit', { object: organization.id }) %} 8 | {% set formTitle = 'organization.edit' | trans %} 9 | {% endif %} 10 | 11 | {% block title %}{{formTitle}}{% endblock %} 12 | 13 | {% block body %} 14 | 15 |

{{formTitle}}

16 | {{ form_start(form, { method: 'POST', action: formAction }) }} 17 | {{ form_row(form.name) }} 18 | {{ form_row(form.submit) }} 19 | {{ form_end(form) }} 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /templates/organization/forecast/_search_type.html.twig: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /templates/organization/mission/_form.html.twig: -------------------------------------------------------------------------------- 1 | {{ form_start(form) }} 2 | 3 | {% if form.organization is defined %} 4 | {{ form_row(form.organization) }} 5 | {% endif %} 6 | 7 | 8 | {{ form_row(form.type) }} 9 |

10 | {{ 'organization.mission.newType' | trans }} 11 |

12 | 13 | {{ form_row(form.name) }} 14 | 15 |
16 | 17 | 18 |
19 | 20 |
21 | {{ form_row(form.startTime) }} 22 | {{ form_row(form.endTime) }} 23 |
24 | 25 | {{ form_rest(form) }} 26 | 27 | 28 | {{ form_end(form) }} 29 | -------------------------------------------------------------------------------- /templates/organization/mission/_search_type.html.twig: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /templates/organization/mission/_show.html.twig: -------------------------------------------------------------------------------- 1 | {% if app.user == mission.organization %} 2 | {{ 'action.edit' | trans }} 3 | {% endif %} 4 | 5 |

6 | {{ mission.type.name | default('organization.mission.title') | trans }} 7 | {{ mission }} 8 |

9 | 10 | {% if mission.startTime is not null %} 11 |

{{ 'common.start' | trans }}: {{ mission.startTime | format_datetime(date_format='full', time_format='short') }}

12 | {% endif %} 13 | 14 | {% if mission.endTime is not null %} 15 |

{{ 'common.end' | trans }}: {{ mission.endTime | format_datetime(date_format='full', time_format='short') }}

16 | {% endif %} 17 | 18 | {% if mission.comment %} 19 |

{{ 'organization.mission.comment' | trans }}

20 | {{ mission.comment | nl2br }} 21 | {% endif %} 22 | 23 |

{{ 'organization.users' | trans }}

24 | {{ include('organization/user/_list.html.twig', {users: mission.users | sortBySkills, organization: mission.organization}) }} 25 | 26 |

{{ 'organization.assets' | trans }}

27 | {{ include('organization/commissionable_asset/_list.html.twig', {assets: mission.assets, organization: mission.organization}) }} 28 | 29 | -------------------------------------------------------------------------------- /templates/organization/mission/add-to-mission-modal-content.html.twig: -------------------------------------------------------------------------------- 1 |
2 |

{{ userToAdd }} {{ userToAdd|userBadges }}

3 |
{{ 'organization.mission.periodList' | trans({ '%from%' : filters.from | format_date('long'), '%to%' : filters.to | format_date('long') }) }}
4 | 5 | 8 | 9 | {% include 'organization/mission/_list.html.twig' with {modalLinks: true} %} 10 |
11 | -------------------------------------------------------------------------------- /templates/organization/mission/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block javascripts %} 4 | {{ parent() }} 5 | {{ encore_entry_script_tags('missions') }} 6 | {% endblock %} 7 | 8 | {% block title %}{{ 'organization.mission.editFormTitle' | trans }}{% endblock %} 9 | 10 | {% block body %} 11 | {{ 'action.delete' | trans }} 12 | 13 | {{ 'common.backToList' | trans }} 14 | 15 |

{{ 'organization.mission.editFormTitle' | trans }}

16 | 17 | {{ include('organization/mission/_form.html.twig', {'button_label': 'action.save' | trans}) }} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /templates/organization/mission/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block javascripts %} 4 | {{ parent() }} 5 | {{ encore_entry_script_tags('missions') }} 6 | {% endblock %} 7 | 8 | {% block title %}{{ 'organization.mission.listTitle' | trans }}{% endblock %} 9 | 10 | {% block body %} 11 |

{{ 'organization.mission.listTitle' | trans }}

12 | 13 | {% include 'organization/mission/_search_type.html.twig' %} 14 | 15 | {% set searchOptions = { 16 | 'from': filters.from | default(false) ? filters.from | date('Y-m-d\\T00:00:00') : null, 17 | 'to': filters.to | default(false) ? filters.to | date('Y-m-d\\T00:00:00') : null, 18 | 'missionTypes': filters.missionTypes | default({}) | map(type => type.id), 19 | } | filter(val => val) %} 20 | 21 | 31 | 32 | {% include('organization/mission/_list.html.twig') %} 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /templates/organization/mission/new.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block javascripts %} 4 | {{ parent() }} 5 | {{ encore_entry_script_tags('missions') }} 6 | {% endblock %} 7 | 8 | {% block title %}{{ 'organization.mission.addNew' | trans }}{% endblock %} 9 | 10 | {% block body %} 11 | {{ 'common.backToList' | trans }} 12 | 13 |

{{ 'organization.mission.addNew' | trans }}

14 | 15 | {{ include('organization/mission/_form.html.twig') }} 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /templates/organization/mission/show-modal-content.html.twig: -------------------------------------------------------------------------------- 1 |
2 | {% include 'organization/mission/_show.html.twig' %} 3 |
4 | -------------------------------------------------------------------------------- /templates/organization/mission/show.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block javascripts %} 4 | {{ parent() }} 5 | {{ encore_entry_script_tags('missions') }} 6 | {% endblock %} 7 | 8 | {% block title %}{{ mission }}{% endblock %} 9 | 10 | {% block body %} 11 | {{ 'common.backToList' | trans }} 12 | 13 | {% include 'organization/mission/_show.html.twig' %} 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /templates/organization/mission_type/_form.html.twig: -------------------------------------------------------------------------------- 1 | {{ form_start(form) }} 2 | 3 | {{ form_row(form.name) }} 4 | 5 | {{ form_label(form.minimumAvailableHours) }} 6 |
7 | {{ form_widget(form.minimumAvailableHours) }} 8 |
9 |
heures
10 |
11 |
12 | {{ form_help(form.minimumAvailableHours) }} 13 | 14 |
15 | {{ form_row(form.userSkillsRequirement) }} 16 |
17 | 18 |
19 |
20 | 21 |
22 | {{ form_row(form.assetTypesRequirement) }} 23 |
24 | 25 |
26 |
27 | 28 | 29 | {{ form_end(form) }} 30 | -------------------------------------------------------------------------------- /templates/organization/mission_type/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block javascripts %} 4 | {{ parent() }} 5 | {{ encore_entry_script_tags('mission-type-form') }} 6 | {% endblock %} 7 | 8 | {% block title %}{{ 'organization.missionType.editFormTitle' | trans }}{% endblock %} 9 | 10 | {% block body %} 11 | {{ 'common.backToList' | trans }} 12 | 13 |

{{ 'organization.missionType.editFormTitle' | trans }}

14 | 15 | {{ include('organization/mission_type/_form.html.twig', {'button_label': 'action.save' | trans}) }} 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /templates/organization/mission_type/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block title %}{{ 'organization.missionType.mainTitle' | trans }}{% endblock %} 4 | 5 | {% block body %} 6 |

{{ 'organization.missionType.mainTitle' | trans }}

7 | 8 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for mission_type in mission_types %} 24 | 25 | 26 | 30 | 31 | {% else %} 32 | 33 | 34 | 35 | {% endfor %} 36 | 37 |
{{ 'common.name' | trans }}{{ 'common.actions' | trans }}
{{ mission_type.name }} 27 | {{ 'action.edit' | trans }} 28 | {{ 'action.delete' | trans }} 29 |
{{ 'message.noAvailableData' | trans }}
38 |
39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /templates/organization/mission_type/new.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block javascripts %} 4 | {{ parent() }} 5 | {{ encore_entry_script_tags('mission-type-form') }} 6 | {% endblock %} 7 | 8 | {% block title %}{{ 'organization.missionType.addNew' | trans }}{% endblock %} 9 | 10 | {% block body %} 11 | {{ 'common.backToList' | trans }} 12 | 13 |

{{ 'organization.missionType.addNew' | trans }}

14 | 15 | {{ include('organization/mission_type/_form.html.twig') }} 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /templates/organization/planning/_availabilities_assets.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/planning/_availabilities_base.html.twig' %} 2 | {% import 'organization/commissionable_asset/_show.html.twig' as assetsMacro %} 3 | 4 | {% block type %}{{ type | assetTypeName }}{% endblock type %} 5 | 6 | {# CAUTION: columns number of blocks itemDataHeader and itemDataDetails should be the same than in _availabilities_users.html.twig #} 7 | {% block itemDataHeader %} 8 | {% for prop in type | assetTypeProperties(itemDataRow) %} 9 | {{ prop.label | default('') }} 10 | {% endfor %} 11 | {% endblock itemDataHeader %} 12 | 13 | {% block itemDataRowHeader %} 14 | 17 | {% endblock itemDataRowHeader %} 18 | 19 | {% block itemDataDetails %} 20 | {% for prop in type | assetTypeProperties(itemDataRow) %} 21 | 22 | {% if prop != null %} 23 | 24 | {{ dynamicPropertyValue(item.entity.properties[prop.key] | default('-'), prop)|truncate(75) }} 25 | 26 | {% endif %} 27 | 28 | {% endfor %} 29 | 30 | {% endblock itemDataDetails %} 31 | -------------------------------------------------------------------------------- /templates/organization/planning/planning.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block javascripts %} 4 | {{ parent() }} 5 | {{ encore_entry_script_tags('planning') }} 6 | {{ encore_entry_script_tags('availability-table') }} 7 | {% endblock %} 8 | 9 | {% block title %}{{ 'calendar.planning' | trans }}{% endblock %} 10 | 11 | {% block container %} 12 |
13 |
14 | {{ include('misc/flash-messages.html.twig') }} 15 |

16 | {{ 'calendar.planning' | trans }} - 17 | {{ 'calendar.period' | trans ({ 18 | '%from%' : periodCalculator.from | format_date(pattern="eeee dd MMMM"), 19 | '%to%' : periodCalculator.to | date_modify('- 1 minute') | format_date(pattern="eeee dd MMMM") 20 | }) }} 21 |

22 | {% include 'organization/planning/_search_type.html.twig' %} 23 |
24 | {% include 'organization/planning/_results.html.twig' %} 25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /templates/organization/search.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block title %}Espace structure{% endblock %} 4 | 5 | {% block javascripts %} 6 | {{ parent() }} 7 | {{ encore_entry_script_tags('availabilitable-list') }} 8 | {% endblock %} 9 | 10 | {% block body %} 11 | {{ include('misc/flash-messages.html.twig') }} 12 | 13 |

{{ 'action.searchQuery' | trans({'%query%': query}) }}

14 | 15 |
16 |

Bénévoles

17 | {% if users|length %} 18 | {% include 'organization/user/_list.html.twig' with {organization: app.user} %} 19 | {% else %} 20 |

{{ 'organization.search.noUsers' | trans }}

21 | {% endif %} 22 |

Afficher la liste de mes bénévoles inscrits

23 | 24 |
25 |

Véhicules

26 | {% if assets|length %} 27 | {% include'organization/commissionable_asset/_list.html.twig' %} 28 | {% else %} 29 |

{{ 'organization.search.noAssets' | trans }}

30 | {% endif %} 31 |

Afficher la liste de mes véhicules

32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /templates/organization/user/_show.html.twig: -------------------------------------------------------------------------------- 1 | {% if app.user == user.organization or app.user == user.organization.parent %} 2 | {{ 'action.edit' | trans }} 3 | {% endif %} 4 | 5 |

{{ user }}

6 | 7 |
8 |
{{ 'user.skills'|trans }}
9 |
{{ user|userBadges }}
10 |
{{ 'user.identificationNumber'|trans }}
11 |
{{ user.identificationNumber }}
12 |
{{ 'user.info'|trans }}
13 |
14 | {{ user.emailAddress }}
15 | 16 | {{ user.phoneNumber|phone_number_format('NATIONAL') }} 17 | 18 |
19 |
{{ 'user.dob'|trans }}
20 |
{{ user.birthday|format_date('long') }}
21 | {% for user_property in user_properties %} 22 |
{{ user_property.columnLabel|default(user_property.label)|default }}
23 |
24 | {{ dynamicPropertyValue(user.properties[user_property.key]|default('-'), user_property)|truncate(75) }} 25 |
26 | {% endfor %} 27 |
28 | 29 |
{{ 'organization.mission.listTitle'|trans }}
30 | {% include 'organization/mission/_list.html.twig' with {missions: user.missions, modalLinks: true} %} 31 | -------------------------------------------------------------------------------- /templates/organization/user/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block javascripts %} 4 | {{ parent() }} 5 | {{ encore_entry_script_tags('form-choice-with-other') }} 6 | {% endblock %} 7 | 8 | {% block body %} 9 |

{{ 'organization.editUserProfile' | trans }}

10 | 11 | {{ 'action.delete' | trans }} 18 | 19 | {% include '/user/_user_form.html.twig' %} 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /templates/organization/user/list.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block javascripts %} 4 | {{ parent() }} 5 | {{ encore_entry_script_tags('availabilitable-list') }} 6 | {% endblock %} 7 | 8 | {% block title %}{{ 'organization.users' | trans }}{% endblock %} 9 | 10 | {% block body %} 11 |

{{ 'organization.users' | trans }} - {{ organization }}

12 | 13 | {{ form(organization_selector_form) }} 14 | 15 | {{ include('organization/user/_list.html.twig') }} 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /templates/organization/user/missions_list.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'organization/base.html.twig' %} 2 | 3 | {% block title %}{{ user }} - {{ 'organization.mission.listTitle' | trans }}{% endblock %} 4 | 5 | {% block body %} 6 |

{{ user }} - {{ 'organization.mission.listTitle' | trans }}

7 | 8 | {% include 'organization/mission/_list.html.twig' %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /templates/organization/user/show-modal-content.html.twig: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /templates/user/_introduction.html.twig: -------------------------------------------------------------------------------- 1 |

2 | {{ 'register.introduction'|trans|raw }} 3 |

4 | -------------------------------------------------------------------------------- /templates/user/_user_form.html.twig: -------------------------------------------------------------------------------- 1 | {{ form_start(form) }} 2 | {{ form_errors(form) }} 3 | 4 |
5 |
6 | {{ form_row(form.identificationNumber, { 'label': 'user.identificationNumber' | trans }) }} 7 |
8 |
9 | {% if form.birthday is defined %} 10 | {{ form_row(form.birthday, { 'label': 'user.dob' | trans }) }} 11 | {% endif %} 12 |
13 |
14 | 15 |
16 |
17 | {{ form_row(form.organization) }} 18 |
19 |
20 | 21 |
22 |
23 | {{ form_row(form.firstName, { 'label': 'user.firstName' | trans }) }} 24 |
25 |
26 | {{ form_row(form.lastName, { 'label': 'user.lastName' | trans }) }} 27 |
28 |
29 | 30 |
31 |
32 | {{ form_row(form.emailAddress, { 'label' : 'user.email' | trans }) }} 33 |
34 |
35 | {{ form_row(form.phoneNumber, { 'label' : 'user.mobile' | trans }) }} 36 |
37 |
38 | 39 |
40 |
41 | {{ form_row(form.skillSet) }} 42 |
43 |
44 | 45 | {{ form_row(form.properties) }} 46 | 47 |
48 |
49 | 50 |
51 |
52 |
53 | {{ form_end(form) }} 54 | -------------------------------------------------------------------------------- /templates/user/account-form.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% set actionName = (user is defined and user.id is not null) ? 'Modification' : 'Création' %} 4 | {% block title %}{{ 'user.accountAction' | trans({ '%action%' : actionName }) }}{% endblock %} 5 | 6 | {% block javascripts %} 7 | {{ parent() }} 8 | {{ encore_entry_script_tags('form-choice-with-other') }} 9 | {% endblock %} 10 | 11 | {% block body %} 12 |

{{ 'user.accountAction' | trans({ '%action%' : actionName }) }}

13 | 14 | {% if user is not defined or user.id is not defined or user.id is null %} 15 | {% include '/user/_introduction.html.twig' %} 16 | {% endif %} 17 | 18 | {% include '/user/_user_form.html.twig' with { 'form': form } %} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /templates/user/availability.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block title %}{{ 'user.availabilities' | trans }}{% endblock %} 4 | 5 | {% block javascripts %} 6 | {{ parent() }} 7 | {{ encore_entry_script_tags('availability-form') }} 8 | {{ encore_entry_script_tags('availability-table') }} 9 | {% endblock %} 10 | 11 | {% block body %} 12 | {% include 'availability/_table.html.twig' with { availabilityType: 'users', availabilityId: app.user.id } %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /tests/Behat/FixturesContext.php: -------------------------------------------------------------------------------- 1 | aliceFixturesLoader = $aliceFixturesLoader; 23 | $this->kernel = $kernel; 24 | $this->entityManager = $entityManager; 25 | } 26 | 27 | /** 28 | * @AfterScenario @javascript 29 | */ 30 | public function loadFixtures(): void 31 | { 32 | StaticDriver::beginTransaction(); 33 | $this->aliceFixturesLoader->load( 34 | new Application($this->kernel), 35 | $this->entityManager, 36 | [], 37 | 'panther', 38 | false, 39 | false 40 | ); 41 | StaticDriver::commit(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Domain/AvailabilitiesDomainTest.php: -------------------------------------------------------------------------------- 1 | expectException(\InvalidArgumentException::class); 18 | $this->expectExceptionMessage($message); 19 | 20 | new AvailabilitiesDomain([], $slotInterval); 21 | } 22 | 23 | public function getInvalidSlotIntervals(): array 24 | { 25 | return [ 26 | ['+2 hours 30 minutes', 'Invalid slot interval: unable to set a complete day with "+2 hours 30 minutes".'], 27 | ['+42 minutes', 'Invalid slot interval: unable to set a complete day with "+42 minutes".'], 28 | ['+5 hours', 'Invalid slot interval: unable to set a complete day with "+5 hours".'], 29 | ]; 30 | } 31 | 32 | /** 33 | * @dataProvider getValidSlotIntervals 34 | */ 35 | public function testNewAvailabilitiesDomain(string $slotInterval): void 36 | { 37 | self::assertInstanceOf(AvailabilitiesDomain::class, new AvailabilitiesDomain([], $slotInterval)); 38 | } 39 | 40 | public function getValidSlotIntervals(): array 41 | { 42 | return [ 43 | ['+1 hour 30 minutes'], 44 | ['+2 hours'], 45 | ['+3 hours'], 46 | ['+4 hours'], 47 | ['+6 hours'], 48 | ]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Entity/OrganizationTest.php: -------------------------------------------------------------------------------- 1 | id = 1; 16 | $organization->name = 'DT75'; 17 | 18 | self::assertSame(1, $organization->id); 19 | self::assertSame('DT75', $organization->name); 20 | self::assertSame('DT75', (string) $organization); 21 | self::assertNull($organization->parent); 22 | } 23 | 24 | public function testCreateOrganizationWithParent(): void 25 | { 26 | $parent = new Organization(); 27 | $parent->id = 1; 28 | $parent->name = 'DT75'; 29 | $child = new Organization(); 30 | $child->id = 2; 31 | $child->name = 'UL09'; 32 | $child->parent = $parent; 33 | 34 | self::assertSame(2, $child->id); 35 | self::assertSame('UL09', $child->name); 36 | self::assertSame('DT75 - UL09', (string) $child); 37 | self::assertSame($parent, $child->parent); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/ObjectManager.php: -------------------------------------------------------------------------------- 1 | boot(); 13 | 14 | $container = $kernel->getContainer(); 15 | 16 | return $container->get('doctrine')->getManager(); 17 | -------------------------------------------------------------------------------- /tests/Twig/Cache/RequestGeneratorTest.php: -------------------------------------------------------------------------------- 1 | generateKey($data)); 18 | } 19 | 20 | public function getFilters(): array 21 | { 22 | return [ 23 | ['3a3160c3d13a2b6e9a56089f00c0743bd55166fb', ['foo']], 24 | ['289df0705aad811a4f04744ea205da25cad15371', ['bar']], 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /translations/security.fr.yaml: -------------------------------------------------------------------------------- 1 | "Invalid credentials.": "Veuillez saisir un numéro NIVOL ou une adresse e-mail valide, ou la date de naissance ne corresponds pas à ce NIVOL/email." 2 | -------------------------------------------------------------------------------- /translations/security_organization.fr.yml: -------------------------------------------------------------------------------- 1 | "Invalid credentials.": "Le mot de passe associé à cette structure est invalide." 2 | -------------------------------------------------------------------------------- /translations/validators.fr.yaml: -------------------------------------------------------------------------------- 1 | organization: 2 | asset_type: 3 | property_unique_error: Au moins un identifiant unique est dupliqué 4 | mission_type: 5 | user_skill_unique_error: Au moins une compétence de bénévole est dupliquée 6 | asset_type_unique_error: Au moins un type de véhicule est dupliqué 7 | --------------------------------------------------------------------------------