From 5c165d38795574b840b13a28c88f20c3081fa41a Mon Sep 17 00:00:00 2001 From: vincent Date: Thu, 25 Jul 2024 07:55:23 +0200 Subject: [PATCH] wip --- assets/app.js | 9 +- assets/bootstrap.js | 5 + assets/controllers.json | 4 + assets/controllers/josm_controller.js | 44 + assets/controllers/map_controller.js | 39 + assets/styles/app.css | 3 + composer.json | 5 + composer.lock | 538 +++++++++++- config/bundles.php | 2 + config/packages/twig.yaml | 2 + config/packages/workflow.yaml | 50 ++ importmap.php | 13 + migrations/Version20240720132139.php | 31 - migrations/Version20240720143743.php | 40 - src/Controller/ProjectController.php | 24 +- src/Controller/TaskController.php | 327 ++++++++ src/DataFixtures/AppFixtures.php | 1439 +++++++++++++++++++++++++++++++++ src/Entity/Comment.php | 52 ++ src/Entity/Project.php | 74 ++ src/Entity/Tag.php | 75 ++ src/Entity/Task.php | 204 +++++ src/Form/CommentType.php | 27 + src/Form/ProjectType.php | 16 +- src/Form/TaskLifecycleType.php | 42 + src/Form/TaskType.php | 33 + src/Repository/CommentRepository.php | 43 + src/Repository/TagRepository.php | 43 + src/Repository/TaskRepository.php | 43 + src/Service/GeoJsonManager.php | 48 ++ src/Service/TaskLifecycleManager.php | 55 ++ symfony.lock | 38 + templates/project/index.html.twig | 9 +- templates/project/show.html.twig | 66 +- templates/task/create.html.twig | 13 + templates/task/show.html.twig | 60 ++ templates/task/update.html.twig | 13 + 36 files changed, 3434 insertions(+), 95 deletions(-) create mode 100644 assets/bootstrap.js create mode 100644 assets/controllers.json create mode 100644 assets/controllers/josm_controller.js create mode 100644 assets/controllers/map_controller.js create mode 100644 config/packages/workflow.yaml delete mode 100644 migrations/Version20240720132139.php delete mode 100644 migrations/Version20240720143743.php create mode 100644 src/Controller/TaskController.php create mode 100644 src/DataFixtures/AppFixtures.php create mode 100644 src/Entity/Comment.php create mode 100644 src/Entity/Tag.php create mode 100644 src/Entity/Task.php create mode 100644 src/Form/CommentType.php create mode 100644 src/Form/TaskLifecycleType.php create mode 100644 src/Form/TaskType.php create mode 100644 src/Repository/CommentRepository.php create mode 100644 src/Repository/TagRepository.php create mode 100644 src/Repository/TaskRepository.php create mode 100644 src/Service/GeoJsonManager.php create mode 100644 src/Service/TaskLifecycleManager.php create mode 100644 templates/task/create.html.twig create mode 100644 templates/task/show.html.twig create mode 100644 templates/task/update.html.twig diff --git a/assets/app.js b/assets/app.js index 1a565f8..10e5fcb 100644 --- a/assets/app.js +++ b/assets/app.js @@ -1,4 +1,9 @@ -import './vendor/bootstrap/dist/css/bootstrap.min.css'; -import './vendor/bootstrap/bootstrap.index.js'; +import './bootstrap.js'; +import './vendor/bootstrap/dist/css/bootstrap.min.css'; +import './vendor/leaflet/dist/leaflet.min.css'; import './styles/app.css'; + +import { Tooltip } from './vendor/bootstrap/bootstrap.index.js'; +const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]') +const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new Tooltip(tooltipTriggerEl)) diff --git a/assets/bootstrap.js b/assets/bootstrap.js new file mode 100644 index 0000000..d4e50c9 --- /dev/null +++ b/assets/bootstrap.js @@ -0,0 +1,5 @@ +import { startStimulusApp } from '@symfony/stimulus-bundle'; + +const app = startStimulusApp(); +// register any custom, 3rd party controllers here +// app.register('some_controller_name', SomeImportedController); diff --git a/assets/controllers.json b/assets/controllers.json new file mode 100644 index 0000000..a1c6e90 --- /dev/null +++ b/assets/controllers.json @@ -0,0 +1,4 @@ +{ + "controllers": [], + "entrypoints": [] +} diff --git a/assets/controllers/josm_controller.js b/assets/controllers/josm_controller.js new file mode 100644 index 0000000..7817295 --- /dev/null +++ b/assets/controllers/josm_controller.js @@ -0,0 +1,44 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static values = { + importurl: String, + layername: String, + } + + remoteControl() { + // cf + const baseurl = 'http://localhost:8111'; + const _this = this; + var url = baseurl + '/version'; + fetch(url) + .then(function (response) { + return response.json(); + }) + .then(function (json) { + console.log('JOSM v' + json.version); + url = baseurl + '/imagery?' + (new URLSearchParams({ + 'id': 'osmfr', // 'fr.orthohr', // cf + })); + fetch(url) + .then(function (response) { + return response.text(); + }) + .then(function (text) { + console.log(text); + }); + url = baseurl + '/import?' + (new URLSearchParams({ + 'new_layer': true, + 'layer_name': _this.layernameValue, + 'url': _this.importurlValue, + })); + fetch(url) + .then(function (response) { + return response.text(); + }) + .then(function (text) { + console.log(text); + }); + }); + } +} diff --git a/assets/controllers/map_controller.js b/assets/controllers/map_controller.js new file mode 100644 index 0000000..8a94e12 --- /dev/null +++ b/assets/controllers/map_controller.js @@ -0,0 +1,39 @@ +import { Controller } from '@hotwired/stimulus'; +import 'leaflet'; + +export default class extends Controller { + static values = { + geojson: String, + } + + connect() { + var map = L.map(this.element); + L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, + attribution: '© OpenStreetMap' + }).addTo(map); + var layer = L.featureGroup(); + var geojsons = JSON.parse(this.geojsonValue); + if (geojsons.length > 0) { + geojsons.forEach(function (geojson) { + const feature0 = geojson.features[0].properties; + L.geoJSON(geojson, { + style: function (feature) { + var color = 'blue'; + switch (feature.properties.color) { + case 'danger' : color = '#dc3545'; break + case 'warning' : color = '#ffc107'; break + case 'success' : color = '#198754'; break + } + return {color: color}; + } + }).bindTooltip(feature0.name).addTo(layer).on('click', function (event) { + window.location.href = event.layer.feature.properties.url; + }); + }); + layer.addTo(map); + console.log(layer.getBounds().toBBoxString()); + map.fitBounds(layer.getBounds()); + } + } +} diff --git a/assets/styles/app.css b/assets/styles/app.css index e69de29..16ea686 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -0,0 +1,3 @@ +.min-vh-50 { + min-height: 50vh; +} diff --git a/composer.json b/composer.json index 7ade4b1..58328b3 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "doctrine/doctrine-bundle": "^2.12", "doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/orm": "^3.2", + "jmikola/geojson": "^1.0", "league/commonmark": "^2.4", "league/html-to-markdown": "^5.1", "stof/doctrine-extensions-bundle": "^1.12", @@ -22,14 +23,18 @@ "symfony/form": "7.1.*", "symfony/framework-bundle": "7.1.*", "symfony/runtime": "7.1.*", + "symfony/stimulus-bundle": "^2.18", "symfony/twig-bundle": "7.1.*", "symfony/validator": "7.1.*", + "symfony/workflow": "7.1.*", "symfony/yaml": "7.1.*", "twig/extra-bundle": "^2.12|^3.0", + "twig/intl-extra": "^3.10", "twig/markdown-extra": "^3.10", "twig/twig": "^2.12|^3.0" }, "require-dev": { + "doctrine/doctrine-fixtures-bundle": "^3.6", "symfony/debug-bundle": "7.1.*", "symfony/maker-bundle": "^1.60", "symfony/stopwatch": "7.1.*", diff --git a/composer.lock b/composer.lock index ee96786..b8cdfe0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9ebc747abf5efb362443c7d1a30b2086", + "content-hash": "ed07bf1e98cc998eddcf49958cb2835f", "packages": [ { "name": "behat/transliterator", @@ -1655,6 +1655,65 @@ "time": "2024-06-25T16:22:14+00:00" }, { + "name": "jmikola/geojson", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/jmikola/geojson.git", + "reference": "e28f3855bb61a91aab32b74c176d76dd0b5658d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmikola/geojson/zipball/e28f3855bb61a91aab32b74c176d76dd0b5658d7", + "reference": "e28f3855bb61a91aab32b74c176d76dd0b5658d7", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.4 || ^8.0", + "symfony/polyfill-php80": "^1.25" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "scrutinizer/ocular": "^1.8.1", + "slevomat/coding-standard": "^8.0", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "GeoJson\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Mikola", + "email": "jmikola@gmail.com" + } + ], + "description": "GeoJSON implementation for PHP", + "homepage": "https://github.com/jmikola/geojson", + "keywords": [ + "geo", + "geojson", + "geospatial" + ], + "support": { + "issues": "https://github.com/jmikola/geojson/issues", + "source": "https://github.com/jmikola/geojson/tree/1.2.0" + }, + "time": "2023-12-04T17:19:43+00:00" + }, + { "name": "league/commonmark", "version": "2.4.2", "source": { @@ -4261,6 +4320,92 @@ "time": "2024-06-28T13:13:31+00:00" }, { + "name": "symfony/intl", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/intl.git", + "reference": "66c1ecda092b1130ada2cf5f59dacfd5b6e9c99c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/intl/zipball/66c1ecda092b1130ada2cf5f59dacfd5b6e9c99c", + "reference": "66c1ecda092b1130ada2cf5f59dacfd5b6e9c99c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/string": "<7.1" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Intl\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/Resources/data/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Eriksen Costa", + "email": "eriksen.costa@infranology.com.br" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides access to the localization data of the ICU library", + "homepage": "https://symfony.com", + "keywords": [ + "i18n", + "icu", + "internationalization", + "intl", + "l10n", + "localization" + ], + "support": { + "source": "https://github.com/symfony/intl/tree/v7.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:57:53+00:00" + }, + { "name": "symfony/options-resolver", "version": "v7.1.1", "source": { @@ -5130,6 +5275,75 @@ "time": "2024-04-18T09:32:20+00:00" }, { + "name": "symfony/stimulus-bundle", + "version": "v2.18.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/stimulus-bundle.git", + "reference": "017b60e036c366c8ce0e77864d5aabab436ad73d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/017b60e036c366c8ce0e77864d5aabab436ad73d", + "reference": "017b60e036c366c8ce0e77864d5aabab436ad73d", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/deprecation-contracts": "^2.0|^3.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "twig/twig": "^2.15.3|^3.8" + }, + "require-dev": { + "symfony/asset-mapper": "^6.3|^7.0", + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "zenstruck/browser": "^1.4" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\UX\\StimulusBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Integration with your Symfony app & Stimulus!", + "keywords": [ + "symfony-ux" + ], + "support": { + "source": "https://github.com/symfony/stimulus-bundle/tree/v2.18.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-06-11T13:21:54+00:00" + }, + { "name": "symfony/stopwatch", "version": "v7.1.1", "source": { @@ -5888,6 +6102,93 @@ "time": "2024-06-28T08:00:31+00:00" }, { + "name": "symfony/workflow", + "version": "v7.1.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/workflow.git", + "reference": "bc9a36fdd6a6fab9f630bd73b428aa06917e17e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/workflow/zipball/bc9a36fdd6a6fab9f630bd73b428aa06917e17e8", + "reference": "bc9a36fdd6a6fab9f630bd73b428aa06917e17e8", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "conflict": { + "symfony/event-dispatcher": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Workflow\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools for managing a workflow or finite state machine", + "homepage": "https://symfony.com", + "keywords": [ + "petrinet", + "place", + "state", + "statemachine", + "transition", + "workflow" + ], + "support": { + "source": "https://github.com/symfony/workflow/tree/v7.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:57:53+00:00" + }, + { "name": "symfony/yaml", "version": "v7.1.1", "source": { @@ -6033,6 +6334,70 @@ "time": "2024-05-11T07:35:57+00:00" }, { + "name": "twig/intl-extra", + "version": "v3.10.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/intl-extra.git", + "reference": "693f6beb8ca91fc6323e01b3addf983812f65c93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/693f6beb8ca91fc6323e01b3addf983812f65c93", + "reference": "693f6beb8ca91fc6323e01b3addf983812f65c93", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/intl": "^5.4|^6.4|^7.0", + "twig/twig": "^3.10" + }, + "require-dev": { + "symfony/phpunit-bridge": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Twig\\Extra\\Intl\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Twig extension for Intl", + "homepage": "https://twig.symfony.com", + "keywords": [ + "intl", + "twig" + ], + "support": { + "source": "https://github.com/twigphp/intl-extra/tree/v3.10.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2024-05-11T07:35:57+00:00" + }, + { "name": "twig/markdown-extra", "version": "v3.10.0", "source": { @@ -6186,6 +6551,177 @@ ], "packages-dev": [ { + "name": "doctrine/data-fixtures", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/data-fixtures.git", + "reference": "bbcb74f2ac6dbe81a14b3c3687d7623490a0448f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/bbcb74f2ac6dbe81a14b3c3687d7623490a0448f", + "reference": "bbcb74f2ac6dbe81a14b3c3687d7623490a0448f", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^0.5.3 || ^1.0", + "doctrine/persistence": "^2.0|^3.0", + "php": "^7.4 || ^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.5 || >=5", + "doctrine/orm": "<2.14 || >=4", + "doctrine/phpcr-odm": "<1.3.0" + }, + "require-dev": { + "doctrine/annotations": "^1.12 || ^2", + "doctrine/coding-standard": "^12", + "doctrine/dbal": "^3.5 || ^4", + "doctrine/mongodb-odm": "^1.3.0 || ^2.0.0", + "doctrine/orm": "^2.14 || ^3", + "ext-sqlite3": "*", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.6.13 || ^10.4.2", + "symfony/cache": "^5.4 || ^6.3 || ^7", + "symfony/var-exporter": "^5.4 || ^6.3 || ^7", + "vimeo/psalm": "^5.9" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)", + "doctrine/mongodb-odm": "For loading MongoDB ODM fixtures", + "doctrine/orm": "For loading ORM fixtures", + "doctrine/phpcr-odm": "For loading PHPCR ODM fixtures" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\DataFixtures\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Data Fixtures for all Doctrine Object Managers", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "database" + ], + "support": { + "issues": "https://github.com/doctrine/data-fixtures/issues", + "source": "https://github.com/doctrine/data-fixtures/tree/1.7.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdata-fixtures", + "type": "tidelift" + } + ], + "time": "2023-11-24T11:18:31+00:00" + }, + { + "name": "doctrine/doctrine-fixtures-bundle", + "version": "3.6.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/DoctrineFixturesBundle.git", + "reference": "d13a08ebf244f74c8adb8ff15aa55d01c404e534" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/d13a08ebf244f74c8adb8ff15aa55d01c404e534", + "reference": "d13a08ebf244f74c8adb8ff15aa55d01c404e534", + "shasum": "" + }, + "require": { + "doctrine/data-fixtures": "^1.3", + "doctrine/doctrine-bundle": "^2.2", + "doctrine/orm": "^2.14.0 || ^3.0", + "doctrine/persistence": "^2.4|^3.0", + "php": "^7.4 || ^8.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/doctrine-bridge": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0" + }, + "conflict": { + "doctrine/dbal": "< 3" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10.39", + "phpunit/phpunit": "^9.6.13", + "symfony/phpunit-bridge": "^6.3.6", + "vimeo/psalm": "^5.15" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Doctrine\\Bundle\\FixturesBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Doctrine Project", + "homepage": "https://www.doctrine-project.org" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony DoctrineFixturesBundle", + "homepage": "https://www.doctrine-project.org", + "keywords": [ + "Fixture", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", + "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.6.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-fixtures-bundle", + "type": "tidelift" + } + ], + "time": "2024-05-07T07:16:35+00:00" + }, + { "name": "nikic/php-parser", "version": "v5.1.0", "source": { diff --git a/config/bundles.php b/config/bundles.php index 030aab0..5aab971 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -10,4 +10,6 @@ return [ Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true], Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true], + Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], ]; diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 63e1192..1cb977e 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -6,6 +6,8 @@ twig: menu: header: - { route: 'app_project', label: 'Projets' } + taskLifecycleManager: '@App\Service\TaskLifecycleManager' + geoJsonManager: '@App\Service\GeoJsonManager' when@test: twig: diff --git a/config/packages/workflow.yaml b/config/packages/workflow.yaml new file mode 100644 index 0000000..1407231 --- /dev/null +++ b/config/packages/workflow.yaml @@ -0,0 +1,50 @@ +framework: + workflows: + task_lifecycle: + type: state_machine + audit_trail: + enabled: true + marking_store: + type: 'method' + property: 'status' + supports: + - App\Entity\Task + initial_marking: !php/const App\Entity\Task::STATUS_TODO + places: + !php/const App\Entity\Task::STATUS_TODO: + metadata: + title: 'À faire' + color: danger + !php/const App\Entity\Task::STATUS_DOING: + metadata: + title: 'En cours' + color: warning + !php/const App\Entity\Task::STATUS_DONE: + metadata: + title: 'Terminé' + color: success + transitions: + !php/const App\Entity\Task::TRANSITION_START: + from: !php/const App\Entity\Task::STATUS_TODO + to: !php/const App\Entity\Task::STATUS_DOING + metadata: + title: 'Commencer la tâche' + route: 'app_task_start' + !php/const App\Entity\Task::TRANSITION_FINISH: + from: !php/const App\Entity\Task::STATUS_DOING + to: !php/const App\Entity\Task::STATUS_DONE + metadata: + title: 'Terminer la tâche' + route: 'app_task_finish' + !php/const App\Entity\Task::TRANSITION_CANCEL: + from: !php/const App\Entity\Task::STATUS_DOING + to: !php/const App\Entity\Task::STATUS_TODO + metadata: + title: 'Abandonner la tâche' + route: 'app_task_cancel' + !php/const App\Entity\Task::TRANSITION_RESET: + from: !php/const App\Entity\Task::STATUS_DONE + to: !php/const App\Entity\Task::STATUS_TODO + metadata: + title: 'Recommencer la tâche' + route: 'app_task_reset' diff --git a/importmap.php b/importmap.php index 953700b..506dd51 100644 --- a/importmap.php +++ b/importmap.php @@ -26,4 +26,17 @@ return [ 'version' => '5.3.3', 'type' => 'css', ], + 'leaflet' => [ + 'version' => '1.9.4', + ], + 'leaflet/dist/leaflet.min.css' => [ + 'version' => '1.9.4', + 'type' => 'css', + ], + '@hotwired/stimulus' => [ + 'version' => '3.2.2', + ], + '@symfony/stimulus-bundle' => [ + 'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js', + ], ]; diff --git a/migrations/Version20240720132139.php b/migrations/Version20240720132139.php deleted file mode 100644 index 527c1d8..0000000 --- a/migrations/Version20240720132139.php +++ /dev/null @@ -1,31 +0,0 @@ -addSql('CREATE TABLE project (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, description CLOB DEFAULT NULL)'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('DROP TABLE project'); - } -} diff --git a/migrations/Version20240720143743.php b/migrations/Version20240720143743.php deleted file mode 100644 index 3af2ce4..0000000 --- a/migrations/Version20240720143743.php +++ /dev/null @@ -1,40 +0,0 @@ -addSql('CREATE TEMPORARY TABLE __temp__project AS SELECT id, name, description FROM project'); - $this->addSql('DROP TABLE project'); - $this->addSql('CREATE TABLE project (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, description CLOB DEFAULT NULL, slug VARCHAR(255) NOT NULL)'); - $this->addSql('INSERT INTO project (id, name, description) SELECT id, name, description FROM __temp__project'); - $this->addSql('DROP TABLE __temp__project'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_2FB3D0EE989D9B62 ON project (slug)'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('CREATE TEMPORARY TABLE __temp__project AS SELECT id, name, description FROM project'); - $this->addSql('DROP TABLE project'); - $this->addSql('CREATE TABLE project (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, description CLOB DEFAULT NULL)'); - $this->addSql('INSERT INTO project (id, name, description) SELECT id, name, description FROM __temp__project'); - $this->addSql('DROP TABLE __temp__project'); - } -} diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index e3c0089..f2b50e1 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -67,11 +67,11 @@ class ProjectController extends AbstractController ]); } - #[Route('/show/{slug}', name: 'app_project_show')] - public function show(EntityManagerInterface $entityManager, $slug): Response + #[Route('/show/{projectSlug}', name: 'app_project_show')] + public function show(EntityManagerInterface $entityManager, $projectSlug): Response { $repository = $entityManager->getRepository(Project::class); - $project = $repository->findOneBySlug($slug); + $project = $repository->findOneBySlug($projectSlug); if (!$project) { $this->addFlash( @@ -82,16 +82,22 @@ class ProjectController extends AbstractController return $this->redirectToRoute('app_project'); } + $tasks = count($project->getTasks()); + $this->addFlash( + 'info', + sprintf('%s tâche%s trouvée%s', $tasks, ($tasks > 1 ? 's' : ''), ($tasks > 1 ? 's' : '')) + ); + return $this->render('project/show.html.twig', [ 'project' => $project, ]); } - #[Route('/update/{slug}', name: 'app_project_update')] - public function update(Request $request, EntityManagerInterface $entityManager, $slug): Response + #[Route('/update/{projectSlug}', name: 'app_project_update')] + public function update(Request $request, EntityManagerInterface $entityManager, $projectSlug): Response { $repository = $entityManager->getRepository(Project::class); - $project = $repository->findOneBySlug($slug); + $project = $repository->findOneBySlug($projectSlug); if (!$project) { $this->addFlash( @@ -135,11 +141,11 @@ class ProjectController extends AbstractController ]); } - #[Route('/remove/{slug}', name: 'app_project_remove')] - public function remove(EntityManagerInterface $entityManager, $slug): Response + #[Route('/remove/{projectSlug}', name: 'app_project_remove')] + public function remove(EntityManagerInterface $entityManager, $projectSlug): Response { $repository = $entityManager->getRepository(Project::class); - $project = $repository->findOneBySlug($slug); + $project = $repository->findOneBySlug($projectSlug); if (!$project) { $this->addFlash( diff --git a/src/Controller/TaskController.php b/src/Controller/TaskController.php new file mode 100644 index 0000000..d88fc03 --- /dev/null +++ b/src/Controller/TaskController.php @@ -0,0 +1,327 @@ +getRepository(Project::class); + $project = $repository->findOneBySlug($projectSlug); + + if (!$project) { + $this->addFlash( + 'warning', + 'Projet non trouvé !' + ); + + return $this->redirectToRoute('app_project'); + } + + $task = new Task(); + $task->setProject($project); + $createForm = $this->createForm(TaskType::class, $task); + $createForm->add('submit', SubmitType::class, [ + 'label' => 'Créer', + ]); + + $createForm->handleRequest($request); + if ($createForm->isSubmitted() and $createForm->isValid()) { + $task = $createForm->getData(); + + try { + $entityManager->persist($task); + $entityManager->flush(); + + $this->addFlash( + 'success', + 'Tâche créée !' + ); + + return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]); + } catch (\Exception $exception) { + $this->addFlash( + 'danger', + 'Impossible de créer la tâche !' + ); + } + } + + + return $this->render('task/create.html.twig', [ + 'project' => $project, + 'create_form' => $createForm, + ]); + } + + #[Route('/{projectSlug}/show/{taskSlug}', name: 'app_task_show')] + public function show(EntityManagerInterface $entityManager, GeoJsonManager $geoJsonManager, $projectSlug, $taskSlug): Response + { + $repository = $entityManager->getRepository(Task::class); + $task = $repository->findOneBySlug($taskSlug); + + if (!$task) { + $this->addFlash( + 'warning', + 'Tâche non trouvée !' + ); + + return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]); + } + + $comment = new Comment(); + $commentForm = $this->createForm(CommentType::class, $comment, [ + 'action' => $this->generateUrl('app_task_comment', ['projectSlug' => $projectSlug, 'taskSlug' => $taskSlug]), + ]); + $commentForm->add('submit', SubmitType::class, [ + 'label' => 'Commenter', + ]); + + return $this->render('task/show.html.twig', [ + 'task' => $task, + 'project' => $task->getProject(), + 'commentForm' => $commentForm, + ]); + } + + #[Route('/{projectSlug}/comment/{taskSlug}', name: 'app_task_comment')] + public function comment(Request $request, EntityManagerInterface $entityManager, $projectSlug, $taskSlug): Response + { + $repository = $entityManager->getRepository(Task::class); + $task = $repository->findOneBySlug($taskSlug); + + if (!$task) { + $this->addFlash( + 'warning', + 'Tâche non trouvée !' + ); + + return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]); + } + + $comment = new Comment(); + $comment->setTask($task); + $commentForm = $this->createForm(CommentType::class, $comment); + $commentForm->add('submit', SubmitType::class, [ + 'label' => 'Commenter', + ]); + + $commentForm->handleRequest($request); + if ($commentForm->isSubmitted() and $commentForm->isValid()) { + $comment = $commentForm->getData(); + + try { + $entityManager->persist($comment); + $entityManager->flush(); + + $this->addFlash( + 'success', + 'Commentaire ajouté !' + ); + } catch (\Exception $exception) { + $this->addFlash( + 'danger', + 'Impossible de commenter !' + ); + } + } + + return $this->redirectToRoute('app_task_show', ['projectSlug' => $projectSlug, 'taskSlug' => $taskSlug]); + } + + #[Route('/{projectSlug}/update/{taskSlug}', name: 'app_task_update')] + public function update(Request $request, EntityManagerInterface $entityManager, $projectSlug, $taskSlug): Response + { + $repository = $entityManager->getRepository(Task::class); + $task = $repository->findOneBySlug($taskSlug); + + if (!$task) { + $this->addFlash( + 'warning', + 'Tâche non trouvée !' + ); + + return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]); + } + + $updateForm = $this->createForm(TaskType::class, $task); + $updateForm->add('submit', SubmitType::class, [ + 'label' => 'Modifier', + ]); + + $updateForm->handleRequest($request); + if ($updateForm->isSubmitted() and $updateForm->isValid()) { + $task = $updateForm->getData(); + + try { + $entityManager->persist($task); + $entityManager->flush(); + + $this->addFlash( + 'success', + 'Tâche modifiée !' + ); + + return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]); + } catch (\Exception $exception) { + $this->addFlash( + 'danger', + 'Impossible de modifier la tâche !' + ); + } + } + + return $this->render('task/update.html.twig', [ + 'project' => $task->getProject(), + 'task' => $task, + 'update_form' => $updateForm, + ]); + } + + #[Route('/{projectSlug}/remove/{taskSlug}', name: 'app_task_remove')] + public function remove(EntityManagerInterface $entityManager, $projectSlug, $taskSlug): Response + { + $repository = $entityManager->getRepository(Task::class); + $task = $repository->findOneBySlug($taskSlug); + + if (!$task) { + $this->addFlash( + 'warning', + 'Tâche non trouvée !' + ); + return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]); + } + + try { + $entityManager->remove($task); + $entityManager->flush(); + + $this->addFlash( + 'success', + 'Tâche supprimée !' + ); + + return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]); + } catch (\Exception $exception) { + $this->addFlash( + 'danger', + 'Impossible de supprimer la tâche !' + ); + } + + return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]); + } + + private function transition(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $projectSlug, $taskSlug, $status): Response + { + $repository = $entityManager->getRepository(Task::class); + $task = $repository->findOneBySlug($taskSlug); + + if (!$task) { + $this->addFlash( + 'warning', + 'Tâche non trouvée !' + ); + return $this->redirectToRoute('app_project_show', ['projectSlug' => $projectSlug]); + } + + try { + $taskLifecycleStateMachine->apply($task, $status); + + $entityManager->persist($task); + $entityManager->flush(); + + $this->addFlash( + 'success', + 'La tâche est modifiée !' + ); + } catch (Exception $exception) { + $this->addFlash( + 'warning', + 'Impossible de modifier la tâche !' + ); + } + + return $this->redirectToRoute('app_task_show', ['projectSlug' => $projectSlug, 'taskSlug' => $taskSlug]); + } + + #[Route('/{projectSlug}/start/{taskSlug}', name: 'app_task_start')] + public function start(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $projectSlug, $taskSlug): Response + { + return $this->transition($taskLifecycleStateMachine, $entityManager, $projectSlug, $taskSlug, Task::TRANSITION_START); + } + + #[Route('/{projectSlug}/finish/{taskSlug}', name: 'app_task_finish')] + public function finish(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $projectSlug, $taskSlug): Response + { + return $this->transition($taskLifecycleStateMachine, $entityManager, $projectSlug, $taskSlug, Task::TRANSITION_FINISH); + } + + #[Route('/{projectSlug}/cancel/{taskSlug}', name: 'app_task_cancel')] + public function cancel(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $projectSlug, $taskSlug): Response + { + return $this->transition($taskLifecycleStateMachine, $entityManager, $projectSlug, $taskSlug, Task::TRANSITION_CANCEL); + } + + #[Route('/{projectSlug}/reset/{taskSlug}', name: 'app_task_reset')] + public function reset(WorkflowInterface $taskLifecycleStateMachine, EntityManagerInterface $entityManager, $projectSlug, $taskSlug): Response + { + return $this->transition($taskLifecycleStateMachine, $entityManager, $projectSlug, $taskSlug, Task::TRANSITION_RESET); + } + + #[Route('/{slug}.geojson', name: 'app_task_geojson')] + public function geojson(Request $request, EntityManagerInterface $entityManager, $slug): Response + { + $repository = $entityManager->getRepository(Task::class); + $task = $repository->findOneBySlug($slug); + + if (!$task) { + $this->addFlash( + 'warning', + 'Tâche non trouvée !' + ); + return $this->redirect($request->headers->get('referer')); + } + + return JsonResponse::fromJsonString($task->getGeojson()); + } + + #[Route('/{slug}.osm', name: 'app_task_osm')] + public function osm(Request $request, EntityManagerInterface $entityManager, $slug): Response + { + $repository = $entityManager->getRepository(Task::class); + $task = $repository->findOneBySlug($slug); + + if (!$task) { + $this->addFlash( + 'warning', + 'Tâche non trouvée !' + ); + return $this->redirect($request->headers->get('referer')); + } + + $xml = new \DOMDocument(); + $xml->loadXml($task->getOsm()); + $response = new Response($xml->saveXML()); + $response->headers->set('Content-Type', 'application/xml'); + return $response; + } + +} diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php new file mode 100644 index 0000000..0cfe8e6 --- /dev/null +++ b/src/DataFixtures/AppFixtures.php @@ -0,0 +1,1439 @@ +setName('test'); + $manager->persist($tag); + + $task = new Task(); + $task->setName('PR52 D’un canal à l’autre'); + $task->setDescription('boucle au départ du port de Plaisance'); + $task->setStatus(Task::STATUS_TODO); + $task->setOsm(''); + $task->setGeojson(<<persist($task); + + $project = new Project(); + $project->setName('Sentiers PR Gard'); + $project->setDescription('Cf '); + $project->addTag($tag); + $project->addTask($task); + $manager->persist($project); + + $manager->flush(); + } +} diff --git a/src/Entity/Comment.php b/src/Entity/Comment.php new file mode 100644 index 0000000..4e7717d --- /dev/null +++ b/src/Entity/Comment.php @@ -0,0 +1,52 @@ +id; + } + + public function getContent(): ?string + { + return $this->content; + } + + public function setContent(string $content): static + { + $this->content = $content; + + return $this; + } + + public function getTask(): ?Task + { + return $this->task; + } + + public function setTask(?Task $task): static + { + $this->task = $task; + + return $this; + } +} diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 53a5c20..8bc3383 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -3,6 +3,8 @@ namespace App\Entity; use App\Repository\ProjectRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Gedmo\Mapping\Annotation as Gedmo; @@ -25,6 +27,24 @@ class Project #[Gedmo\Slug(fields: ['name'])] private ?string $slug = null; + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'projects')] + private Collection $tags; + + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Task::class, mappedBy: 'project', orphanRemoval: true)] + private Collection $tasks; + + public function __construct() + { + $this->tags = new ArrayCollection(); + $this->tasks = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; @@ -59,4 +79,58 @@ class Project return $this->slug; } + /** + * @return Collection + */ + public function getTags(): Collection + { + return $this->tags; + } + + public function addTag(Tag $tag): static + { + if (!$this->tags->contains($tag)) { + $this->tags->add($tag); + } + + return $this; + } + + public function removeTag(Tag $tag): static + { + $this->tags->removeElement($tag); + + return $this; + } + + /** + * @return Collection + */ + public function getTasks(): Collection + { + return $this->tasks; + } + + public function addTask(Task $task): static + { + if (!$this->tasks->contains($task)) { + $this->tasks->add($task); + $task->setProject($this); + } + + return $this; + } + + public function removeTask(Task $task): static + { + if ($this->tasks->removeElement($task)) { + // set the owning side to null (unless already changed) + if ($task->getProject() === $this) { + $task->setProject(null); + } + } + + return $this; + } + } diff --git a/src/Entity/Tag.php b/src/Entity/Tag.php new file mode 100644 index 0000000..9c9379c --- /dev/null +++ b/src/Entity/Tag.php @@ -0,0 +1,75 @@ + + */ + #[ORM\ManyToMany(targetEntity: Project::class, mappedBy: 'tags')] + private Collection $projects; + + public function __construct() + { + $this->projects = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + /** + * @return Collection + */ + public function getProjects(): Collection + { + return $this->projects; + } + + public function addProject(Project $project): static + { + if (!$this->projects->contains($project)) { + $this->projects->add($project); + $project->addTag($this); + } + + return $this; + } + + public function removeProject(Project $project): static + { + if ($this->projects->removeElement($project)) { + $project->removeTag($this); + } + + return $this; + } +} diff --git a/src/Entity/Task.php b/src/Entity/Task.php new file mode 100644 index 0000000..b324e5d --- /dev/null +++ b/src/Entity/Task.php @@ -0,0 +1,204 @@ + + */ + #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'task', orphanRemoval: true)] + private Collection $comments; + + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $description = null; + + public function __construct() + { + $this->comments = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function getSlug(): ?string + { + return $this->slug; + } + + public function getUrgent(): ?int + { + return $this->urgent; + } + + public function setUrgent(?int $urgent): static + { + $this->urgent = $urgent; + + return $this; + } + + public function getImportant(): ?int + { + return $this->important; + } + + public function setImportant(?int $important): static + { + $this->important = $important; + + return $this; + } + + public function getStatus(): ?string + { + return $this->status; + } + + public function setStatus($status, array $context = []): static + { + $this->status = $status; + + return $this; + } + + public function getProject(): ?Project + { + return $this->project; + } + + public function setProject(?Project $project): static + { + $this->project = $project; + + return $this; + } + + public function getGeojson(): ?string + { + return $this->geojson; + } + + public function setGeojson(string $geojson): static + { + $this->geojson = $geojson; + + return $this; + } + + public function getOsm(): ?string + { + return $this->osm; + } + + public function setOsm(string $osm): static + { + $this->osm = $osm; + + return $this; + } + + /** + * @return Collection + */ + public function getComments(): Collection + { + return $this->comments; + } + + public function addComment(Comment $comment): static + { + if (!$this->comments->contains($comment)) { + $this->comments->add($comment); + $comment->setTask($this); + } + + return $this; + } + + public function removeComment(Comment $comment): static + { + if ($this->comments->removeElement($comment)) { + // set the owning side to null (unless already changed) + if ($comment->getTask() === $this) { + $comment->setTask(null); + } + } + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + + return $this; + } +} diff --git a/src/Form/CommentType.php b/src/Form/CommentType.php new file mode 100644 index 0000000..8571199 --- /dev/null +++ b/src/Form/CommentType.php @@ -0,0 +1,27 @@ +add('content', null, ['label' => false]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Comment::class, + ]); + } +} diff --git a/src/Form/ProjectType.php b/src/Form/ProjectType.php index 07d8809..d896c5d 100644 --- a/src/Form/ProjectType.php +++ b/src/Form/ProjectType.php @@ -3,6 +3,8 @@ namespace App\Form; use App\Entity\Project; +use App\Entity\Tag; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -12,12 +14,14 @@ class ProjectType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { $builder - ->add('name', null, [ - 'label' => 'Nom', - ]) - ->add('description', null, [ - 'label' => 'Description', - 'help' => 'On peut mettre du Markdown ici…', + ->add('name', null, ['label' => 'Nom']) + ->add('description', null, ['label' => 'Description']) + ->add('tags', EntityType::class, [ + 'label' => 'Étiquettes', + 'class' => Tag::class, + 'choice_label' => 'name', + 'multiple' => true, + 'expanded' => true, ]) ; } diff --git a/src/Form/TaskLifecycleType.php b/src/Form/TaskLifecycleType.php new file mode 100644 index 0000000..8d1b338 --- /dev/null +++ b/src/Form/TaskLifecycleType.php @@ -0,0 +1,42 @@ +taskLifecycleStateMachine->getDefinition()->getPlaces(); + $metas = $this->taskLifecycleStateMachine->getMetadataStore(); + + foreach ($places as $place => $id) { + $meta = $metas->getPlaceMetadata($place); + if (isset($meta['title'])) { + $choices[$meta['title']] = $place; + } + } + + $resolver->setDefaults([ + 'choices' => $choices, + ]); + } + + public function getParent(): string + { + return ChoiceType::class; + } +} diff --git a/src/Form/TaskType.php b/src/Form/TaskType.php new file mode 100644 index 0000000..8cb9cc5 --- /dev/null +++ b/src/Form/TaskType.php @@ -0,0 +1,33 @@ +add('name', null, ['label' => 'Nom']) + ->add('description', null, ['label' => 'Description']) + ->add('geojson', TextareaType::class, ['label' => 'GeoJSON']) + ->add('osm', TextareaType::class, ['label' => 'OSM']) + ->add('status', TaskLifecycleType::class, ['label' => 'État']) + ->add('urgent', null, ['label' => 'Urgence']) + ->add('important', null, ['label' => 'Importance']) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Task::class, + ]); + } +} diff --git a/src/Repository/CommentRepository.php b/src/Repository/CommentRepository.php new file mode 100644 index 0000000..47ea76e --- /dev/null +++ b/src/Repository/CommentRepository.php @@ -0,0 +1,43 @@ + + */ +class CommentRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Comment::class); + } + + // /** + // * @return Comment[] Returns an array of Comment objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('c') + // ->andWhere('c.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('c.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Comment + // { + // return $this->createQueryBuilder('c') + // ->andWhere('c.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Repository/TagRepository.php b/src/Repository/TagRepository.php new file mode 100644 index 0000000..1b3c116 --- /dev/null +++ b/src/Repository/TagRepository.php @@ -0,0 +1,43 @@ + + */ +class TagRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Tag::class); + } + + // /** + // * @return Tag[] Returns an array of Tag objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('t.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Tag + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Repository/TaskRepository.php b/src/Repository/TaskRepository.php new file mode 100644 index 0000000..97061ee --- /dev/null +++ b/src/Repository/TaskRepository.php @@ -0,0 +1,43 @@ + + */ +class TaskRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Task::class); + } + + // /** + // * @return Task[] Returns an array of Task objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('t.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?Task + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Service/GeoJsonManager.php b/src/Service/GeoJsonManager.php new file mode 100644 index 0000000..91788d8 --- /dev/null +++ b/src/Service/GeoJsonManager.php @@ -0,0 +1,48 @@ +getGeojson(), true); + $data['features'][0]['properties'] = array_merge($data['features'][0]['properties'], [ + 'name' => $task->getName(), + 'url' => $this->router->generate('app_task_show', ['projectSlug' => $task->getProject()->getSlug(), 'taskSlug' => $task->getSlug()], UrlGeneratorInterface::ABSOLUTE_URL), + 'color' => $this->taskLifecycleStateMachine->getMetadataStore()->getPlaceMetadata($task->getStatus())['color'], + ]); + return $data; + } + + public function generateGeoJson($entity) + { + $geoJsons = []; + + if ($entity instanceof Task) { + $task = $entity; + $geoJsons[] = GeoJson::jsonUnserialize($this->getFullGeojson($task)); + } elseif ($entity instanceof Project) { + $project = $entity; + foreach ($project->getTasks() as $task) { + $geoJsons[] = GeoJson::jsonUnserialize($this->getFullGeojson($task)); + } + } + + return $geoJsons; + } + +} diff --git a/src/Service/TaskLifecycleManager.php b/src/Service/TaskLifecycleManager.php new file mode 100644 index 0000000..d1176a5 --- /dev/null +++ b/src/Service/TaskLifecycleManager.php @@ -0,0 +1,55 @@ +taskLifecycleStateMachine->getDefinition()->getPlaces(); + $metas = $this->taskLifecycleStateMachine->getMetadataStore(); + + if (empty($places)) { + return $stats; + } + + foreach ($places as $place => $id) { + $stats[$place] = array_merge($metas->getPlaceMetadata($place), [ + 'value' => 0, + ]); + } + + $max = 0; + foreach ($project->getTasks() as $task) + { + $stats[$task->getStatus()]['value'] += 1; + $max += 1; + } + + if ($max === 0) { + return $stats; + } + + $stats = array_map(function ($data) use ($max) { + $data['max'] = $max; + $data['percentage'] = ((float) $data['value'] * 100.0) / $max; + + return $data; + }, $stats); + + return $stats; + } + + +} diff --git a/symfony.lock b/symfony.lock index 96fa5bc..8491c85 100644 --- a/symfony.lock +++ b/symfony.lock @@ -13,6 +13,18 @@ "src/Repository/.gitignore" ] }, + "doctrine/doctrine-fixtures-bundle": { + "version": "3.6", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.0", + "ref": "1f5514cfa15b947298df4d771e694e578d4c204d" + }, + "files": [ + "src/DataFixtures/AppFixtures.php" + ] + }, "doctrine/doctrine-migrations-bundle": { "version": "3.3", "recipe": { @@ -130,6 +142,20 @@ "config/routes.yaml" ] }, + "symfony/stimulus-bundle": { + "version": "2.18", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.13", + "ref": "6acd9ff4f7fd5626d2962109bd4ebab351d43c43" + }, + "files": [ + "assets/bootstrap.js", + "assets/controllers.json", + "assets/controllers/hello_controller.js" + ] + }, "symfony/twig-bundle": { "version": "7.1", "recipe": { @@ -168,6 +194,18 @@ "config/routes/web_profiler.yaml" ] }, + "symfony/workflow": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.3", + "ref": "3b2f8ca32a07fcb00f899649053943fa3d8bbfb6" + }, + "files": [ + "config/packages/workflow.yaml" + ] + }, "twig/extra-bundle": { "version": "v3.10.0" } diff --git a/templates/project/index.html.twig b/templates/project/index.html.twig index 1a0ae5f..01c41c7 100644 --- a/templates/project/index.html.twig +++ b/templates/project/index.html.twig @@ -19,9 +19,14 @@
-

{{ project.name }}

+

+ {{ project.name }} + {% for tag in project.tags %} + {{ tag.name }} + {% endfor %} +

{% if project.description %}

{{ project.description|markdown_to_html }}

{% endif %} - Voir + Voir
diff --git a/templates/project/show.html.twig b/templates/project/show.html.twig index 6cbf494..bdb9109 100644 --- a/templates/project/show.html.twig +++ b/templates/project/show.html.twig @@ -2,20 +2,78 @@ {% block breadcrumb %} - + {% endblock %} -{% block page_title %}Projet {{ project.name }}{% endblock %} +{% block page_title %} + {{ project.name }} + {% for tag in project.tags %} + {{ tag.name }} + {% endfor %} +{% endblock %} {% block page_content %} +{% if project.description is not empty %}
{{ project.description|markdown_to_html }}
+{% endif %} + +

Carte

+
+
+
+
+
+ +{% if project.tasks is not empty %} +
+
+
+ {% set stats = taskLifecycleManager.getProjectStats(project) %} + {% for place, data in stats %} +
+
{{ data.percentage|format_number({fraction_digit: 0}) ~ '%' }}
+
+ {% endfor %} +
+
+
+ +
+
+ + + + + + + + + + + + {% for task in project.tasks %} + + + + + + + + {% endfor %} + +
IdentifiantNomÉtatImportanceUrgence
{{ task.id }}{{ task.name }}{{ workflow_metadata(task, 'title', task.status) }}{{ task.important }}{{ task.urgent }}
+
+
+{% endif %} {% endblock %} diff --git a/templates/task/create.html.twig b/templates/task/create.html.twig new file mode 100644 index 0000000..17170bf --- /dev/null +++ b/templates/task/create.html.twig @@ -0,0 +1,13 @@ +{% extends 'base.html.twig' %} + +{% block breadcrumb %} + + + +{% endblock %} + +{% block page_title %}Créer une nouvelle tâche{% endblock %} + +{% block page_content %} +{{ form(create_form) }} +{% endblock %} diff --git a/templates/task/show.html.twig b/templates/task/show.html.twig new file mode 100644 index 0000000..f0f28cc --- /dev/null +++ b/templates/task/show.html.twig @@ -0,0 +1,60 @@ +{% extends 'base.html.twig' %} + +{% block breadcrumb %} + + + +{% endblock %} + +{% block page_title %} + {{ task.name }} + {{ workflow_metadata(task, 'title', task.status) }} +{% endblock %} + +{% block page_content %} +
+
+ Revenir au projet + Modifier la tâche + Supprimer la tâche + Télécharger GeoJSON + {% for transition in workflow_transitions(task) %} + {{ workflow_metadata(task, 'title', transition) }} + {% endfor %} + +
+
+ +{% if task.description is not empty %} +

Description

+
+
{{ task.description|markdown_to_html }}
+
+{% endif %} + +

Carte

+
+
+
+
+
+ + +

Commentaires

+{% if task.comments is not empty %} +
+
+ {% for comment in task.comments %} +
+ {{ comment.content|markdown_to_html }} +
+ {% endfor %} +
+
+{% endif %} +
+
+ {{ form(commentForm) }} +
+
+{% endblock %} diff --git a/templates/task/update.html.twig b/templates/task/update.html.twig new file mode 100644 index 0000000..f8606b1 --- /dev/null +++ b/templates/task/update.html.twig @@ -0,0 +1,13 @@ +{% extends 'base.html.twig' %} + +{% block breadcrumb %} + + + +{% endblock %} + +{% block page_title %}Modifier la tâche {{ task.name }}{% endblock %} + +{% block page_content %} +{{ form(update_form) }} +{% endblock %}