Browse Source

wip

master
vincent 3 months ago
parent
commit
5c165d3879
36 changed files with 3434 additions and 95 deletions
  1. +7
    -2
      assets/app.js
  2. +5
    -0
      assets/bootstrap.js
  3. +4
    -0
      assets/controllers.json
  4. +44
    -0
      assets/controllers/josm_controller.js
  5. +39
    -0
      assets/controllers/map_controller.js
  6. +3
    -0
      assets/styles/app.css
  7. +5
    -0
      composer.json
  8. +537
    -1
      composer.lock
  9. +2
    -0
      config/bundles.php
  10. +2
    -0
      config/packages/twig.yaml
  11. +50
    -0
      config/packages/workflow.yaml
  12. +13
    -0
      importmap.php
  13. +0
    -31
      migrations/Version20240720132139.php
  14. +0
    -40
      migrations/Version20240720143743.php
  15. +15
    -9
      src/Controller/ProjectController.php
  16. +327
    -0
      src/Controller/TaskController.php
  17. +1439
    -0
      src/DataFixtures/AppFixtures.php
  18. +52
    -0
      src/Entity/Comment.php
  19. +74
    -0
      src/Entity/Project.php
  20. +75
    -0
      src/Entity/Tag.php
  21. +204
    -0
      src/Entity/Task.php
  22. +27
    -0
      src/Form/CommentType.php
  23. +10
    -6
      src/Form/ProjectType.php
  24. +42
    -0
      src/Form/TaskLifecycleType.php
  25. +33
    -0
      src/Form/TaskType.php
  26. +43
    -0
      src/Repository/CommentRepository.php
  27. +43
    -0
      src/Repository/TagRepository.php
  28. +43
    -0
      src/Repository/TaskRepository.php
  29. +48
    -0
      src/Service/GeoJsonManager.php
  30. +55
    -0
      src/Service/TaskLifecycleManager.php
  31. +38
    -0
      symfony.lock
  32. +7
    -2
      templates/project/index.html.twig
  33. +62
    -4
      templates/project/show.html.twig
  34. +13
    -0
      templates/task/create.html.twig
  35. +60
    -0
      templates/task/show.html.twig
  36. +13
    -0
      templates/task/update.html.twig

+ 7
- 2
assets/app.js View File

@ -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 './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))

+ 5
- 0
assets/bootstrap.js View File

@ -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);

+ 4
- 0
assets/controllers.json View File

@ -0,0 +1,4 @@
{
"controllers": [],
"entrypoints": []
}

+ 44
- 0
assets/controllers/josm_controller.js View File

@ -0,0 +1,44 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static values = {
importurl: String,
layername: String,
}
remoteControl() {
// cf <https://josm.openstreetmap.de/wiki/Help/RemoteControlCommands>
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 <https://josm.openstreetmap.de/wiki/Maps>
}));
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);
});
});
}
}

+ 39
- 0
assets/controllers/map_controller.js View File

@ -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: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).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());
}
}
}

+ 3
- 0
assets/styles/app.css View File

@ -0,0 +1,3 @@
.min-vh-50 {
min-height: 50vh;
}

+ 5
- 0
composer.json View File

@ -11,6 +11,7 @@
"doctrine/doctrine-bundle": "^2.12", "doctrine/doctrine-bundle": "^2.12",
"doctrine/doctrine-migrations-bundle": "^3.3", "doctrine/doctrine-migrations-bundle": "^3.3",
"doctrine/orm": "^3.2", "doctrine/orm": "^3.2",
"jmikola/geojson": "^1.0",
"league/commonmark": "^2.4", "league/commonmark": "^2.4",
"league/html-to-markdown": "^5.1", "league/html-to-markdown": "^5.1",
"stof/doctrine-extensions-bundle": "^1.12", "stof/doctrine-extensions-bundle": "^1.12",
@ -22,14 +23,18 @@
"symfony/form": "7.1.*", "symfony/form": "7.1.*",
"symfony/framework-bundle": "7.1.*", "symfony/framework-bundle": "7.1.*",
"symfony/runtime": "7.1.*", "symfony/runtime": "7.1.*",
"symfony/stimulus-bundle": "^2.18",
"symfony/twig-bundle": "7.1.*", "symfony/twig-bundle": "7.1.*",
"symfony/validator": "7.1.*", "symfony/validator": "7.1.*",
"symfony/workflow": "7.1.*",
"symfony/yaml": "7.1.*", "symfony/yaml": "7.1.*",
"twig/extra-bundle": "^2.12|^3.0", "twig/extra-bundle": "^2.12|^3.0",
"twig/intl-extra": "^3.10",
"twig/markdown-extra": "^3.10", "twig/markdown-extra": "^3.10",
"twig/twig": "^2.12|^3.0" "twig/twig": "^2.12|^3.0"
}, },
"require-dev": { "require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.6",
"symfony/debug-bundle": "7.1.*", "symfony/debug-bundle": "7.1.*",
"symfony/maker-bundle": "^1.60", "symfony/maker-bundle": "^1.60",
"symfony/stopwatch": "7.1.*", "symfony/stopwatch": "7.1.*",


+ 537
- 1
composer.lock View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "9ebc747abf5efb362443c7d1a30b2086",
"content-hash": "ed07bf1e98cc998eddcf49958cb2835f",
"packages": [ "packages": [
{ {
"name": "behat/transliterator", "name": "behat/transliterator",
@ -1655,6 +1655,65 @@
"time": "2024-06-25T16:22:14+00:00" "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", "name": "league/commonmark",
"version": "2.4.2", "version": "2.4.2",
"source": { "source": {
@ -4261,6 +4320,92 @@
"time": "2024-06-28T13:13:31+00:00" "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", "name": "symfony/options-resolver",
"version": "v7.1.1", "version": "v7.1.1",
"source": { "source": {
@ -5130,6 +5275,75 @@
"time": "2024-04-18T09:32:20+00:00" "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", "name": "symfony/stopwatch",
"version": "v7.1.1", "version": "v7.1.1",
"source": { "source": {
@ -5888,6 +6102,93 @@
"time": "2024-06-28T08:00:31+00:00" "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", "name": "symfony/yaml",
"version": "v7.1.1", "version": "v7.1.1",
"source": { "source": {
@ -6033,6 +6334,70 @@
"time": "2024-05-11T07:35:57+00:00" "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", "name": "twig/markdown-extra",
"version": "v3.10.0", "version": "v3.10.0",
"source": { "source": {
@ -6186,6 +6551,177 @@
], ],
"packages-dev": [ "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", "name": "nikic/php-parser",
"version": "v5.1.0", "version": "v5.1.0",
"source": { "source": {


+ 2
- 0
config/bundles.php View File

@ -10,4 +10,6 @@ return [
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true], Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => 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],
]; ];

+ 2
- 0
config/packages/twig.yaml View File

@ -6,6 +6,8 @@ twig:
menu: menu:
header: header:
- { route: 'app_project', label: 'Projets' } - { route: 'app_project', label: 'Projets' }
taskLifecycleManager: '@App\Service\TaskLifecycleManager'
geoJsonManager: '@App\Service\GeoJsonManager'
when@test: when@test:
twig: twig:


+ 50
- 0
config/packages/workflow.yaml View File

@ -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'

+ 13
- 0
importmap.php View File

@ -26,4 +26,17 @@ return [
'version' => '5.3.3', 'version' => '5.3.3',
'type' => 'css', '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',
],
]; ];

+ 0
- 31
migrations/Version20240720132139.php View File

@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240720132139 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->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');
}
}

+ 0
- 40
migrations/Version20240720143743.php View File

@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20240720143743 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() 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, 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');
}
}

+ 15
- 9
src/Controller/ProjectController.php View File

@ -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); $repository = $entityManager->getRepository(Project::class);
$project = $repository->findOneBySlug($slug);
$project = $repository->findOneBySlug($projectSlug);
if (!$project) { if (!$project) {
$this->addFlash( $this->addFlash(
@ -82,16 +82,22 @@ class ProjectController extends AbstractController
return $this->redirectToRoute('app_project'); 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', [ return $this->render('project/show.html.twig', [
'project' => $project, '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); $repository = $entityManager->getRepository(Project::class);
$project = $repository->findOneBySlug($slug);
$project = $repository->findOneBySlug($projectSlug);
if (!$project) { if (!$project) {
$this->addFlash( $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); $repository = $entityManager->getRepository(Project::class);
$project = $repository->findOneBySlug($slug);
$project = $repository->findOneBySlug($projectSlug);
if (!$project) { if (!$project) {
$this->addFlash( $this->addFlash(


+ 327
- 0
src/Controller/TaskController.php View File

@ -0,0 +1,327 @@
<?php
namespace App\Controller;
use App\Entity\Comment;
use App\Entity\Project;
use App\Entity\Task;
use App\Form\CommentType;
use App\Form\TaskType;
use App\Service\GeoJsonManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Workflow\WorkflowInterface;
#[Route('/task')]
class TaskController extends AbstractController
{
#[Route('/{projectSlug}/create', name: 'app_task_create')]
public function create(Request $request, EntityManagerInterface $entityManager, $projectSlug): Response
{
$repository = $entityManager->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;
}
}

+ 1439
- 0
src/DataFixtures/AppFixtures.php
File diff suppressed because it is too large
View File


+ 52
- 0
src/Entity/Comment.php View File

@ -0,0 +1,52 @@
<?php
namespace App\Entity;
use App\Repository\CommentRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CommentRepository::class)]
class Comment
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $content = null;
#[ORM\ManyToOne(inversedBy: 'comments')]
#[ORM\JoinColumn(nullable: false)]
private ?Task $task = null;
public function getId(): ?int
{
return $this->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;
}
}

+ 74
- 0
src/Entity/Project.php View File

@ -3,6 +3,8 @@
namespace App\Entity; namespace App\Entity;
use App\Repository\ProjectRepository; use App\Repository\ProjectRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo; use Gedmo\Mapping\Annotation as Gedmo;
@ -25,6 +27,24 @@ class Project
#[Gedmo\Slug(fields: ['name'])] #[Gedmo\Slug(fields: ['name'])]
private ?string $slug = null; private ?string $slug = null;
/**
* @var Collection<int, Tag>
*/
#[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'projects')]
private Collection $tags;
/**
* @var Collection<int, Task>
*/
#[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 public function getId(): ?int
{ {
return $this->id; return $this->id;
@ -59,4 +79,58 @@ class Project
return $this->slug; return $this->slug;
} }
/**
* @return Collection<int, Tag>
*/
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<int, Task>
*/
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;
}
} }

+ 75
- 0
src/Entity/Tag.php View File

@ -0,0 +1,75 @@
<?php
namespace App\Entity;
use App\Repository\TagRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TagRepository::class)]
class Tag
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
/**
* @var Collection<int, Project>
*/
#[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<int, Project>
*/
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;
}
}

+ 204
- 0
src/Entity/Task.php View File

@ -0,0 +1,204 @@
<?php
namespace App\Entity;
use App\Repository\TaskRepository;
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;
#[ORM\Entity(repositoryClass: TaskRepository::class)]
class Task
{
const STATUS_TODO = 'todo';
const STATUS_DOING = 'doing';
const STATUS_DONE = 'done';
const TRANSITION_START = 'start';
const TRANSITION_FINISH = 'finish';
const TRANSITION_CANCEL = 'cancel';
const TRANSITION_RESET = 'reset';
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column(length: 255, unique: true)]
#[Gedmo\Slug(fields: ['name'])]
private ?string $slug = null;
#[ORM\Column(nullable: true)]
private ?int $urgent = null;
#[ORM\Column(nullable: true)]
private ?int $important = null;
#[ORM\Column(length: 255)]
private ?string $status = null;
#[ORM\ManyToOne(inversedBy: 'tasks')]
#[ORM\JoinColumn(nullable: false)]
private ?Project $project = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $geojson = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $osm = null;
/**
* @var Collection<int, Comment>
*/
#[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<int, Comment>
*/
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;
}
}

+ 27
- 0
src/Form/CommentType.php View File

@ -0,0 +1,27 @@
<?php
namespace App\Form;
use App\Entity\Comment;
use App\Entity\Task;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class CommentType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('content', null, ['label' => false])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Comment::class,
]);
}
}

+ 10
- 6
src/Form/ProjectType.php View File

@ -3,6 +3,8 @@
namespace App\Form; namespace App\Form;
use App\Entity\Project; use App\Entity\Project;
use App\Entity\Tag;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
@ -12,12 +14,14 @@ class ProjectType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void public function buildForm(FormBuilderInterface $builder, array $options): void
{ {
$builder $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,
]) ])
; ;
} }


+ 42
- 0
src/Form/TaskLifecycleType.php View File

@ -0,0 +1,42 @@
<?php
namespace App\Form;
use App\Entity\Task;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Workflow\WorkflowInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;
class TaskLifecycleType extends AbstractType
{
public function __construct(
private WorkflowInterface $taskLifecycleStateMachine,
) {
}
public function configureOptions(OptionsResolver $resolver): void
{
$choices = [];
$places = $this->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;
}
}

+ 33
- 0
src/Form/TaskType.php View File

@ -0,0 +1,33 @@
<?php
namespace App\Form;
use App\Entity\Task;
use App\Form\TaskLifecycleType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->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,
]);
}
}

+ 43
- 0
src/Repository/CommentRepository.php View File

@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\Comment;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Comment>
*/
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()
// ;
// }
}

+ 43
- 0
src/Repository/TagRepository.php View File

@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\Tag;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Tag>
*/
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()
// ;
// }
}

+ 43
- 0
src/Repository/TaskRepository.php View File

@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\Task;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Task>
*/
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()
// ;
// }
}

+ 48
- 0
src/Service/GeoJsonManager.php View File

@ -0,0 +1,48 @@
<?php
namespace App\Service;
use App\Entity\Project;
use App\Entity\Task;
use GeoJson\GeoJson;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Workflow\WorkflowInterface;
class GeoJsonManager
{
public function __construct(
private UrlGeneratorInterface $router,
private WorkflowInterface $taskLifecycleStateMachine,
) {
}
private function getFullGeoJson(Task $task)
{
$data = json_decode($task->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;
}
}

+ 55
- 0
src/Service/TaskLifecycleManager.php View File

@ -0,0 +1,55 @@
<?php
namespace App\Service;
use App\Entity\Project;
use Symfony\Component\Workflow\WorkflowInterface;
class TaskLifecycleManager
{
public function __construct(
private WorkflowInterface $taskLifecycleStateMachine,
) {
}
public function getProjectStats(Project $project)
{
$stats = [];
$places = $this->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;
}
}

+ 38
- 0
symfony.lock View File

@ -13,6 +13,18 @@
"src/Repository/.gitignore" "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": { "doctrine/doctrine-migrations-bundle": {
"version": "3.3", "version": "3.3",
"recipe": { "recipe": {
@ -130,6 +142,20 @@
"config/routes.yaml" "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": { "symfony/twig-bundle": {
"version": "7.1", "version": "7.1",
"recipe": { "recipe": {
@ -168,6 +194,18 @@
"config/routes/web_profiler.yaml" "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": { "twig/extra-bundle": {
"version": "v3.10.0" "version": "v3.10.0"
} }


+ 7
- 2
templates/project/index.html.twig View File

@ -19,9 +19,14 @@
<div class="col col-md-4 mb-3"> <div class="col col-md-4 mb-3">
<div class="card h-100"> <div class="card h-100">
<div class="card-body"> <div class="card-body">
<h2 class="card-title">{{ project.name }}</h2>
<h2 class="card-title">
{{ project.name }}
{% for tag in project.tags %}
<span class="badge text-bg-info ms-2">{{ tag.name }}</span>
{% endfor %}
</h2>
{% if project.description %}<p class="card-text">{{ project.description|markdown_to_html }}</p>{% endif %} {% if project.description %}<p class="card-text">{{ project.description|markdown_to_html }}</p>{% endif %}
<a href="{{ path('app_project_show', {'slug': project.slug}) }}" class="btn btn-primary">Voir</a>
<a href="{{ path('app_project_show', {'projectSlug': project.slug}) }}" class="btn btn-primary">Voir</a>
</div> </div>
</div> </div>
</div> </div>


+ 62
- 4
templates/project/show.html.twig View File

@ -2,20 +2,78 @@
{% block breadcrumb %} {% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ path('app_project') }}">Projets</a></li> <li class="breadcrumb-item"><a href="{{ path('app_project') }}">Projets</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_show', {'slug': project.slug}) }}">Projet {{ project.name }}</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_show', {'projectSlug': project.slug}) }}">Projet {{ project.name }}</a></li>
{% endblock %} {% endblock %}
{% block page_title %}Projet {{ project.name }}{% endblock %}
{% block page_title %}
{{ project.name }}
{% for tag in project.tags %}
<span class="badge text-bg-info ms-2">{{ tag.name }}</span>
{% endfor %}
{% endblock %}
{% block page_content %} {% block page_content %}
<div class="row"> <div class="row">
<div class="col mb-3"> <div class="col mb-3">
<a href="{{ path('app_project_update', {'slug': project.slug}) }}" class="btn btn-primary">Modifier le projet</a>
<a href="{{ path('app_project_remove', {'slug': project.slug}) }}" class="btn btn-primary">Supprimer le projet</a>
<a href="{{ path('app_project') }}" class="btn btn-primary">Revenir aux projets</a>
<a href="{{ path('app_project_update', {'projectSlug': project.slug}) }}" class="btn btn-primary">Modifier le projet</a>
<a href="{{ path('app_project_remove', {'projectSlug': project.slug}) }}" class="btn btn-primary">Supprimer le projet</a>
<a href="{{ path('app_task_create', {'projectSlug': project.slug}) }}" class="btn btn-primary">Créer une tâche</a>
</div> </div>
</div> </div>
{% if project.description is not empty %}
<div class="row"> <div class="row">
<div class="col mb-3 lead">{{ project.description|markdown_to_html }}</div> <div class="col mb-3 lead">{{ project.description|markdown_to_html }}</div>
</div> </div>
{% endif %}
<h2 class="mb-3">Carte</h2>
<div class="row">
<div class="col mb-3">
<div id="map" class="img-fluid img-thumbnail min-vh-50" data-controller="map" data-map-geojson-value="{{ geoJsonManager.generateGeoJson(project)|json_encode }}"></div>
</div>
</div>
{% if project.tasks is not empty %}
<div class="row">
<div class="col mb-3">
<div class="progress-stacked">
{% set stats = taskLifecycleManager.getProjectStats(project) %}
{% for place, data in stats %}
<div class="progress" role="progressbar" data-bs-toggle="tooltip" data-bs-title="{{ data.title }} {{ data.value ~ '/' ~ data.max }}" aria-label="{{ data.title }}" aria-valuenow="{{ data.value }}" aria-valuemin="0" aria-valuemax="{{ data.max }}" style="width:{{ data.percentage ~ '%' }}">
<div class="progress-bar {{ 'bg-' ~ data.color }}">{{ data.percentage|format_number({fraction_digit: 0}) ~ '%' }}</div>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="row">
<div class="col">
<table class="table table-sm table-hover">
<thead>
<tr>
<th scope="col">Identifiant</th>
<th scope="col">Nom</th>
<th scope="col">État</th>
<th scope="col">Importance</th>
<th scope="col">Urgence</th>
</tr>
</thead>
<tbody>
{% for task in project.tasks %}
<tr class="{{ 'table-' ~ workflow_metadata(task, 'color', task.status) }}">
<th scope="row">{{ task.id }}</th>
<td><a href="{{ path('app_task_show', {'projectSlug': project.slug, 'taskSlug': task.slug}) }}">{{ task.name }}</a></td>
<td>{{ workflow_metadata(task, 'title', task.status) }}</td>
<td>{{ task.important }}</td>
<td>{{ task.urgent }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}

+ 13
- 0
templates/task/create.html.twig View File

@ -0,0 +1,13 @@
{% extends 'base.html.twig' %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ path('app_project') }}">Projets</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_show', {'projectSlug': project.slug}) }}">Projet {{ project.name }}</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_task_create', {'projectSlug': project.slug}) }}">Créer une nouvelle tâche</a></li>
{% endblock %}
{% block page_title %}Créer une nouvelle tâche{% endblock %}
{% block page_content %}
{{ form(create_form) }}
{% endblock %}

+ 60
- 0
templates/task/show.html.twig View File

@ -0,0 +1,60 @@
{% extends 'base.html.twig' %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ path('app_project') }}">Projets</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_show', {'projectSlug': project.slug}) }}">Projet {{ project.name }}</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_task_show', {'projectSlug': project.slug, 'taskSlug': task.slug}) }}">Tâche {{ task.name }}</a></li>
{% endblock %}
{% block page_title %}
{{ task.name }}
<span class="badge {{ 'text-bg-' ~ workflow_metadata(task, 'color', task.status) }} ms-2">{{ workflow_metadata(task, 'title', task.status) }}</span>
{% endblock %}
{% block page_content %}
<div class="row">
<div class="col mb-3">
<a href="{{ path('app_project_show', {'projectSlug': project.slug}) }}" class="btn btn-primary">Revenir au projet</a>
<a href="{{ path('app_task_update', {'projectSlug': project.slug, 'taskSlug': task.slug}) }}" class="btn btn-primary">Modifier la tâche</a>
<a href="{{ path('app_task_remove', {'projectSlug': project.slug, 'taskSlug': task.slug}) }}" target="_blank" class="btn btn-primary">Supprimer la tâche</a>
<a href="{{ path('app_task_geojson', {'slug': task.slug}) }}" target="_blank" class="btn btn-primary">Télécharger GeoJSON</a>
{% for transition in workflow_transitions(task) %}
<a href="{{ path(workflow_metadata(task, 'route', transition), {'projectSlug': project.slug, 'taskSlug': task.slug}) }}" class="btn btn-primary">{{ workflow_metadata(task, 'title', transition) }}</a>
{% endfor %}
<button class="btn btn-primary" type="button" data-controller="josm" data-action="click->josm#remoteControl" data-josm-importurl-value="{{ url('app_task_osm', {'slug': task.slug}) }}" data-josm-layername-value="{{ task.name }}">Télécommande JOSM</button>
</div>
</div>
{% if task.description is not empty %}
<h2 class="mb-3">Description</h2>
<div class="row">
<div class="col mb-3 lead">{{ task.description|markdown_to_html }}</div>
</div>
{% endif %}
<h2 class="mb-3">Carte</h2>
<div class="row">
<div class="col mb-3">
<div id="map" class="img-fluid img-thumbnail min-vh-50" data-controller="map" data-map-geojson-value="{{ geoJsonManager.generateGeoJson(task)|json_encode }}"></div>
</div>
</div>
<h2 class="mb-3">Commentaires</h2>
{% if task.comments is not empty %}
<div class="row">
<div class="col mb-3">
{% for comment in task.comments %}
<blockquote class="blockquote">
{{ comment.content|markdown_to_html }}
</blockquote>
{% endfor %}
</div>
</div>
{% endif %}
<div class="row">
<div class="col mb-3">
{{ form(commentForm) }}
</div>
</div>
{% endblock %}

+ 13
- 0
templates/task/update.html.twig View File

@ -0,0 +1,13 @@
{% extends 'base.html.twig' %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ path('app_project') }}">Projets</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_project_show', {'projectSlug': project.slug}) }}">Projet {{ project.name }}</a></li>
<li class="breadcrumb-item"><a href="{{ path('app_task_show', {'projectSlug': project.slug, 'taskSlug': task.slug}) }}">Tâche {{ task.name }}</a></li>
{% endblock %}
{% block page_title %}Modifier la tâche {{ task.name }}{% endblock %}
{% block page_content %}
{{ form(update_form) }}
{% endblock %}

Loading…
Cancel
Save