Merge remote-tracking branch 'origin/master' into features/household-context

This commit is contained in:
Julien Fastré 2021-06-04 15:11:36 +02:00
commit 3e57ff3ffa
44 changed files with 1112 additions and 388 deletions

View File

@ -8,7 +8,7 @@ cache:
before_script: before_script:
# add extensions to postgres # add extensions to postgres
- PGPASSWORD=$POSTGRES_PASSWORD psql -U $POSTGRES_USER -h db -c "CREATE EXTENSION IF NOT EXISTS unaccent; CREATE EXTENSION IF NOT EXISTS pg_trgm; CREATE EXTENSION IF NOT EXISTS btree_gist;" - PGPASSWORD=$POSTGRES_PASSWORD psql -U $POSTGRES_USER -h db -c "CREATE EXTENSION IF NOT EXISTS unaccent; CREATE EXTENSION IF NOT EXISTS pg_trgm;"
# Install and run Composer # Install and run Composer
- curl -sS https://getcomposer.org/installer | php - curl -sS https://getcomposer.org/installer | php
- php -d memory_limit=2G composer.phar install - php -d memory_limit=2G composer.phar install

View File

@ -73,7 +73,7 @@
"symfony/web-profiler-bundle": "^5.0", "symfony/web-profiler-bundle": "^5.0",
"symfony/var-dumper": "4.*", "symfony/var-dumper": "4.*",
"symfony/debug-bundle": "^5.1", "symfony/debug-bundle": "^5.1",
"symfony/phpunit-bridge": "^5.2", "symfony/phpunit-bridge": "^5.2",
"nelmio/alice": "^3.8" "nelmio/alice": "^3.8"
}, },
"scripts": { "scripts": {

View File

@ -1,63 +0,0 @@
<?php
namespace Chill\MainBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\Routing\Annotation\Route;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\AddressReference;
/**
* Class AddressController
*
* @package Chill\MainBundle\Controller
*/
class AddressController extends AbstractController
{
/**
* Get API Data for showing endpoint
*
* @Route(
* "/{_locale}/main/api/1.0/address/{address_id}/show.{_format}",
* name="chill_main_address_api_show"
* )
* @ParamConverter("address", options={"id": "address_id"})
*/
public function showAddress(Address $address, $_format): Response
{
// TODO check ACL ?
switch ($_format) {
case 'json':
return $this->json($address);
default:
throw new BadRequestException('Unsupported format');
}
}
/**
* Get API Data for showing endpoint
*
* @Route(
* "/{_locale}/main/api/1.0/address-reference/{address_reference_id}/show.{_format}",
* name="chill_main_address_reference_api_show"
* )
* @ParamConverter("addressReference", options={"id": "address_reference_id"})
*/
public function showAddressReference(AddressReference $addressReference, $_format): Response
{
// TODO check ACL ?
switch ($_format) {
case 'json':
return $this->json($addressReference);
default:
throw new BadRequestException('Unsupported format');
}
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
/**
* Class AddressReferenceAPIController
*
* @package Chill\MainBundle\Controller
* @author Champs Libres
*/
class AddressReferenceAPIController extends ApiController
{
protected function customizeQuery(string $action, Request $request, $qb): void
{
if ($request->query->has('postal_code')) {
$qb->where('e.postcode = :postal_code')
->setParameter('postal_code', $request->query->get('postal_code'));
}
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
/**
* Class PostalCodeAPIController
*
* @package Chill\MainBundle\Controller
* @author Champs Libres
*/
class PostalCodeAPIController extends ApiController
{
protected function customizeQuery(string $action, Request $request, $qb): void
{
if ($request->query->has('country')) {
$qb->where('e.country = :country')
->setParameter('country', $request->query->get('country'));
}
}
}

View File

@ -284,6 +284,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
] ]
], ],
[ [
'controller' => \Chill\MainBundle\Controller\AddressReferenceAPIController::class,
'class' => \Chill\MainBundle\Entity\AddressReference::class, 'class' => \Chill\MainBundle\Entity\AddressReference::class,
'name' => 'address_reference', 'name' => 'address_reference',
'base_path' => '/api/1.0/main/address-reference', 'base_path' => '/api/1.0/main/address-reference',
@ -304,6 +305,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
] ]
], ],
[ [
'controller' => \Chill\MainBundle\Controller\PostalCodeAPIController::class,
'class' => \Chill\MainBundle\Entity\PostalCode::class, 'class' => \Chill\MainBundle\Entity\PostalCode::class,
'name' => 'postal_code', 'name' => 'postal_code',
'base_path' => '/api/1.0/main/postal-code', 'base_path' => '/api/1.0/main/postal-code',

View File

@ -9,11 +9,11 @@ use \JsonSerializable;
* *
*/ */
class Point implements JsonSerializable { class Point implements JsonSerializable {
private float $lat; private ?float $lat = null;
private float $lon; private ?float $lon = null;
public static string $SRID = '4326'; public static string $SRID = '4326';
private function __construct(float $lon, float $lat) private function __construct(?float $lon, ?float $lat)
{ {
$this->lat = $lat; $this->lat = $lat;
$this->lon = $lon; $this->lon = $lon;

View File

@ -4,6 +4,7 @@ namespace Chill\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Serializer\Annotation\Groups;
use Chill\MainBundle\Doctrine\Model\Point; use Chill\MainBundle\Doctrine\Model\Point;
use Chill\ThirdPartyBundle\Entity\ThirdParty; use Chill\ThirdPartyBundle\Entity\ThirdParty;
@ -22,6 +23,7 @@ class Address
* @ORM\Id * @ORM\Id
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO") * @ORM\GeneratedValue(strategy="AUTO")
* @groups({"write"})
*/ */
private $id; private $id;
@ -29,6 +31,7 @@ class Address
* @var string * @var string
* *
* @ORM\Column(type="string", length=255) * @ORM\Column(type="string", length=255)
* @groups({"write"})
*/ */
private $street = ''; private $street = '';
@ -36,6 +39,7 @@ class Address
* @var string * @var string
* *
* @ORM\Column(type="string", length=255) * @ORM\Column(type="string", length=255)
* @groups({"write"})
*/ */
private $streetNumber = ''; private $streetNumber = '';
@ -43,6 +47,7 @@ class Address
* @var PostalCode * @var PostalCode
* *
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode") * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode")
* @groups({"write"})
*/ */
private $postcode; private $postcode;
@ -50,6 +55,7 @@ class Address
* @var string|null * @var string|null
* *
* @ORM\Column(type="string", length=16, nullable=true) * @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"})
*/ */
private $floor; private $floor;
@ -57,6 +63,7 @@ class Address
* @var string|null * @var string|null
* *
* @ORM\Column(type="string", length=16, nullable=true) * @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"})
*/ */
private $corridor; private $corridor;
@ -64,6 +71,7 @@ class Address
* @var string|null * @var string|null
* *
* @ORM\Column(type="string", length=16, nullable=true) * @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"})
*/ */
private $steps; private $steps;
@ -71,6 +79,7 @@ class Address
* @var string|null * @var string|null
* *
* @ORM\Column(type="string", length=255, nullable=true) * @ORM\Column(type="string", length=255, nullable=true)
* @groups({"write"})
*/ */
private $buildingName; private $buildingName;
@ -78,6 +87,7 @@ class Address
* @var string|null * @var string|null
* *
* @ORM\Column(type="string", length=16, nullable=true) * @ORM\Column(type="string", length=16, nullable=true)
* @groups({"write"})
*/ */
private $flat; private $flat;
@ -85,6 +95,7 @@ class Address
* @var string|null * @var string|null
* *
* @ORM\Column(type="string", length=255, nullable=true) * @ORM\Column(type="string", length=255, nullable=true)
* @groups({"write"})
*/ */
private $distribution; private $distribution;
@ -92,6 +103,7 @@ class Address
* @var string|null * @var string|null
* *
* @ORM\Column(type="string", length=255, nullable=true) * @ORM\Column(type="string", length=255, nullable=true)
* @groups({"write"})
*/ */
private $extra; private $extra;
@ -102,6 +114,7 @@ class Address
* @var \DateTime * @var \DateTime
* *
* @ORM\Column(type="date") * @ORM\Column(type="date")
* @groups({"write"})
*/ */
private $validFrom; private $validFrom;
@ -112,11 +125,13 @@ class Address
* @var \DateTime|null * @var \DateTime|null
* *
* @ORM\Column(type="date", nullable=true) * @ORM\Column(type="date", nullable=true)
* @groups({"write"})
*/ */
private $validTo; private $validTo;
/** /**
* True if the address is a "no address", aka homeless person, ... * True if the address is a "no address", aka homeless person, ...
* @groups({"write"})
* *
* @var bool * @var bool
*/ */
@ -128,6 +143,7 @@ class Address
* @var Point|null * @var Point|null
* *
* @ORM\Column(type="point", nullable=true) * @ORM\Column(type="point", nullable=true)
* @groups({"write"})
*/ */
private $point; private $point;
@ -137,6 +153,7 @@ class Address
* @var ThirdParty|null * @var ThirdParty|null
* *
* @ORM\ManyToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty") * @ORM\ManyToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty")
* @groups({"write"})
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL") * @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/ */
private $linkedToThirdParty; private $linkedToThirdParty;

View File

@ -4,6 +4,7 @@ namespace Chill\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Chill\MainBundle\Doctrine\Model\Point; use Chill\MainBundle\Doctrine\Model\Point;
use Symfony\Component\Serializer\Annotation\Groups;
/** /**
* @ORM\Entity() * @ORM\Entity()
@ -16,21 +17,25 @@ class AddressReference
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
* @ORM\Column(type="integer") * @ORM\Column(type="integer")
* @groups({"read"})
*/ */
private $id; private $id;
/** /**
* @ORM\Column(type="string", length=255) * @ORM\Column(type="string", length=255)
* @groups({"read"})
*/ */
private $refId; private $refId;
/** /**
* @ORM\Column(type="string", length=255, nullable=true) * @ORM\Column(type="string", length=255, nullable=true)
* @groups({"read"})
*/ */
private $street; private $street;
/** /**
* @ORM\Column(type="string", length=255, nullable=true) * @ORM\Column(type="string", length=255, nullable=true)
* @groups({"read"})
*/ */
private $streetNumber; private $streetNumber;
@ -38,16 +43,19 @@ class AddressReference
* @var PostalCode * @var PostalCode
* *
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode") * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode")
* @groups({"read"})
*/ */
private $postcode; private $postcode;
/** /**
* @ORM\Column(type="string", length=255, nullable=true) * @ORM\Column(type="string", length=255, nullable=true)
* @groups({"read"})
*/ */
private $municipalityCode; private $municipalityCode;
/** /**
* @ORM\Column(type="string", length=255, nullable=true) * @ORM\Column(type="string", length=255, nullable=true)
* @groups({"read"})
*/ */
private $source; private $source;
@ -57,6 +65,7 @@ class AddressReference
* @var Point * @var Point
* *
* @ORM\Column(type="point") * @ORM\Column(type="point")
* @groups({"read"})
*/ */
private $point; private $point;

View File

@ -3,6 +3,7 @@
namespace Chill\MainBundle\Entity; namespace Chill\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/** /**
* Country * Country
@ -20,6 +21,7 @@ class Country
* @ORM\Id * @ORM\Id
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO") * @ORM\GeneratedValue(strategy="AUTO")
* @groups({"read"})
*/ */
private $id; private $id;
@ -27,13 +29,16 @@ class Country
* @var string * @var string
* *
* @ORM\Column(type="json_array") * @ORM\Column(type="json_array")
* @groups({"read"})
*
*/ */
private $name; private $name;
/** /**
* @var string * @var string
* *
* @ORM\Column(type="string", length=3) * @ORM\Column(type="string", length=3)
* @groups({"read"})
*/ */
private $countryCode; private $countryCode;
@ -41,7 +46,7 @@ class Country
/** /**
* Get id * Get id
* *
* @return integer * @return integer
*/ */
public function getId() public function getId()
{ {
@ -57,20 +62,20 @@ class Country
public function setName($name) public function setName($name)
{ {
$this->name = $name; $this->name = $name;
return $this; return $this;
} }
/** /**
* Get name * Get name
* *
* @return string * @return string
*/ */
public function getName() public function getName()
{ {
return $this->name; return $this->name;
} }
/** /**
* @return string * @return string
*/ */
@ -90,12 +95,12 @@ class Country
/** /**
* *
* @param string $countryCode * @param string $countryCode
*/ */
public function setCountryCode($countryCode) public function setCountryCode($countryCode)
{ {
$this->countryCode = $countryCode; $this->countryCode = $countryCode;
return $this; return $this;
} }
} }

View File

@ -3,6 +3,7 @@
namespace Chill\MainBundle\Entity; namespace Chill\MainBundle\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
/** /**
* PostalCode * PostalCode
@ -25,6 +26,7 @@ class PostalCode
* @ORM\Id * @ORM\Id
* @ORM\Column(name="id", type="integer") * @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO") * @ORM\GeneratedValue(strategy="AUTO")
* @groups({"read"})
*/ */
private $id; private $id;
@ -32,6 +34,7 @@ class PostalCode
* @var string * @var string
* *
* @ORM\Column(type="string", length=255, name="label") * @ORM\Column(type="string", length=255, name="label")
* @groups({"read"})
*/ */
private $name; private $name;
@ -39,6 +42,7 @@ class PostalCode
* @var string * @var string
* *
* @ORM\Column(type="string", length=100) * @ORM\Column(type="string", length=100)
* @groups({"read"})
*/ */
private $code; private $code;
@ -46,6 +50,7 @@ class PostalCode
* @var Country * @var Country
* *
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Country") * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Country")
* @groups({"read"})
*/ */
private $country; private $country;

View File

@ -23,7 +23,7 @@
// @import "bootstrap/scss/button-group"; // @import "bootstrap/scss/button-group";
// @import "bootstrap/scss/input-group"; // @import "bootstrap/scss/input-group";
// @import "bootstrap/scss/custom-forms"; // @import "bootstrap/scss/custom-forms";
// @import "bootstrap/scss/nav"; @import "bootstrap/scss/nav";
// @import "bootstrap/scss/navbar"; // @import "bootstrap/scss/navbar";
// @import "bootstrap/scss/card"; // @import "bootstrap/scss/card";
// @import "bootstrap/scss/breadcrumb"; // @import "bootstrap/scss/breadcrumb";

View File

@ -50,7 +50,7 @@ div#header-accompanying_course-name {
background: none repeat scroll 0 0 #718596; background: none repeat scroll 0 0 #718596;
color: #FFF; color: #FFF;
h1 { h1 {
margin: 0.4em 0; margin: 0.4em 0;
} }
span { span {
a { a {
@ -71,7 +71,7 @@ div#header-accompanying_course-details {
/* /*
* FLEX RESPONSIVE TABLE/BLOCK PRESENTATION * FLEX RESPONSIVE TABLE/BLOCK PRESENTATION
*/ */
div.flex-bloc, div.flex-bloc,
div.flex-table { div.flex-table {
h2, h3, h4, dl, p { h2, h3, h4, dl, p {
margin: 0; margin: 0;
@ -85,38 +85,38 @@ div.flex-table {
* Bloc appearance * Bloc appearance
*/ */
div.flex-bloc { div.flex-bloc {
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
align-items: stretch; align-items: stretch;
align-content: stretch; align-content: stretch;
div.item-bloc { div.item-bloc {
flex-grow: 0; flex-shrink: 1; flex-basis: 50%; flex-grow: 0; flex-shrink: 1; flex-basis: 50%;
margin: 0; margin: 0;
border: 1px solid #000; border: 1px solid #000;
padding: 1em; padding: 1em;
border-top: 0; border-top: 0;
&:nth-child(1), &:nth-child(2) { &:nth-child(1), &:nth-child(2) {
border-top: 1px solid #000; border-top: 1px solid #000;
} }
border-left: 0; border-left: 0;
&:nth-child(odd) { &:nth-child(odd) {
border-left: 1px solid #000; border-left: 1px solid #000;
} }
//background-color: #e6e6e6; //background-color: #e6e6e6;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
div.item-row { div.item-row {
flex-grow: 1; flex-shrink: 1; flex-basis: auto; flex-grow: 1; flex-shrink: 1; flex-basis: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
div.item-col { div.item-col {
&:first-child { &:first-child {
flex-grow: 0; flex-shrink: 0; flex-basis: auto; flex-grow: 0; flex-shrink: 0; flex-basis: auto;
@ -124,7 +124,7 @@ div.flex-bloc {
&:last-child { &:last-child {
flex-grow: 1; flex-shrink: 1; flex-basis: auto; flex-grow: 1; flex-shrink: 1; flex-basis: auto;
display: flex; display: flex;
.list-content { // ul, dl, or div .list-content { // ul, dl, or div
} }
ul.record_actions { ul.record_actions {
@ -134,7 +134,7 @@ div.flex-bloc {
li { li {
margin-right: 5px; margin-right: 5px;
} }
} }
} }
} }
} }
@ -147,7 +147,7 @@ div.flex-bloc {
margin: auto 0; margin: auto 0;
div.item-bloc { div.item-bloc {
border-left: 1px solid #000; border-left: 1px solid #000;
&:nth-child(2) { &:nth-child(2) {
border-top: 0; border-top: 0;
} }
} }
@ -156,13 +156,13 @@ div.flex-bloc {
/* /*
* Table appearance * Table appearance
*/ */
div.flex-table { div.flex-table {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
align-content: stretch; align-content: stretch;
div.item-bloc { div.item-bloc {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -173,7 +173,7 @@ div.flex-table {
border-top: 1px solid #000; border-top: 1px solid #000;
} }
&:nth-child(even) { &:nth-child(even) {
background-color: #e6e6e6; background-color: #e6e6e6;
} }
div.item-row { div.item-row {
@ -185,7 +185,7 @@ div.flex-table {
padding-top: 0.5em; padding-top: 0.5em;
flex-direction: column; flex-direction: column;
} }
div.item-col { div.item-col {
&:first-child { &:first-child {
flex-grow: 0; flex-shrink: 0; flex-basis: 33%; flex-grow: 0; flex-shrink: 0; flex-basis: 33%;
@ -194,7 +194,7 @@ div.flex-table {
flex-grow: 1; flex-shrink: 1; flex-basis: auto; flex-grow: 1; flex-shrink: 1; flex-basis: auto;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
.list-content { // ul, dl, or div .list-content { // ul, dl, or div
} }
ul.record_actions { ul.record_actions {
@ -204,7 +204,7 @@ div.flex-table {
li { li {
margin-right: 5px; margin-right: 5px;
} }
} }
} }
} }
@media only screen and (max-width: 900px) { @media only screen and (max-width: 900px) {
@ -217,9 +217,47 @@ div.flex-table {
} }
} }
} }
// neutralize // neutralize
div.chill_address div.chill_address_address p { text-indent: 0; } div.chill_address div.chill_address_address p { text-indent: 0; }
} }
} }
} }
/*
* Address form
*/
div.address_form {
display: flex;
flex-direction: column;
div.address_form__header {
}
div.address_form__select {
display: flex;
flex-direction: row;
justify-content: space-between;
div.address_form__select__body {
display: flex;
flex-direction: column;
flex-grow: 1;
}
div.address_form__select__map {
margin: 0px 20px;
div#address_map {
height:400px;
width:400px;
}
}
}
div.address_form__more {
}
}

View File

@ -1,5 +1,5 @@
<template> <template>
<div v-if="address.address"> <div v-if="address.address">
{{ address.address.street }}, {{ address.address.streetNumber }} {{ address.address.street }}, {{ address.address.streetNumber }}
</div> </div>
@ -9,7 +9,7 @@
<div v-if="address.country"> <div v-if="address.country">
{{ address.country.name }} {{ address.country.name }}
</div> </div>
<add-address <add-address
@addNewAddress="addNewAddress"> @addNewAddress="addNewAddress">
</add-address> </add-address>
@ -33,7 +33,26 @@ export default {
methods: { methods: {
addNewAddress({ address, modal }) { addNewAddress({ address, modal }) {
console.log('@@@ CLICK button addNewAdress', address); console.log('@@@ CLICK button addNewAdress', address);
this.$store.dispatch('addAddress', address.selected);
let newAddress = {
'isNoAddress': address.isNoAddress,
'street': address.selected.address.street,
'streetNumber': address.selected.address.streetNumber,
'postcode': {'id': address.selected.city.id },
'floor': address.floor,
'corridor': address.corridor,
'steps': address.steps,
'flat': address.flat,
'buildingName': address.buildingName,
'distribution': address.distribution,
'extra': address.extra
};
if (address.selected.address.point !== undefined){
newAddress = Object.assign(newAddress, {'point': address.selected.address.point.coordinates});
}
this.$store.dispatch('addAddress', newAddress);
modal.showModal = false; modal.showModal = false;
} }
} }

View File

@ -1,12 +1,13 @@
const addressMessages = { const addressMessages = {
fr: { fr: {
add_an_address: 'Ajouter une adresse', add_an_address_title: 'Ajouter une adresse',
select_an_address: 'Sélectionner une adresse', select_an_address_title: 'Sélectionner une adresse',
fill_an_address: 'Compléter l\'adresse', fill_an_address: 'Compléter l\'adresse',
select_country: 'Choisir le pays', select_country: 'Choisir le pays',
select_city: 'Choisir une localité', select_city: 'Choisir une localité',
select_address: 'Choisir une adresse', select_address: 'Choisir une adresse',
isNoAddress: 'L\'adresse n\'est pas celle d\'un domicile fixe ?', create_address: 'Appuyer sur "Entrée" pour créer une nouvelle adresse',
isNoAddress: 'Pas d\'adresse complète',
floor: 'Étage', floor: 'Étage',
corridor: 'Couloir', corridor: 'Couloir',
steps: 'Escalier', steps: 'Escalier',

View File

@ -1,8 +1,7 @@
import 'es6-promise/auto'; import 'es6-promise/auto';
import { createStore } from 'vuex'; import { createStore } from 'vuex';
// le fetch POST serait rangé dans la logique du composant qui appelle AddAddress import { postAddress } from '../../_api/AddAddress'
//import { postAddress } from '... api'
const debug = process.env.NODE_ENV !== 'production'; const debug = process.env.NODE_ENV !== 'production';
@ -10,11 +9,14 @@ const store = createStore({
strict: debug, strict: debug,
state: { state: {
address: {}, address: {},
errorMsg: {} errorMsg: []
}, },
getters: { getters: {
}, },
mutations: { mutations: {
catchError(state, error) {
state.errorMsg.push(error);
},
addAddress(state, address) { addAddress(state, address) {
console.log('@M addAddress address', address); console.log('@M addAddress address', address);
state.address = address; state.address = address;
@ -23,19 +25,19 @@ const store = createStore({
actions: { actions: {
addAddress({ commit }, payload) { addAddress({ commit }, payload) {
console.log('@A addAddress payload', payload); console.log('@A addAddress payload', payload);
commit('addAddress', payload); // à remplacer par //commit('addAddress', payload); // à remplacer par la suite
// fetch POST qui envoie l'adresse, et récupère la confirmation que c'est ok. //fetch POST qui envoie l'adresse, et récupère la confirmation que c'est ok.
// La confirmation est l'adresse elle-même. //La confirmation est l'adresse elle-même.
//
// postAddress(payload) postAddress(payload)
// .fetch(address => new Promise((resolve, reject) => { .then(address => new Promise((resolve, reject) => {
// commit('addAddress', address); commit('addAddress', address);
// resolve(); resolve();
// })) }))
// .catch((error) => { .catch((error) => {
// state.errorMsg.push(error.message); commit('catchError', error);
// }); });
} }
} }
}); });

View File

@ -1,37 +1,43 @@
/* /*
* Endpoint countries GET * Endpoint chill_api_single_country__index
* TODO * method GET, get Country Object
* @returns {Promise} a promise containing all Country object
*/ */
const fetchCountries = () => { const fetchCountries = () => {
console.log('<<< fetching countries'); console.log('<<< fetching countries');
return [
{id: 1, name: 'France', countryCode: 'FR'}, const url = `/api/1.0/main/country.json?item_per_page=1000`;
{id: 2, name: 'Belgium', countryCode: 'BE'} return fetch(url)
]; .then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
}; };
/* /*
* Endpoint cities GET * Endpoint chill_api_single_postal_code__index
* TODO * method GET, get Country Object
* @returns {Promise} a promise containing all Postal Code objects filtered with country
*/ */
const fetchCities = (country) => { const fetchCities = (country) => {
console.log('<<< fetching cities for', country); console.log('<<< fetching cities for', country);
return [ const url = `/api/1.0/main/postal-code.json?item_per_page=1000&country=${country.id}`;
{id: 1, name: 'Bruxelles', code: '1000', country: 'BE'}, return fetch(url)
{id: 2, name: 'Aisne', code: '85045', country: 'FR'}, .then(response => {
{id: 3, name: 'Saint-Gervais', code: '85230', country: 'FR'} if (response.ok) { return response.json(); }
]; throw Error('Error with request resource response');
});
}; };
/* /*
* Endpoint chill_main_address_reference_api_show * Endpoint chill_api_single_address_reference__index
* method GET, get AddressReference Object * method GET, get AddressReference Object
* @returns {Promise} a promise containing all AddressReference object * @returns {Promise} a promise containing all AddressReference objects filtered with postal code
*/ */
const fetchReferenceAddresses = (city) => { const fetchReferenceAddresses = (postalCode) => {
console.log('<<< fetching references addresses for', city); // city n'est pas utilisé pour le moment console.log('<<< fetching references addresses for', postalCode);
//TODO deal with huge number of addresses... we should do suggestion...
const url = `/api/1.0/main/address-reference.json`; const url = `/api/1.0/main/address-reference.json?item_per_page=1000&postal_code=${postalCode.id}`;
return fetch(url) return fetch(url)
.then(response => { .then(response => {
if (response.ok) { return response.json(); } if (response.ok) { return response.json(); }
@ -39,8 +45,31 @@ const fetchReferenceAddresses = (city) => {
}); });
}; };
/*
* Endpoint chill_api_single_address__entity__create
* method POST, post Address Object
* @returns {Promise}
*/
const postAddress = (address) => {
console.log(address);
const url = `/api/1.0/main/address.json?`;
const body = address;
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(body)
}).then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
export { export {
fetchCountries, fetchCountries,
fetchCities, fetchCities,
fetchReferenceAddresses fetchReferenceAddresses,
postAddress
}; };

View File

@ -1,6 +1,6 @@
<template> <template>
<button class="sc-button bt-create centered mt-4" @click="openModal"> <button class="sc-button bt-create centered mt-4" @click="openModal">
{{ $t('add_an_address') }} {{ $t('add_an_address_title') }}
</button> </button>
<teleport to="body"> <teleport to="body">
@ -9,76 +9,64 @@
@close="modal.showModal = false"> @close="modal.showModal = false">
<template v-slot:header> <template v-slot:header>
<h3 class="modal-title">{{ $t('add_an_address') }}</h3> <h3 class="modal-title">{{ $t('add_an_address_title') }}</h3>
</template> </template>
<template v-slot:body> <template v-slot:body>
<div class="address_form">
<h4>{{ $t('select_an_address') }}</h4>
<label for="isNoAddress">
<input type="checkbox"
name="isNoAddress"
v-bind:placeholder="$t('isNoAddress')"
v-model="isNoAddress"
v-bind:value="value"/>
{{ $t('isNoAddress') }}
</label>
<country-selection <div class="address_form__header">
v-bind:address="address" <h4>{{ $t('select_an_address_title') }}</h4>
v-bind:getCities="getCities"> </div>
</country-selection>
<city-selection
v-bind:address="address"
v-bind:getReferenceAddresses="getReferenceAddresses">
</city-selection>
<address-selection
v-bind:address="address"
v-bind:updateMapCenter="updateMapCenter">
</address-selection>
<address-map <div class="address_form__select">
v-bind:address="address"
ref="addressMap"> <div class="address_form__select__body">
</address-map> <label for="isNoAddress">
<input type="checkbox"
<address-more name="isNoAddress"
v-if="!isNoAddress" v-bind:placeholder="$t('isNoAddress')"
v-bind:address="address"> v-model="isNoAddress"
</address-more> v-bind:value="value"/>
{{ $t('isNoAddress') }}
<!-- </label>
<div class="address_form__fields__isNoAddress"></div>
<div class="address_form__select"> <country-selection
<div class="address_form__select__header"></div> v-bind:address="address"
<div class="address_form__select__left"></div> v-bind:getCities="getCities">
<div class="address_form__map"></div> </country-selection>
<city-selection
v-bind:address="address"
v-bind:getReferenceAddresses="getReferenceAddresses">
</city-selection>
<address-selection
v-bind:address="address"
v-bind:updateMapCenter="updateMapCenter">
</address-selection>
</div>
<div class="address_form__select__map">
<address-map
v-bind:address="address"
ref="addressMap">
</address-map>
</div>
</div>
<div class="address_form__more">
<address-more
v-if="!isNoAddress"
v-bind:address="address">
</address-more>
</div>
</div> </div>
<div class="address_form__fields">
<div class="address_form__fields__header"></div>
<div class="address_form__fields__left"></div>
<div class="address_form__fields__right"></div>
</div>
à discuter,
mais je pense qu'il est préférable de profiter de l'imbriquation des classes css
div.address_form {
div.select {
div.header {}
div.left {}
div.map {}
}
}
-->
</template> </template>
<template v-slot:footer> <template v-slot:footer>
<button class="sc-button green" <button class="sc-button green"
@click.prevent="$emit('addNewAddress', { address, modal })"> @click.prevent="$emit('addNewAddress', { address, modal })">
<i class="fa fa-plus fa-fw"></i>{{ $t('action.add')}} <i class="fa fa-plus fa-fw"></i>{{ $t('action.add')}}
</button> </button>
@ -155,49 +143,47 @@ export default {
} }
} }
}, },
mounted() {
this.getCountries();
},
methods: { methods: {
openModal() { openModal() {
this.modal.showModal = true; this.modal.showModal = true;
this.resetAll(); this.resetAll();
this.getCountries();
//this.$nextTick(function() { //this.$nextTick(function() {
// this.$refs.search.focus(); // positionner le curseur à l'ouverture de la modale // this.$refs.search.focus(); // positionner le curseur à l'ouverture de la modale
//}) //})
}, },
getCountries() { getCountries() {
console.log('getCountries'); console.log('getCountries');
this.address.loaded.countries = fetchCountries(); // à remplacer par fetchCountries().then(countries => new Promise((resolve, reject) => {
// fetchCountries().then(countries => new Promise((resolve, reject) => { this.address.loaded.countries = countries.results;
// this.address.loaded.countries = countries; resolve()
// resolve() }))
// })) .catch((error) => {
// .catch((error) => { this.errorMsg.push(error.message);
// this.errorMsg.push(error.message); });
// });
}, },
getCities(country) { getCities(country) {
console.log('getCities for', country.name); console.log('getCities for', country.name);
this.address.loaded.cities = fetchCities(); // à remplacer par fetchCities(country).then(cities => new Promise((resolve, reject) => {
// fetchCities(country).then(cities => new Promise((resolve, reject) => { this.address.loaded.cities = cities.results;
// this.address.loaded.cities = cities; resolve()
// resolve() }))
// })) .catch((error) => {
// .catch((error) => { this.errorMsg.push(error.message);
// this.errorMsg.push(error.message); });
// });
}, },
getReferenceAddresses(city) { getReferenceAddresses(city) {
console.log('getReferenceAddresses for', city.name); console.log('getReferenceAddresses for', city.name);
fetchReferenceAddresses(city) // il me semble que le paramètre city va limiter le poids des adresses de références reçues fetchReferenceAddresses(city).then(addresses => new Promise((resolve, reject) => {
.then(addresses => new Promise((resolve, reject) => {
console.log('addresses', addresses); console.log('addresses', addresses);
this.address.loaded.addresses = addresses.results; this.address.loaded.addresses = addresses.results;
resolve(); resolve();
})) }))
.catch((error) => { .catch((error) => {
this.errorMsg.push(error.message); this.errorMsg.push(error.message);
}); });
}, },
updateMapCenter(point) { updateMapCenter(point) {
console.log('point', point); console.log('point', point);
@ -208,7 +194,7 @@ export default {
resetAll() { resetAll() {
console.log('reset all selected'); console.log('reset all selected');
this.address.loaded.addresses = []; this.address.loaded.addresses = [];
this.address.selected.address = {}; this.address.selected.address = {};
this.address.loaded.cities = []; this.address.loaded.cities = [];
this.address.selected.city = {}; this.address.selected.city = {};
this.address.selected.country = {}; this.address.selected.country = {};

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="container"> <div class="container">
<div id='address_map' style='height:400px; width:400px;'></div> <div id='address_map'></div>
</div> </div>
</template> </template>
@ -11,6 +11,7 @@ import markerIconPng from 'leaflet/dist/images/marker-icon.png'
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
let map; let map;
let marker;
export default { export default {
name: 'AddressMap', name: 'AddressMap',
@ -30,13 +31,15 @@ export default {
const markerIcon = L.icon({ const markerIcon = L.icon({
iconUrl: markerIconPng, iconUrl: markerIconPng,
iconAnchor: [12, 41],
}); });
L.marker([48.8589, 2.3469], {icon: markerIcon}).addTo(map); marker = L.marker([48.8589, 2.3469], {icon: markerIcon}).addTo(map);
}, },
update() { update() {
console.log('update map with : ', this.address.addressMap.center) console.log('update map with : ', this.address.addressMap.center)
marker.setLatLng(this.address.addressMap.center);
map.setView(this.address.addressMap.center, 12); map.setView(this.address.addressMap.center, 12);
} }
}, },

View File

@ -1,38 +1,56 @@
<template> <template>
<div class="container"> <div class="container">
<select <VueMultiselect
v-model="selected"> v-model="value"
<option :value="{}" disabled selected>{{ $t('select_address') }}</option> @select="selectAddress"
<option name="field"
v-for="item in this.addresses" track-by="id"
v-bind:item="item" label="value"
v-bind:key="item.id" :custom-label="transName"
v-bind:value="item"> :taggable="true"
{{ item.street }}, {{ item.streetNumber }} :multiple="false"
</option> @tag="addAddress"
</select> :placeholder="$t('select_address')"
:tagPlaceholder="$t('create_address')"
:options="addresses">
</VueMultiselect>
</div> </div>
</template> </template>
<script> <script>
import VueMultiselect from 'vue-multiselect';
export default { export default {
name: 'AddressSelection', name: 'AddressSelection',
components: { VueMultiselect },
props: ['address', 'updateMapCenter'], props: ['address', 'updateMapCenter'],
data() {
return {
value: null
}
},
computed: { computed: {
addresses() { addresses() {
return this.address.loaded.addresses; return this.address.loaded.addresses;
}
},
methods: {
transName(value) {
return value.streetNumber === undefined ? value.street : `${value.street}, ${value.streetNumber}`
}, },
selected: { selectAddress(value) {
set(value) { this.address.selected.address = value;
console.log('selected value', value); this.updateMapCenter(value.point);
this.address.selected.address = value;
this.updateMapCenter(value.point);
},
get() {
return this.address.selected.address;
}
}, },
addAddress (newAddress) {
const address = {
street: newAddress
};
this.value = address;
this.address.selected.address = address;
}
} }
}; };
</script> </script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@ -1,38 +1,45 @@
<template> <template>
<div class="container"> <div class="container">
<select <VueMultiselect
v-model="selected"> v-model="value"
<option :value="{}" disabled selected>{{ $t('select_city') }}</option> @select="selectCity"
<option name="field"
v-for="item in this.cities" track-by="id"
v-bind:item="item" label="value"
v-bind:key="item.id" :custom-label="transName"
v-bind:value="item"> :placeholder="$t('select_city')"
{{ item.code }}-{{ item.name }} :options="cities">
</option> </VueMultiselect>
</select>
</div> </div>
</template> </template>
<script> <script>
import VueMultiselect from 'vue-multiselect';
export default { export default {
name: 'CitySelection', name: 'CitySelection',
components: { VueMultiselect },
props: ['address', 'getReferenceAddresses'], props: ['address', 'getReferenceAddresses'],
data() {
return {
value: null
}
},
methods: {
transName(value) {
return `${value.code}-${value.name}`
},
selectCity(value) {
this.address.selected.city = value;
this.getReferenceAddresses(value);
},
},
computed: { computed: {
cities() { cities() {
return this.address.loaded.cities; return this.address.loaded.cities;
}, }
selected: {
set(value) {
console.log('selected value', value.name);
this.address.selected.city = value;
this.getReferenceAddresses(value);
},
get() {
return this.address.selected.city;
}
},
} }
}; };
</script> </script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@ -1,38 +1,59 @@
<template> <template>
<div class="container"> <div class="container">
<select <VueMultiselect
v-model="selected"> v-model="value"
<option :value="{}" disabled selected>{{ $t('select_country') }}</option> name="field"
<option track-by="id"
v-for="item in this.countries" label="name"
v-bind:item="item" :custom-label="transName"
v-bind:key="item.id" :placeholder="$t('select_country')"
v-bind:value="item"> :options="countries"
{{ item.name }} @select="selectCountry">
</option> </VueMultiselect>
</select>
</div> </div>
</template> </template>
<script> <script>
import VueMultiselect from 'vue-multiselect';
export default { export default {
name: 'CountrySelection', name: 'CountrySelection',
components: { VueMultiselect },
props: ['address', 'getCities'], props: ['address', 'getCities'],
computed: { data() {
countries() { return {
return this.address.loaded.countries; value: this.address.loaded.countries.filter(c => c.countryCode === 'FR')[0]
}, }
selected: { },
set(value) { methods: {
console.log('selected value', value.name); init() {
this.address.selected.country = value; if (this.value !== undefined) {
this.getCities(value); this.getCities(this.value);
},
get() {
return this.address.selected.country;
} }
},
transName ({ name }) {
return name.fr //TODO multilang
},
selectCountry(value) {
console.log(value);
this.address.selected.country = value;
this.getCities(value);
},
},
mounted(){
this.init()
},
computed: {
countries() {
const countries = this.address.loaded.countries;
let orderedCountries = [];
orderedCountries.push(...countries.filter(c => c.countryCode === 'FR'))
orderedCountries.push(...countries.filter(c => c.countryCode === 'BE'))
orderedCountries.push(...countries.filter(c => c.countryCode !== 'FR').filter(c => c.countryCode !== 'BE'))
return orderedCountries;
} }
} }
}; };
</script> </script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@ -0,0 +1,153 @@
<template>
<a class="sc-button" target="_blank"
:class="classAction"
:title="$t(titleAction)"
@click="openModal">
{{ buttonText }}
</a>
<teleport to="body">
<modal v-if="modal.showModal"
:modalDialogClass="modal.modalDialogClass"
@close="modal.showModal = false">
<template v-slot:header>
<h3 class="modal-title">{{ $t(titleModal) }}</h3>
</template>
<template v-slot:body v-if="type === 'person'">
<on-the-fly-person
v-bind:id="id"
v-bind:type="type"
v-bind:action="action"
ref="castPerson">
</on-the-fly-person>
</template>
<template v-slot:body v-else-if="type === 'thirdparty'">
<on-the-fly-thirdparty
v-bind:id="id"
v-bind:type="type"
v-bind:action="action"
ref="castThirdparty">
</on-the-fly-thirdparty>
</template>
<template v-slot:body v-else>
<on-the-fly-create
v-bind:action="action"
ref="castNew">
</on-the-fly-create>
</template>
<template v-slot:footer>
<button v-if="action === 'show'"
@click="changeActionTo('edit')"
class="sc-button bt-update"> <!-- @click.prevent="$emit('..', ..)" -->
</button>
<button v-else
class="sc-button bt-save"
@click="saveAction"
> <!--
-->
{{ $t('action.save')}}
</button>
</template>
</modal>
</teleport>
</template>
<script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue';
import OnTheFlyPerson from 'ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue';
import OnTheFlyThirdparty from './OnTheFly/ThirdParty.vue';
import OnTheFlyCreate from './OnTheFly/Create.vue';
export default {
name: 'OnTheFly',
components: {
Modal,
OnTheFlyPerson,
OnTheFlyThirdparty,
OnTheFlyCreate
},
props: ['type', 'id', 'action', 'buttonText'],
data() {
return {
modal: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl"
},
action: this.action
}
},
computed: {
action() {
return this.$data.action;
},
classAction() {
switch (this.action) {
case 'show':
return 'bt-show';
case 'edit':
return 'bt-update';
case 'create':
return 'bt-create';
}
},
titleAction() {
switch (this.action) {
case 'show':
return 'action.show';
case 'edit':
return 'action.edit';
case 'create':
return 'action.create';
}
},
titleModal() {
switch (this.action) {
case 'show':
return 'onthefly.show.' + this.type;
case 'edit':
return 'onthefly.edit.' + this.type;
case 'create':
return 'onthefly.create.title';
}
}
},
methods: {
openModal() {
this.modal.showModal = true;
this.$nextTick(function() {
//this.$refs.search.focus();
})
},
changeActionTo(action) {
// [BUG] clic first on show item button; in modal clic edit button; close modal; clic again on show item button
this.$data.action = action;
},
saveAction() {
console.log('saveAction');
if (this.type === 'person') {
this.$refs.castPerson.postData();
} else if (this.type === 'thirdparty') {
this.$refs.castThirdparty.postData();
} else {
// saveAction() ==cast=to==> child.castByType() ==cast=to==> grand-child.postData()
this.$refs.castNew.castByType();
}
this.modal.showModal = false;
}
}
}
</script>
<style lang="css" scoped>
a {
cursor: pointer;
}
</style>

View File

@ -0,0 +1,84 @@
<template>
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" :class="{ active: isActive('person') }">
<label for="person">
<input type="radio" name="person" v-model="radioType" value="person">
{{ $t('onthefly.create.person') }}
</label>
</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{ active: isActive('thirdparty') }">
<label for="thirdparty">
<input type="radio" name="thirdparty" v-model="radioType" value="thirdparty">
{{ $t('onthefly.create.thirdparty') }}
</label>
</a>
</li>
</ul>
<div class="my-4">
<on-the-fly-person
v-if="type === 'person'"
v-bind:action="action"
ref="castPerson">
</on-the-fly-person>
<on-the-fly-thirdparty
v-if="type === 'thirdparty'"
v-bind:action="action"
ref="castThirdparty">
</on-the-fly-thirdparty>
</div>
</template>
<script>
import OnTheFlyPerson from 'ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue';
import OnTheFlyThirdparty from './ThirdParty.vue'; // TODO move in ChillThirdpartyAssets
export default {
name: "OnTheFlyCreate",
props: ['action'],
components: {
OnTheFlyPerson,
OnTheFlyThirdparty
},
data() {
return {
type: 'person' //by default
}
},
computed: {
radioType: {
set(type) {
this.type = type;
},
get() {
return this.type;
}
}
},
methods: {
isActive(tab) {
return (this.type === tab) ? true : false;
},
castByType() {
console.log('saveActionByType');
if (this.type === 'person') {
this.$refs.castPerson.postData();
} else if (this.type === 'thirdparty') {
this.$refs.castThirdparty.postData();
} else {
throw Error('Invalid type of entity');
}
}
}
}
</script>
<style lang="css" scoped>
label {
cursor: pointer;
}
</style>

View File

@ -0,0 +1,23 @@
<template>
<div v-if="action === 'show'">
show
thirdparty
{{ id }}
</div>
<div v-else-if="action === 'edit' || action === 'create'">
{{ action }}
thirdparty
{{ id }}
</div>
</template>
<script>
export default {
name: "OnTheFlyThirdParty",
props: ['id', 'type', 'action']
}
// TODO move in ChillThirdpartyAssets
</script>
<style lang="css" scoped>
</style>

View File

@ -46,6 +46,22 @@ const messages = {
previous: "Précédent", previous: "Précédent",
top: "Haut", top: "Haut",
bottom: "Bas", bottom: "Bas",
},
onthefly: {
show: {
person: "Détails de l'usager",
thirdparty: "Détails du tiers"
},
edit: {
person: "Modifier un usager",
thirdparty: "Modifier un tiers"
},
create: {
button: "Créer \"{q}\"",
title: "Créer à la volée…",
person: "un nouvel usager",
thirdparty: "un nouveau tiers"
},
} }
} }
}; };

View File

@ -0,0 +1,34 @@
<?php
namespace Chill\MainBundle\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Chill\MainBundle\Doctrine\Model\Point;
class PointNormalizer implements DenormalizerInterface
{
public function supportsDenormalization($data, string $type, string $format = null) : bool
{
return $type === Point::class;
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
if (!is_array($data)) {
throw new InvalidArgumentException('point data is not an array. It should be an array of 2 coordinates.');
} else {
if (count($data) !== 2) {
throw new InvalidArgumentException('point data is not an array of 2 elements. It should be an array of 2 coordinates.');
} else {
return Point::fromLonLat($data[0], $data[1]);
}
}
}
}

View File

@ -31,14 +31,14 @@ services:
- [ setContainer, ["@service_container"]] - [ setContainer, ["@service_container"]]
tags: tags:
- { name: twig.extension } - { name: twig.extension }
chill.main.twig.widget: chill.main.twig.widget:
class: Chill\MainBundle\Templating\Widget\WidgetRenderingTwig class: Chill\MainBundle\Templating\Widget\WidgetRenderingTwig
arguments: arguments:
- "@event_dispatcher" - "@event_dispatcher"
tags: tags:
- { name: twig.extension } - { name: twig.extension }
chill.main.twig.csv_cell: chill.main.twig.csv_cell:
class: Chill\MainBundle\Templating\CSVCellTwig class: Chill\MainBundle\Templating\CSVCellTwig
tags: tags:
@ -65,3 +65,4 @@ services:
- "@security.authorization_checker" - "@security.authorization_checker"
- "@chill.main.security.authorization.helper" - "@chill.main.security.authorization.helper"
- "@security.token_storage" - "@security.token_storage"

View File

@ -7,7 +7,7 @@ services:
$validator: '@Symfony\Component\Validator\Validator\ValidatorInterface' $validator: '@Symfony\Component\Validator\Validator\ValidatorInterface'
tags: tags:
- { name: console.command } - { name: console.command }
Chill\MainBundle\Command\ChillUserSendRenewPasswordCodeCommand: Chill\MainBundle\Command\ChillUserSendRenewPasswordCodeCommand:
arguments: arguments:
$logger: '@Psr\Log\LoggerInterface' $logger: '@Psr\Log\LoggerInterface'

View File

@ -1,7 +1,7 @@
--- ---
services: services:
# note: the autowiring for serializers and normalizers is declared # note: the autowiring for serializers and normalizers is declared
# into ../services.yaml # into ../services.yaml
Chill\MainBundle\Serializer\Normalizer\DoctrineExistingEntityNormalizer: Chill\MainBundle\Serializer\Normalizer\DoctrineExistingEntityNormalizer:

View File

@ -10,7 +10,7 @@ use Doctrine\Migrations\AbstractMigration;
/** /**
* Add extension btree_gist * Add extension btree_gist
*/ */
final class Version20210528104651 extends AbstractMigration final class Version20210528090000 extends AbstractMigration
{ {
public function getDescription(): string public function getDescription(): string
{ {

View File

@ -9,8 +9,7 @@
<table class="rounded" v-if="participations.length > 0"> <table class="rounded" v-if="participations.length > 0">
<thead> <thead>
<tr> <tr>
<th class="chill-orange">{{ $t('persons_associated.firstname') }}</th> <th class="chill-orange">{{ $t('persons_associated.name') }}</th>
<th class="chill-orange">{{ $t('persons_associated.lastname') }}</th>
<th class="chill-orange">{{ $t('persons_associated.startdate') }}</th> <th class="chill-orange">{{ $t('persons_associated.startdate') }}</th>
<th class="chill-orange">{{ $t('persons_associated.enddate') }}</th> <th class="chill-orange">{{ $t('persons_associated.enddate') }}</th>
<th class="chill-orange">{{ $t('action.actions') }}</th> <th class="chill-orange">{{ $t('action.actions') }}</th>

View File

@ -1,7 +1,9 @@
<template> <template>
<tr> <tr>
<td>{{ participation.person.firstName }}</td> <td>
<td>{{ participation.person.lastName }}</td> {{ participation.person.firstName }}
{{ participation.person.lastName }}
</td>
<td><span v-if="participation.startDate"> <td><span v-if="participation.startDate">
{{ $d(participation.startDate.datetime, 'short') }}</span> {{ $d(participation.startDate.datetime, 'short') }}</span>
</td> </td>
@ -11,16 +13,18 @@
<td> <td>
<ul class="record_actions"> <ul class="record_actions">
<li> <li>
<a class="sc-button bt-show" target="_blank" <on-the-fly
:href="url.show" v-bind:type="participation.person.type"
:title="$t('action.show')"> v-bind:id="participation.person.id"
</a> action="show">
</on-the-fly>
</li> </li>
<li> <li>
<a class="sc-button bt-update" target="_blank" <on-the-fly
:href="url.edit" v-bind:type="participation.person.type"
:title="$t('action.edit')"> v-bind:id="participation.person.id"
</a> action="edit">
</on-the-fly>
</li> </li>
<!--li> <!--li>
<button class="sc-button bt-delete" <button class="sc-button bt-delete"
@ -31,7 +35,7 @@
<li> <li>
<button v-if="!participation.endDate" <button v-if="!participation.endDate"
class="sc-button bt-remove" class="sc-button bt-remove"
:title="$t('action.remove')" v-bind:title="$t('action.remove')"
@click.prevent="$emit('close', participation)"> @click.prevent="$emit('close', participation)">
</button> </button>
<button v-else class="sc-button bt-remove disabled"></button> <button v-else class="sc-button bt-remove disabled"></button>
@ -42,17 +46,14 @@
</template> </template>
<script> <script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default { export default {
name: 'PersonItem', name: 'PersonItem',
props: ['participation'], components: {
data() { OnTheFly
return {
url: {
show: '/fr/person/' + this.participation.person.id + '/general',
edit: '/fr/person/' + this.participation.person.id + '/general/edit'
}
}
}, },
emits: ['remove', 'close'] props: ['participation'],
emits: ['remove', 'close'],
} }
</script> </script>

View File

@ -41,7 +41,18 @@
<ul class="record_actions"> <ul class="record_actions">
<li> <li>
<a class="sc-button bt-show" :title="$t('action.show')" target="_blank" :href="url.show"></a> <on-the-fly
v-bind:type="accompanyingCourse.requestor.type"
v-bind:id="accompanyingCourse.requestor.id"
action="show">
</on-the-fly>
</li>
<li>
<on-the-fly
v-bind:type="accompanyingCourse.requestor.type"
v-bind:id="accompanyingCourse.requestor.id"
action="edit">
</on-the-fly>
</li> </li>
</ul> </ul>
</div> </div>
@ -76,12 +87,14 @@
</template> </template>
<script> <script>
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue' import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default { export default {
name: 'Requestor', name: 'Requestor',
components: { components: {
AddPersons, AddPersons,
OnTheFly
}, },
data() { data() {
return { return {
@ -107,13 +120,6 @@ export default {
get() { get() {
return this.$store.state.accompanyingCourse.requestorAnonymous; return this.$store.state.accompanyingCourse.requestorAnonymous;
} }
},
url() {
return (this.accompanyingCourse.requestor.type === 'person') ? {
show: `/fr/person/${this.accompanyingCourse.requestor.id}/general`,
} : {
show: `/fr/thirdparty/thirdparty/${this.accompanyingCourse.requestor.id}/show`,
}
} }
}, },
methods: { methods: {

View File

@ -21,24 +21,25 @@
<td> <td>
<ul class="record_actions"> <ul class="record_actions">
<li> <li>
<a class="sc-button bt-show" target="_blank" <on-the-fly
:href="url.show" v-bind:type="resource.resource.type"
:title="$t('action.show')"> v-bind:id="resource.resource.id"
</a> action="show">
</on-the-fly>
</li> </li>
<li> <li>
<a class="sc-button bt-update" target="_blank" <on-the-fly
:href="url.edit" v-bind:type="resource.resource.type"
:title="$t('action.edit')"> v-bind:id="resource.resource.id"
</a> action="edit">
</on-the-fly>
</li> </li>
<li> <li>
<button <button
class="sc-button bt-remove" class="sc-button bt-remove"
:title="$t('action.remove')" v-bind:title="$t('action.remove')"
@click.prevent="$emit('remove', resource)"> @click.prevent="$emit('remove', resource)">
</button> </button>
</li> </li>
</ul> </ul>
</td> </td>
@ -46,23 +47,14 @@
</template> </template>
<script> <script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default { export default {
name: 'ResourceItem', name: 'ResourceItem',
components: {
OnTheFly
},
props: ['resource'], props: ['resource'],
emits: ['remove'], emits: ['remove']
computed: {
type() {
return this.resource.resource.type;
},
url() {
return (this.type === 'person') ? {
show: `/fr/person/${this.resource.resource.id}/general`,
edit: `/fr/person/${this.resource.resource.id}/general/edit`
} : {
show: `/fr/thirdparty/thirdparty/${this.resource.resource.id}/show`,
edit: `/fr/thirdparty/thirdparty/${this.resource.resource.id}/update`
}
}
}
} }
</script> </script>

View File

@ -31,6 +31,7 @@ const appMessages = {
counter: "Il n'y a pas encore d'usager | 1 usager | {count} usagers", counter: "Il n'y a pas encore d'usager | 1 usager | {count} usagers",
firstname: "Prénom", firstname: "Prénom",
lastname: "Nom", lastname: "Nom",
name: "Nom",
startdate: "Date d'entrée", startdate: "Date d'entrée",
enddate: "Date de sortie", enddate: "Date de sortie",
add_persons: "Ajouter des usagers", add_persons: "Ajouter des usagers",
@ -73,7 +74,7 @@ const appMessages = {
comment: { comment: {
title: "Observations", title: "Observations",
label: "Ajout d'une note", label: "Ajout d'une note",
content: "Rédigez une première note...", content: "Rédigez une première note",
created_by: "créé par {0}, le {1}" created_by: "créé par {0}, le {1}"
}, },
confirm: { confirm: {

View File

@ -0,0 +1,34 @@
/*
* GET a person by id
*/
const getPerson = (id) => {
const url = `/api/1.0/person/person/${id}.json`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* POST a new person
*/
const postPerson = (body) => {
const url = `/api/1.0/person/person.json`;
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(body)
})
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
export {
getPerson,
postPerson
};

View File

@ -65,9 +65,14 @@
@updateSelected="updateSelected"> @updateSelected="updateSelected">
</person-suggestion> </person-suggestion>
<button v-if="query.length >= 3" class="sc-button bt-create ml-5 mt-2" name="createPerson"> <div class="create-button">
{{ $t('action.create') }} "{{ query }}" <on-the-fly
</button> v-if="query.length >= 3"
v-bind:buttonText="$t('onthefly.create.button', {q: query})"
action="create"><!-- TODO first close this modal -->
</on-the-fly>
</div>
</div> </div>
</template> </template>
@ -84,6 +89,7 @@
<script> <script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal'; import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
import PersonSuggestion from './AddPersons/PersonSuggestion'; import PersonSuggestion from './AddPersons/PersonSuggestion';
import { searchPersons, searchPersons_2 } from 'ChillPersonAssets/vuejs/_api/AddPersons'; import { searchPersons, searchPersons_2 } from 'ChillPersonAssets/vuejs/_api/AddPersons';
@ -92,6 +98,7 @@ export default {
components: { components: {
Modal, Modal,
PersonSuggestion, PersonSuggestion,
OnTheFly
}, },
props: [ props: [
'buttonTitle', 'buttonTitle',
@ -170,7 +177,7 @@ export default {
if (query.length >= 3) { if (query.length >= 3) {
searchPersons_2({ query, options: this.options }) searchPersons_2({ query, options: this.options })
.then(suggested => new Promise((resolve, reject) => { .then(suggested => new Promise((resolve, reject) => {
console.log('suggested', suggested); //console.log('suggested', suggested);
this.loadSuggestions(suggested.results); this.loadSuggestions(suggested.results);
resolve(); resolve();
})); }));
@ -179,14 +186,14 @@ export default {
} }
}, },
loadSuggestions(suggested) { loadSuggestions(suggested) {
console.log('suggested', suggested); //console.log('suggested', suggested);
this.search.suggested = suggested; this.search.suggested = suggested;
this.search.suggested.forEach(function(item) { this.search.suggested.forEach(function(item) {
item.key = this.itemKey(item); item.key = this.itemKey(item);
}, this); }, this);
}, },
updateSelected(value) { updateSelected(value) {
console.log('value', value); //console.log('value', value);
this.search.selected = value; this.search.selected = value;
}, },
resetSearch() { resetSearch() {
@ -251,4 +258,8 @@ export default {
} }
} }
} }
.create-button > a {
margin-top: 0.5em;
margin-left: 2.6em;
}
</style> </style>

View File

@ -42,7 +42,7 @@ export default {
computed: { computed: {
selected: { selected: {
set(value) { set(value) {
console.log('value', value); //console.log('value', value);
this.$emit('updateSelected', value); this.$emit('updateSelected', value);
}, },
get() { get() {

View File

@ -14,22 +14,23 @@
<span class="badge badge-pill badge-secondary" :title="item.key"> <span class="badge badge-pill badge-secondary" :title="item.key">
{{ $t('item.type_person') }} {{ $t('item.type_person') }}
</span> </span>
<a class="sc-button bt-show" target="_blank" :title="item.key" :href="url.show"></a> <on-the-fly
type="person"
v-bind:id="item.result.id"
action="show">
</on-the-fly>
</div> </div>
</template> </template>
<script> <script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default { export default {
name: 'SuggestionPerson', name: 'SuggestionPerson',
props: ['item'], components: {
data() { OnTheFly
return {
url: {
show: '/fr/person/' + this.item.result.person_id + '/general',
edit: '/fr/person/' + this.item.result.person_id + '/general/edit'
},
}
}, },
props: ['item']
} }
</script> </script>

View File

@ -15,22 +15,23 @@
<span class="badge badge-pill badge-secondary" :title="item.key"> <span class="badge badge-pill badge-secondary" :title="item.key">
{{ $t('item.type_thirdparty') }} {{ $t('item.type_thirdparty') }}
</span> </span>
<a class="sc-button bt-show" target="_blank" :title="item.key" :href="url.show"></a> <on-the-fly
type="thirdparty"
v-bind:id="item.result.id"
action="show">
</on-the-fly>
</div> </div>
</template> </template>
<script> <script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default { export default {
name: 'SuggestionThirdParty', name: 'SuggestionThirdParty',
props: ['item'], components: {
data() { OnTheFly
return {
url: {
show: '/fr/thirdparty/thirdparty/' + this.item.result.thirdparty_id + '/show',
edit: '/fr/thirdparty/thirdparty/' + this.item.result.thirdparty_id + '/edit'
},
}
}, },
props: ['item']
} }
</script> </script>

View File

@ -0,0 +1,200 @@
<template>
<div v-if="action === 'show'">
<div class="flex-table">
<div class="item-bloc">
<div class="item-row">
<div class="item-col">
<h3 :title="person.id">{{ person.text }}</h3>
<p>
<i class="fa fa-fw"
:class="genderClass">
<!--
:title="$t(genderTranslation)"
-->
</i>
<span v-if="person.birthdate">
{{ $t('person.born', { e: feminized }) }}
{{ $d(person.birthdate.datetime, 'short') }}
</span>
</p>
</div>
<div class="item-col">
<dl class="list-content">
<dt>{{ $t('person.firstname') }}</dt>
<dd>{{ person.firstName }}</dd>
<dt>{{ $t('person.lastname') }}</dt>
<dd>{{ person.lastName }}</dd>
<dt>{{ $t('person.altnames') }}</dt>
<dd>{{ person.altNames }}</dd>
<span v-if="person.center">
<dt>{{ $t('person.center_name') }}</dt>
<dd :title="person.center.id">{{ person.center.name }}</dd>
</span>
<dt>{{ $t('person.phonenumber') }}</dt>
<dd>{{ person.phonenumber }}</dd>
<dt>{{ $t('person.mobilenumber') }}</dt>
<dd>{{ person.mobilenumber }}</dd>
<dt>{{ $t('person.gender.title') }}</dt>
<!--
<dd>{{ $t(genderTranslation) }}</dd>
-->
</dl>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="action === 'edit' || action === 'create'">
<input v-model="firstName" :placeholder="$t('person.firstname')" />
<input v-model="lastName" :placeholder="$t('person.lastname')" />
<!-- TODO fix placeholder if undefined
TODO dynamically get gender options
-->
<select v-model="gender">
<option disabled value="">{{ $t('person.gender.placeholder') }}</option>
<option value="woman">{{ $t('person.gender.woman') }}</option>
<option value="man">{{ $t('person.gender.man') }}</option>
<option value="neuter">{{ $t('person.gender.neuter') }}</option>
</select>
<i class="fa fa-birthday-cake"></i>
<input type="date"
id="chill_personbundle_person_birthdate"
name="chill_personbundle_person[birthdate]"
v-model="birthDate"
/>
<i class="fa fa-phone">
</i><input v-model="phonenumber" :placeholder="$t('person.phonenumber')" />
<i class="fa fa-mobile">
</i><input v-model="mobilenumber" :placeholder="$t('person.mobilenumber')" />
</div>
</template>
<script>
import { getPerson, postPerson } from '../../_api/OnTheFly';
export default {
name: "OnTheFlyPerson",
props: ['id', 'type', 'action'],
data() {
return {
person: {
type: 'person'
}
}
},
computed: {
firstName: {
set(value) { this.person.firstName = value; },
get() { return this.person.firstName; }
},
lastName: {
set(value) { this.person.lastName = value; },
get() { return this.person.lastName; }
},
gender: {
set(value) { this.person.gender = value; },
get() { return this.person.gender; }
},
birthDate: {
set(value) {
if (this.person.birthdate) {
this.person.birthdate.datetime = value + "T00:00:00+0100";
} else {
this.person.birthdate = { datetime: value + "T00:00:00+0100"};
}
},
get() {
return (this.person.birthdate) ? this.person.birthdate.datetime.split('T')[0] : '';
}
},
phonenumber: {
set(value) { this.person.phonenumber = value; },
get() { return this.person.phonenumber; }
},
mobilenumber: {
set(value) { this.person.mobilenumber = value; },
get() { return this.person.mobilenumber; }
},
genderClass() {
switch (this.person.gender) {
case 'woman':
return 'fa-venus';
case 'man':
return 'fa-mars';
case 'neuter':
return 'fa-neuter';
}
},
genderTranslation() {
switch (this.person.gender) {
case 'woman':
return 'person.gender.woman';
case 'man':
return 'person.gender.man';
case 'neuter':
return 'person.gender.neuter';
}
},
feminized() {
return (this.person.gender === 'woman')? 'e' : '';
}
},
mounted() {
if (this.action !== 'create') {
this.loadData();
}
},
methods: {
loadData() {
getPerson(this.id)
.then(person => new Promise((resolve, reject) => {
this.person = person;
console.log('get person', this.person);
resolve();
}));
},
postData() {
postPerson(this.person)
.then(person => new Promise((resolve, reject) => {
this.person = person;
console.log('post person', person);
resolve();
}));
}
}
}
</script>
<style lang="scss" scoped>
ul {
li::marker {
}
}
div.flex-table {
div.item-bloc {
div.item-row {
div.item-col:last-child {
justify-content: flex-start;
}
}
}
}
dl {
dd {
margin-left: 1em;
}
}
</style>

View File

@ -15,7 +15,21 @@ const personMessages = {
person: { person: {
firstname: "Prénom", firstname: "Prénom",
lastname: "Nom", lastname: "Nom",
born: "né le ", born: "né{e} le ",
center_id: "Identifiant du centre",
center_type: "Type de centre",
center_name: "Territoire", // vendée
phonenumber: "Téléphone",
mobilenumber: "Mobile",
altnames: "Autres noms",
gender: {
title: "Genre",
placeholder: "Choisissez le genre de l'usager",
woman: "Femme",
man: "Homme",
neuter: "Neutre",
}
}, },
error_only_one_person: "Une seule personne peut être sélectionnée !" error_only_one_person: "Une seule personne peut être sélectionnée !"
} }

View File

@ -31,7 +31,7 @@
{% set gender = (p.person.gender == 'woman') ? 'fa-venus' : {% set gender = (p.person.gender == 'woman') ? 'fa-venus' :
(p.person.gender == 'man') ? 'fa-mars' : 'fa-neuter' %} (p.person.gender == 'man') ? 'fa-mars' : 'fa-neuter' %}
{% set genderTitle = (p.person.gender == 'woman') ? 'femme' : {% set genderTitle = (p.person.gender == 'woman') ? 'femme' :
(p.person.gender == 'homme') ? 'fa-mars' : 'neutre' %} (p.person.gender == 'man') ? 'homme' : 'neutre' %}
<i class="fa fa-fw {{ gender }}" title="{{ genderTitle }}"></i>{{ born ~ ' le ' ~ p.person.birthdate|format_date('short') }} <i class="fa fa-fw {{ gender }}" title="{{ genderTitle }}"></i>{{ born ~ ' le ' ~ p.person.birthdate|format_date('short') }}
</p> </p>
</div> </div>