Merge branch 'master' into notification_finitions

This commit is contained in:
Julien Fastré 2022-01-17 15:29:48 +01:00
commit 2ad798c0bf
36 changed files with 422 additions and 122 deletions

View File

@ -11,7 +11,14 @@ and this project adheres to
## Unreleased ## Unreleased
<!-- write down unreleased development here --> <!-- write down unreleased development here -->
* [main] Add editableByUser field to locationType entity, adapt the admin template and add this condition in the location-type endpoint (see https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/297)
* [main] Add mainLocation field to User entity and add it in user form type
* rewrite page which allow to select activity
* [main] Add mainLocation field to User entity and add it in user form type
* [course list in person context] show full username/label for ref
* [accompanying period work] remove the possibility to generate document from an accompanying period work
* vuejs: add validation on required fields for AddPerson, Address and Location components
* vuejs: treat 422 validation errors in locations and AddPerson components
## Test releases ## Test releases

View File

@ -9,8 +9,9 @@ div.new-activity-select-type {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: flex-start;
gap: 12px; gap: 12px;
margin-bottom: 30px;
div.bloc { div.bloc {
width: 200px; width: 200px;
@ -27,26 +28,26 @@ div.new-activity-select-type {
// precise dashboard specific details // precise dashboard specific details
p.date-label { p.date-label {
display: inline-block; display: inline-block;
margin: 0 0.5em 0 0; margin: 0 0.5em 0 0;
font-weight: 700; font-weight: 700;
font-size: 18pt; font-size: 18pt;
} }
div.dashboard, div.dashboard,
h2.badge-title { h2.badge-title {
ul.list-content { ul.list-content {
font-size: 70%; font-size: 70%;
list-style-type: none; list-style-type: none;
padding-left: 0; padding-left: 0;
margin: 0; margin: 0;
li { li {
margin-bottom: 0.2em; margin-bottom: 0.2em;
// exception: change bg color for action badges above dashboard // exception: change bg color for action badges above dashboard
.bg-light { .bg-light {
background-color: $chill-light-gray !important; background-color: $chill-light-gray !important;
} }
} }
} }
} }
//// ACTIVITY SHOW AND FORM PAGES //// ACTIVITY SHOW AND FORM PAGES

View File

@ -11,7 +11,7 @@ import Location from './components/Location.vue';
export default { export default {
name: "App", name: "App",
props: ['hasSocialIssues', 'hasLocation', 'hasPerson'], props: ['hasSocialIssues', 'hasLocation', 'hasPerson'],
components: { components: {
ConcernedGroups, ConcernedGroups,
SocialIssuesAcc, SocialIssuesAcc,

View File

@ -24,7 +24,7 @@
v-model="location" v-model="location"
> >
</VueMultiselect> </VueMultiselect>
<new-location v-bind:locations="locations"></new-location> <new-location v-bind:availableLocations="availableLocations"></new-location>
</div> </div>
</div> </div>
</teleport> </teleport>

View File

@ -18,15 +18,6 @@
</template> </template>
<template v-slot:body> <template v-slot:body>
<form> <form>
<div class="form-floating mb-3">
<p v-if="errors.length">
<b>{{ $t('activity.errors') }}</b>
<ul>
<li v-for="error in errors" :key="error">{{ error }}</li>
</ul>
</p>
</div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<select class="form-select form-select-lg" id="type" required v-model="selectType"> <select class="form-select form-select-lg" id="type" required v-model="selectType">
<option selected disabled value="">{{ $t('activity.choose_location_type') }}</option> <option selected disabled value="">{{ $t('activity.choose_location_type') }}</option>
@ -62,6 +53,12 @@
<input class="form-control form-control-lg" id="email" v-model="inputEmail" placeholder /> <input class="form-control form-control-lg" id="email" v-model="inputEmail" placeholder />
<label for="email">{{ $t('activity.location_fields.email') }}</label> <label for="email">{{ $t('activity.location_fields.email') }}</label>
</div> </div>
<div class="alert alert-warning" v-if="errors.length">
<ul>
<li v-for="(e, i) in errors" :key="i">{{ e }}</li>
</ul>
</div>
</form> </form>
</template> </template>
<template v-slot:footer> <template v-slot:footer>
@ -81,7 +78,8 @@
import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue'; import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue';
import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue"; import AddAddress from "ChillMainAssets/vuejs/Address/components/AddAddress.vue";
import { mapState } from "vuex"; import { mapState } from "vuex";
import { getLocationTypes, postLocation } from "../../api"; import { getLocationTypes } from "../../api";
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
export default { export default {
name: "NewLocation", name: "NewLocation",
@ -89,7 +87,7 @@ export default {
Modal, Modal,
AddAddress, AddAddress,
}, },
props: ['locations'], props: ['availableLocations'],
data() { data() {
return { return {
errors: [], errors: [],
@ -223,7 +221,6 @@ export default {
}, },
saveNewLocation() { saveNewLocation() {
if (this.checkForm()) { if (this.checkForm()) {
console.log('saveNewLocation', this.selected);
let body = { let body = {
type: 'location', type: 'location',
name: this.selected.name, name: this.selected.name,
@ -242,23 +239,28 @@ export default {
} }
}); });
} }
postLocation(body)
.then( makeFetch('POST', '/api/1.0/main/location.json', body)
location => new Promise(resolve => { .then(response => {
this.locations.push(location); this.$store.dispatch('addAvailableLocationGroup', {
this.$store.dispatch('updateLocation', location); locationGroup: 'Localisations nouvellement créées',
resolve(); locations: [response]
this.modal.showModal = false; });
}) this.$store.dispatch('updateLocation', response);
).catch( this.modal.showModal = false;
err => { })
this.errors.push(err.message); .catch((error) => {
if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.errors.push(v);
}
} else {
this.errors.push('An error occurred');
} }
); })
}; };
}, },
submitNewAddress(payload) { submitNewAddress(payload) {
console.log('submitNewAddress', payload);
this.selected.addressId = payload.addressId; this.selected.addressId = payload.addressId;
this.addAddress.context.addressId = payload.addressId; this.addAddress.context.addressId = payload.addressId;
this.addAddress.context.edit = true; this.addAddress.context.edit = true;

View File

@ -12,7 +12,11 @@ const hasLocation = document.querySelector('#location') !== null;
const hasPerson = document.querySelector('#add-persons') !== null; const hasPerson = document.querySelector('#add-persons') !== null;
const app = createApp({ const app = createApp({
template: `<app :hasSocialIssues="hasSocialIssues", :hasLocation="hasLocation", :hasPerson="hasPerson"></app>`, template: `<app
:hasSocialIssues="hasSocialIssues"
:hasLocation="hasLocation"
:hasPerson="hasPerson"
></app>`,
data() { data() {
return { return {
hasSocialIssues, hasSocialIssues,

View File

@ -240,6 +240,9 @@ const store = createStore({
}); });
commit("updateActionsSelected", payload); commit("updateActionsSelected", payload);
}, },
addAvailableLocationGroup({ commit }, payload) {
commit("addAvailableLocationGroup", payload);
},
addPersonsInvolved({ commit }, payload) { addPersonsInvolved({ commit }, payload) {
//console.log('### action addPersonsInvolved', payload.result.type); //console.log('### action addPersonsInvolved', payload.result.type);
switch (payload.result.type) { switch (payload.result.type) {

View File

@ -25,7 +25,7 @@
'activityData': activityData 'activityData': activityData
}) }}"> }) }}">
<div class="bloc btn btn-primary btn-lg btn-block"> <div class="btn btn-primary">
{{ activityType.name|localize_translatable_string }} {{ activityType.name|localize_translatable_string }}
</div> </div>
</a> </a>

View File

@ -24,6 +24,7 @@ class LocationTypeApiController extends ApiController
$query->andWhere( $query->andWhere(
$query->expr()->andX( $query->expr()->andX(
$query->expr()->eq('e.availableForUsers', "'TRUE'"), $query->expr()->eq('e.availableForUsers', "'TRUE'"),
$query->expr()->eq('e.editableByUsers', "'TRUE'"),
$query->expr()->eq('e.active', "'TRUE'"), $query->expr()->eq('e.active', "'TRUE'"),
) )
); );

View File

@ -67,6 +67,12 @@ class LocationType
*/ */
private ?string $defaultFor = null; private ?string $defaultFor = null;
/**
* @ORM\Column(type="boolean")
* @Serializer\Groups({"read"})
*/
private bool $editableByUsers = true;
/** /**
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
@ -107,6 +113,11 @@ class LocationType
return $this->defaultFor; return $this->defaultFor;
} }
public function getEditableByUsers(): ?bool
{
return $this->editableByUsers;
}
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
@ -152,6 +163,13 @@ class LocationType
return $this; return $this;
} }
public function setEditableByUsers(bool $editableByUsers): self
{
$this->editableByUsers = $editableByUsers;
return $this;
}
public function setTitle(array $title): self public function setTitle(array $title): self
{ {
$this->title = $title; $this->title = $title;

View File

@ -97,6 +97,11 @@ class User implements AdvancedUserInterface
*/ */
private ?Center $mainCenter = null; private ?Center $mainCenter = null;
/**
* @ORM\ManyToOne(targetEntity=Location::class)
*/
private ?Location $mainLocation = null;
/** /**
* @ORM\ManyToOne(targetEntity=Scope::class) * @ORM\ManyToOne(targetEntity=Scope::class)
*/ */
@ -228,6 +233,11 @@ class User implements AdvancedUserInterface
return $this->mainCenter; return $this->mainCenter;
} }
public function getMainLocation(): ?Location
{
return $this->mainLocation;
}
public function getMainScope(): ?Scope public function getMainScope(): ?Scope
{ {
return $this->mainScope; return $this->mainScope;
@ -405,6 +415,13 @@ class User implements AdvancedUserInterface
return $this; return $this;
} }
public function setMainLocation(?Location $mainLocation): User
{
$this->mainLocation = $mainLocation;
return $this;
}
public function setMainScope(?Scope $mainScope): User public function setMainScope(?Scope $mainScope): User
{ {
$this->mainScope = $mainScope; $this->mainScope = $mainScope;

View File

@ -39,6 +39,17 @@ final class LocationTypeType extends AbstractType
'expanded' => true, 'expanded' => true,
] ]
) )
->add(
'editableByUsers',
ChoiceType::class,
[
'choices' => [
'Yes' => true,
'No' => false,
],
'expanded' => true,
]
)
->add( ->add(
'addressRequired', 'addressRequired',
ChoiceType::class, ChoiceType::class,

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form; namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelper;
@ -75,6 +76,22 @@ class UserType extends AbstractType
'choice_label' => function (UserJob $c) { 'choice_label' => function (UserJob $c) {
return $this->translatableStringHelper->localize($c->getLabel()); return $this->translatableStringHelper->localize($c->getLabel());
}, },
])
->add('mainLocation', EntityType::class, [
'label' => 'Main location',
'required' => false,
'placeholder' => 'choose a location',
'class' => Location::class,
'choice_label' => function (Location $l) {
return $this->translatableStringHelper->localize($l->getLocationType()->getTitle()) . ' - ' . $l->getName();
},
'query_builder' => static function (EntityRepository $er) {
$qb = $er->createQueryBuilder('l');
$qb->orderBy('l.locationType');
$qb->where('l.availableForUsers = TRUE');
return $qb;
},
]); ]);
if ($options['is_creation']) { if ($options['is_creation']) {

View File

@ -423,3 +423,8 @@ span.item-key {
background-color: #0000000a; background-color: #0000000a;
//text-decoration: dotted underline; //text-decoration: dotted underline;
} }
// increase toast message z-index (above all modals)
div.v-toast {
z-index: 10000!important;
}

View File

@ -85,7 +85,9 @@ const fetchScopes = () => {
const ValidationException = (response) => { const ValidationException = (response) => {
const error = {}; const error = {};
error.name = 'ValidationException'; error.name = 'ValidationException';
error.violations = response.violations.map((violation) => `${violation.title}`); error.violations = response.violations.map((violation) => `${violation.title}: ${violation.propertyPath}`);
error.titles = response.violations.map((violation) => violation.title);
error.propertyPaths = response.violations.map((violation) => violation.propertyPath);
return error; return error;
} }

View File

@ -98,6 +98,8 @@
v-bind:defaultz="this.defaultz" v-bind:defaultz="this.defaultz"
v-bind:entity="this.entity" v-bind:entity="this.entity"
v-bind:flag="this.flag" v-bind:flag="this.flag"
v-bind:errors="this.errors"
v-bind:checkErrors="this.checkErrors"
@getCities="getCities" @getCities="getCities"
@getReferenceAddresses="getReferenceAddresses"> @getReferenceAddresses="getReferenceAddresses">
</edit-pane> </edit-pane>
@ -123,6 +125,8 @@
v-bind:defaultz="this.defaultz" v-bind:defaultz="this.defaultz"
v-bind:entity="this.entity" v-bind:entity="this.entity"
v-bind:flag="this.flag" v-bind:flag="this.flag"
v-bind:errors="this.errors"
v-bind:checkErrors="this.checkErrors"
v-bind:insideModal="false" v-bind:insideModal="false"
@getCities="getCities" @getCities="getCities"
@getReferenceAddresses="getReferenceAddresses"> @getReferenceAddresses="getReferenceAddresses">
@ -256,8 +260,10 @@ export default {
editPane: false, editPane: false,
datePane: false, datePane: false,
loading: false, loading: false,
success: false success: false,
dirty: false
}, },
errors: [],
defaultz: { defaultz: {
button: { button: {
text: { create: 'add_an_address_title', edit: 'edit_address' }, text: { create: 'add_an_address_title', edit: 'edit_address' },
@ -529,6 +535,23 @@ export default {
}); });
}, },
checkErrors() {
this.errors = [];
if (this.flag.dirty) {
if (this.entity.selected.country === null) {
this.errors.push("Un pays doit être sélectionné.");
}
if (Object.keys(this.entity.selected.city).length === 0) {
this.errors.push("Une ville doit être sélectionnée.");
}
if (!this.entity.selected.isNoAddress) {
if (this.entity.selected.address.street === null || this.entity.selected.address.streetNumber === null) {
this.errors.push("Une adresse doit être sélectionnée.");
}
}
}
},
/* /*
* Make form ready for new changes * Make form ready for new changes
*/ */

View File

@ -12,6 +12,7 @@
@search-change="listenInputSearch" @search-change="listenInputSearch"
ref="addressSelector" ref="addressSelector"
@select="selectAddress" @select="selectAddress"
@remove="remove"
name="field" name="field"
track-by="id" track-by="id"
label="value" label="value"
@ -56,7 +57,7 @@ import { searchReferenceAddresses, fetchReferenceAddresses } from '../../api.js'
export default { export default {
name: 'AddressSelection', name: 'AddressSelection',
components: { VueMultiselect }, components: { VueMultiselect },
props: ['entity', 'context', 'updateMapCenter'], props: ['entity', 'context', 'updateMapCenter', 'flag', 'checkErrors'],
data() { data() {
return { return {
value: this.context.edit ? this.entity.address.addressReference : null, value: this.context.edit ? this.entity.address.addressReference : null,
@ -109,6 +110,13 @@ export default {
this.entity.selected.address.streetNumber = value.streetNumber; this.entity.selected.address.streetNumber = value.streetNumber;
this.entity.selected.writeNew.address = false; this.entity.selected.writeNew.address = false;
this.updateMapCenter(value.point); this.updateMapCenter(value.point);
this.flag.dirty = true;
this.checkErrors();
},
remove() {
this.flag.dirty = true;
this.entity.selected.address = {};
this.checkErrors();
}, },
listenInputSearch(query) { listenInputSearch(query) {
//console.log('listenInputSearch', query, this.isAddressSelectorOpen); //console.log('listenInputSearch', query, this.isAddressSelectorOpen);
@ -149,6 +157,8 @@ export default {
this.entity.selected.address.street = addr.street; this.entity.selected.address.street = addr.street;
this.entity.selected.address.streetNumber = addr.number; this.entity.selected.address.streetNumber = addr.number;
this.entity.selected.writeNew.address = true; this.entity.selected.writeNew.address = true;
this.flag.dirty = true;
this.checkErrors();
} }
}, },
splitAddress(address) { splitAddress(address) {

View File

@ -7,6 +7,7 @@
@search-change="listenInputSearch" @search-change="listenInputSearch"
ref="citySelector" ref="citySelector"
@select="selectCity" @select="selectCity"
@remove="remove"
name="field" name="field"
track-by="id" track-by="id"
label="value" label="value"
@ -55,12 +56,12 @@ import { searchCities, fetchCities } from '../../api.js';
export default { export default {
name: 'CitySelection', name: 'CitySelection',
components: { VueMultiselect }, components: { VueMultiselect },
props: ['entity', 'context', 'focusOnAddress', 'updateMapCenter'], props: ['entity', 'context', 'focusOnAddress', 'updateMapCenter', 'flag', 'checkErrors'],
emits: ['getReferenceAddresses'], emits: ['getReferenceAddresses'],
data() { data() {
return { return {
value: this.context.edit ? this.entity.address.postcode : null, value: this.context.edit ? this.entity.address.postcode : null,
isLoading: false isLoading: false,
} }
}, },
computed: { computed: {
@ -123,6 +124,13 @@ export default {
if (value.center) { if (value.center) {
this.updateMapCenter(value.center); this.updateMapCenter(value.center);
} }
this.flag.dirty = true;
this.checkErrors();
},
remove() {
this.flag.dirty = true;
this.entity.selected.city = {};
this.checkErrors();
}, },
listenInputSearch(query) { listenInputSearch(query) {
if (query.length > 2) { if (query.length > 2) {

View File

@ -12,7 +12,9 @@
:select-label="$t('multiselect.select_label')" :select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')" :deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')" :selected-label="$t('multiselect.selected_label')"
@select="selectCountry"> @select="selectCountry"
@remove="remove"
>
</VueMultiselect> </VueMultiselect>
</div> </div>
</template> </template>
@ -23,7 +25,7 @@ import VueMultiselect from 'vue-multiselect';
export default { export default {
name: 'CountrySelection', name: 'CountrySelection',
components: { VueMultiselect }, components: { VueMultiselect },
props: ['context', 'entity'], props: ['context', 'entity', 'flag', 'checkErrors'],
emits: ['getCities'], emits: ['getCities'],
data() { data() {
return { return {
@ -34,14 +36,13 @@ export default {
}, },
computed: { computed: {
sortedCountries() { sortedCountries() {
//console.log('sorted countries');
const countries = this.entity.loaded.countries; const countries = this.entity.loaded.countries;
let sortedCountries = []; let sortedCountries = [];
sortedCountries.push(...countries.filter(c => c.countryCode === 'FR')) sortedCountries.push(...countries.filter(c => c.countryCode === 'FR'))
sortedCountries.push(...countries.filter(c => c.countryCode === 'BE')) sortedCountries.push(...countries.filter(c => c.countryCode === 'BE'))
sortedCountries.push(...countries.filter(c => c.countryCode !== 'FR').filter(c => c.countryCode !== 'BE')) sortedCountries.push(...countries.filter(c => c.countryCode !== 'FR').filter(c => c.countryCode !== 'BE'))
return sortedCountries; return sortedCountries;
} },
}, },
mounted() { mounted() {
this.init(); this.init();
@ -50,6 +51,7 @@ export default {
init() { init() {
if (this.value !== undefined) { if (this.value !== undefined) {
this.selectCountry(this.value); this.selectCountry(this.value);
this.flag.dirty = false;
} }
}, },
selectCountryByCode(countryCode) { selectCountryByCode(countryCode) {
@ -62,7 +64,13 @@ export default {
//console.log('select country', value); //console.log('select country', value);
this.entity.selected.country = value; this.entity.selected.country = value;
this.$emit('getCities', value); this.$emit('getCities', value);
} this.checkErrors();
},
remove() {
this.flag.dirty = true;
this.entity.selected.country = null;
this.checkErrors();
},
} }
}; };

View File

@ -7,6 +7,12 @@
<span class="sr-only">Loading...</span> <span class="sr-only">Loading...</span>
</div> </div>
<div v-if="errors.length" class="alert alert-warning" >
<ul>
<li v-for="(e, i) in errors" :key="i">{{ e }}</li>
</ul>
</div>
<h4 class="h3">{{ $t('select_an_address_title') }}</h4> <h4 class="h3">{{ $t('select_an_address_title') }}</h4>
<div class="row my-3"> <div class="row my-3">
<div class="col-lg-6"> <div class="col-lg-6">
@ -25,6 +31,8 @@
<country-selection <country-selection
v-bind:context="context" v-bind:context="context"
v-bind:entity="entity" v-bind:entity="entity"
v-bind:flag="flag"
v-bind:checkErrors="checkErrors"
@getCities="$emit('getCities', selected.country)"> @getCities="$emit('getCities', selected.country)">
</country-selection> </country-selection>
@ -33,13 +41,17 @@
v-bind:context="context" v-bind:context="context"
v-bind:focusOnAddress="focusOnAddress" v-bind:focusOnAddress="focusOnAddress"
v-bind:updateMapCenter="updateMapCenter" v-bind:updateMapCenter="updateMapCenter"
v-bind:flag="flag"
v-bind:checkErrors="checkErrors"
@getReferenceAddresses="$emit('getReferenceAddresses', selected.city)"> @getReferenceAddresses="$emit('getReferenceAddresses', selected.city)">
</city-selection> </city-selection>
<address-selection v-if="!isNoAddress" <address-selection v-if="!isNoAddress"
v-bind:entity="entity" v-bind:entity="entity"
v-bind:context="context" v-bind:context="context"
v-bind:updateMapCenter="updateMapCenter"> v-bind:updateMapCenter="updateMapCenter"
v-bind:flag="flag"
v-bind:checkErrors="checkErrors">
</address-selection> </address-selection>
</div> </div>
@ -99,7 +111,9 @@ export default {
'flag', 'flag',
'entity', 'entity',
'errorMsg', 'errorMsg',
'insideModal' 'insideModal',
'errors',
'checkErrors',
], ],
emits: ['getCities', 'getReferenceAddresses'], emits: ['getCities', 'getReferenceAddresses'],
data() { data() {
@ -128,7 +142,7 @@ export default {
get() { get() {
return this.entity.selected.isNoAddress; return this.entity.selected.isNoAddress;
} }
} },
}, },
methods: { methods: {
focusOnAddress() { focusOnAddress() {

View File

@ -90,7 +90,7 @@ export default {
OnTheFlyThirdparty, OnTheFlyThirdparty,
OnTheFlyCreate OnTheFlyCreate
}, },
props: ['type', 'id', 'action', 'buttonText', 'displayBadge', 'parent'], props: ['type', 'id', 'action', 'buttonText', 'displayBadge', 'parent', 'canCloseModal'],
emits: ['saveFormOnTheFly'], emits: ['saveFormOnTheFly'],
data() { data() {
return { return {
@ -162,7 +162,20 @@ export default {
return 'entity-' + this.type + ' badge-' + this.type; return 'entity-' + this.type + ' badge-' + this.type;
} }
}, },
watch: {
canCloseModal: {
handler: function(val, oldVal) {
if (val) {
this.closeModal();
}
},
deep: true
}
},
methods: { methods: {
closeModal() {
this.modal.showModal = false;
},
openModal() { openModal() {
//console.log('## OPEN ON THE FLY MODAL'); //console.log('## OPEN ON THE FLY MODAL');
//console.log('## type:', this.type, ', action:', this.action); //console.log('## type:', this.type, ', action:', this.action);
@ -200,8 +213,6 @@ export default {
// pass datas to parent // pass datas to parent
this.$emit('saveFormOnTheFly', { type: type, data: data }); this.$emit('saveFormOnTheFly', { type: type, data: data });
this.modal.showModal = false;
}, },
buildLocation(id, type) { buildLocation(id, type) {
if (type === 'person') { if (type === 'person') {

View File

@ -8,6 +8,7 @@
<tr> <tr>
<th>{{ 'Title'|trans }}</th> <th>{{ 'Title'|trans }}</th>
<th>{{ 'Available for users'|trans }}</th> <th>{{ 'Available for users'|trans }}</th>
<th>{{ 'Editable by users'|trans }}</th>
<th>{{ 'Address required'|trans }}</th> <th>{{ 'Address required'|trans }}</th>
<th>{{ 'Contact data'|trans }}</th> <th>{{ 'Contact data'|trans }}</th>
<th>{{ 'Active'|trans }}</th> <th>{{ 'Active'|trans }}</th>
@ -25,6 +26,13 @@
<i class="fa fa-square-o"></i> <i class="fa fa-square-o"></i>
{%- endif -%} {%- endif -%}
</td> </td>
<td style="text-align:center;">
{%- if entity.editableByUsers -%}
<i class="fa fa-check-square-o"></i>
{%- else -%}
<i class="fa fa-square-o"></i>
{%- endif -%}
</td>
<td>{{ entity.addressRequired|trans }}</td> <td>{{ entity.addressRequired|trans }}</td>
<td>{{ entity.contactData|trans }}</td> <td>{{ entity.contactData|trans }}</td>
<td style="text-align:center;"> <td style="text-align:center;">

View File

@ -58,7 +58,7 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
); );
$locationContext = array_merge( $locationContext = array_merge(
$context, $context,
['docgen:expects' => Location::class, 'groups' => 'dogen:read'] ['docgen:expects' => Location::class, 'groups' => 'docgen:read']
); );
if (null === $user && 'docgen' === $format) { if (null === $user && 'docgen' === $format) {
@ -67,6 +67,7 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
'main_center' => $this->normalizer->normalize(null, $format, $centerContext), 'main_center' => $this->normalizer->normalize(null, $format, $centerContext),
'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext), 'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext),
'current_location' => $this->normalizer->normalize(null, $format, $locationContext), 'current_location' => $this->normalizer->normalize(null, $format, $locationContext),
'main_location' => $this->normalizer->normalize(null, $format, $locationContext),
]); ]);
} }
@ -84,6 +85,7 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
if ('docgen' === $format) { if ('docgen' === $format) {
$data['current_location'] = $this->normalizer->normalize($user->getCurrentLocation(), $format, $locationContext); $data['current_location'] = $this->normalizer->normalize($user->getCurrentLocation(), $format, $locationContext);
$data['main_location'] = $this->normalizer->normalize($user->getMainLocation(), $format, $locationContext);
} }
return $data; return $data;

View File

@ -0,0 +1,36 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add editableByUsers field to ChillMain/LocationType.
*/
final class Version20220112150413 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_location_type DROP editableByUsers');
}
public function getDescription(): string
{
return 'Add editableByUsers field to ChillMain/LocationType';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_location_type ADD editableByUsers BOOLEAN DEFAULT TRUE');
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add mainLocation to User.
*/
final class Version20220112161136 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE users DROP CONSTRAINT FK_1483A5E9DB622A42');
$this->addSql('DROP INDEX IDX_1483A5E9DB622A42');
$this->addSql('ALTER TABLE users DROP mainLocation_id');
}
public function getDescription(): string
{
return 'Add mainLocation to User';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE users ADD mainLocation_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE users ADD CONSTRAINT FK_1483A5E9DB622A42 FOREIGN KEY (mainLocation_id) REFERENCES chill_main_location (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_1483A5E9DB622A42 ON users (mainLocation_id)');
}
}

View File

@ -202,6 +202,7 @@ Location: Localisation
Location type list: Liste des types de localisation Location type list: Liste des types de localisation
Create a new location type: Créer un nouveau type de localisation Create a new location type: Créer un nouveau type de localisation
Available for users: Disponible aux utilisateurs Available for users: Disponible aux utilisateurs
Editable by users: Éditable par les utilisateurs
Address required: Adresse requise? Address required: Adresse requise?
Contact data: Données de contact? Contact data: Données de contact?
optional: optionnel optional: optionnel

View File

@ -151,7 +151,6 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var DateTime * @var DateTime
* *
* @ORM\Column(type="date", nullable=true) * @ORM\Column(type="date", nullable=true)
* @Assert\Date
* @Birthdate * @Birthdate
*/ */
private $birthdate; private $birthdate;
@ -259,7 +258,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string * @var string
* *
* @ORM\Column(type="string", length=255) * @ORM\Column(type="string", length=255)
* @Assert\NotBlank * @Assert\NotBlank(message="The firstname cannot be empty")
* @Assert\Length( * @Assert\Length(
* max=255, * max=255,
* ) * )
@ -282,7 +281,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string * @var string
* *
* @ORM\Column(type="string", length=9, nullable=true) * @ORM\Column(type="string", length=9, nullable=true)
* @Assert\NotNull * @Assert\NotNull(message="The gender must be set")
*/ */
private $gender; private $gender;
@ -326,7 +325,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string * @var string
* *
* @ORM\Column(type="string", length=255) * @ORM\Column(type="string", length=255)
* @Assert\NotBlank * @Assert\NotBlank(message="The lastname cannot be empty")
* @Assert\Length( * @Assert\Length(
* max=255, * max=255,
* ) * )

View File

@ -184,5 +184,9 @@ div[class*='activity-'] {
background-color: $chill-ll-gray; background-color: $chill-ll-gray;
color: $chill-blue; color: $chill-blue;
} }
&.bg-confidential {
background-color: $chill-ll-gray;
color: $chill-red;
}
} }

View File

@ -233,18 +233,6 @@
</ul> </ul>
</div> </div>
<div>
<pick-template
entityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork"
:templates="this.templatesAvailablesForAction"
:entityId="work.id"
:beforeMove="beforeGenerateTemplate">
<template v-slot:title>
<h3>{{ $t('Generate doc') }}</h3>
</template>
</pick-template>
</div>
<div v-if="errors.length > 0" id="errors" class="alert alert-danger flashbag"> <div v-if="errors.length > 0" id="errors" class="alert alert-danger flashbag">
<p>{{ $t('fix_these_errors') }}</p> <p>{{ $t('fix_these_errors') }}</p>
<ul> <ul>
@ -458,10 +446,6 @@ export default {
submit() { submit() {
this.$store.dispatch('submit'); this.$store.dispatch('submit');
}, },
beforeGenerateTemplate() {
console.log('before generate');
return Promise.resolve();
},
saveFormOnTheFly(payload) { saveFormOnTheFly(payload) {
console.log('saveFormOnTheFly: type', payload.type, ', data', payload.data); console.log('saveFormOnTheFly: type', payload.type, ', data', payload.data);
payload.target = 'resource'; payload.target = 'resource';

View File

@ -66,9 +66,10 @@
<div class="create-button"> <div class="create-button">
<on-the-fly <on-the-fly
v-if="query.length >= 3" v-if="query.length >= 3"
v-bind:buttonText="$t('onthefly.create.button', {q: query})" :buttonText="$t('onthefly.create.button', {q: query})"
action="create" action="create"
@saveFormOnTheFly="saveFormOnTheFly"> @saveFormOnTheFly="saveFormOnTheFly"
:canCloseModal="canCloseOnTheFlyModal">
</on-the-fly> </on-the-fly>
</div> </div>
@ -91,8 +92,7 @@ import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue'; import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
import PersonSuggestion from './AddPersons/PersonSuggestion'; import PersonSuggestion from './AddPersons/PersonSuggestion';
import { searchEntities } from 'ChillPersonAssets/vuejs/_api/AddPersons'; import { searchEntities } from 'ChillPersonAssets/vuejs/_api/AddPersons';
import { postPerson } from "ChillPersonAssets/vuejs/_api/OnTheFly"; import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
import { postThirdparty } from "ChillThirdPartyAssets/vuejs/_api/OnTheFly";
export default { export default {
name: 'AddPersons', name: 'AddPersons',
@ -120,7 +120,8 @@ export default {
suggested: [], suggested: [],
selected: [], selected: [],
priorSuggestion: {} priorSuggestion: {}
} },
canCloseOnTheFlyModal: false
} }
}, },
computed: { computed: {
@ -267,22 +268,36 @@ export default {
saveFormOnTheFly({ type, data }) { saveFormOnTheFly({ type, data }) {
console.log('saveFormOnTheFly from addPersons, type', type, ', data', data); console.log('saveFormOnTheFly from addPersons, type', type, ', data', data);
if (type === 'person') { if (type === 'person') {
console.log('type person with', data); makeFetch('POST', '/api/1.0/person/person.json', data)
postPerson(data) .then(response => {
.then(person => new Promise((resolve, reject) => { this.newPriorSuggestion(response);
console.log('onthefly create: post person', person); this.canCloseOnTheFlyModal = true;
this.newPriorSuggestion(person); })
resolve(); .catch((error) => {
})); if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.$toast.open({message: v });
}
} else {
this.$toast.open({message: 'An error occurred'});
}
})
} }
else if (type === 'thirdparty') { else if (type === 'thirdparty') {
console.log('type thirdparty with', data); makeFetch('POST', '/api/1.0/thirdparty/thirdparty.json', data)
postThirdparty(data) .then(response => {
.then(thirdparty => new Promise((resolve, reject) => { this.newPriorSuggestion(response);
console.log('onthefly create: post thirdparty', thirdparty); this.canCloseOnTheFlyModal = true;
this.newPriorSuggestion(thirdparty); })
resolve(); .catch((error) => {
})); if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.$toast.open({message: v });
}
} else {
this.$toast.open({message: 'An error occurred'});
}
})
} }
} }
}, },

View File

@ -22,24 +22,45 @@
<div v-else-if="action === 'edit' || action === 'create'"> <div v-else-if="action === 'edit' || action === 'create'">
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input class="form-control form-control-lg" id="lastname" v-model="lastName" v-bind:placeholder="$t('person.lastname')" /> <input
class="form-control form-control-lg"
id="lastname"
v-model="lastName"
:placeholder="$t('person.lastname')"
@change="checkErrors"
/>
<label for="lastname">{{ $t('person.lastname') }}</label> <label for="lastname">{{ $t('person.lastname') }}</label>
</div> </div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input class="form-control form-control-lg" id="firstname" v-model="firstName" v-bind:placeholder="$t('person.firstname')" /> <input
class="form-control form-control-lg"
id="firstname"
v-model="firstName"
:placeholder="$t('person.firstname')"
@change="checkErrors"
/>
<label for="firstname">{{ $t('person.firstname') }}</label> <label for="firstname">{{ $t('person.firstname') }}</label>
</div> </div>
<div v-for="(a) in config.altNames" :key="a.key" class="form-floating mb-3"> <div v-for="(a) in config.altNames" :key="a.key" class="form-floating mb-3">
<input class="form-control form-control-lg" :id="a.key" @input="onAltNameInput" /> <input
class="form-control form-control-lg"
:id="a.key"
@input="onAltNameInput"
/>
<label :for="a.key">{{ a.labels.fr }}</label> <label :for="a.key">{{ a.labels.fr }}</label>
</div> </div>
<!-- TODO fix placeholder if undefined <!-- TODO fix placeholder if undefined
--> -->
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<select class="form-select form-select-lg" id="gender" v-model="gender"> <select
class="form-select form-select-lg"
id="gender"
v-model="gender"
@change="checkErrors"
>
<option selected disabled >{{ $t('person.gender.placeholder') }}</option> <option selected disabled >{{ $t('person.gender.placeholder') }}</option>
<option value="woman">{{ $t('person.gender.woman') }}</option> <option value="woman">{{ $t('person.gender.woman') }}</option>
<option value="man">{{ $t('person.gender.man') }}</option> <option value="man">{{ $t('person.gender.man') }}</option>
@ -62,8 +83,8 @@
<span class="input-group-text" id="phonenumber"><i class="fa fa-fw fa-phone"></i></span> <span class="input-group-text" id="phonenumber"><i class="fa fa-fw fa-phone"></i></span>
<input class="form-control form-control-lg" <input class="form-control form-control-lg"
v-model="phonenumber" v-model="phonenumber"
v-bind:placeholder="$t('person.phonenumber')" :placeholder="$t('person.phonenumber')"
v-bind:aria-label="$t('person.phonenumber')" :aria-label="$t('person.phonenumber')"
aria-describedby="phonenumber" /> aria-describedby="phonenumber" />
</div> </div>
@ -71,8 +92,8 @@
<span class="input-group-text" id="mobilenumber"><i class="fa fa-fw fa-mobile"></i></span> <span class="input-group-text" id="mobilenumber"><i class="fa fa-fw fa-mobile"></i></span>
<input class="form-control form-control-lg" <input class="form-control form-control-lg"
v-model="mobilenumber" v-model="mobilenumber"
v-bind:placeholder="$t('person.mobilenumber')" :placeholder="$t('person.mobilenumber')"
v-bind:aria-label="$t('person.mobilenumber')" :aria-label="$t('person.mobilenumber')"
aria-describedby="mobilenumber" /> aria-describedby="mobilenumber" />
</div> </div>
@ -80,11 +101,17 @@
<span class="input-group-text" id="email"><i class="fa fa-fw fa-at"></i></span> <span class="input-group-text" id="email"><i class="fa fa-fw fa-at"></i></span>
<input class="form-control form-control-lg" <input class="form-control form-control-lg"
v-model="email" v-model="email"
v-bind:placeholder="$t('person.email')" :placeholder="$t('person.email')"
v-bind:aria-label="$t('person.email')" :aria-label="$t('person.email')"
aria-describedby="email" /> aria-describedby="email" />
</div> </div>
<div class="alert alert-warning" v-if="errors.length">
<ul>
<li v-for="(e, i) in errors" :key="i">{{ e }}</li>
</ul>
</div>
</div> </div>
</template> </template>
@ -108,6 +135,7 @@ export default {
config: { config: {
altNames: [] altNames: []
}, },
errors: []
} }
}, },
computed: { computed: {
@ -183,6 +211,18 @@ export default {
} }
}, },
methods: { methods: {
checkErrors(e) {
this.errors = [];
if (!this.person.lastName) {
this.errors.push("Le nom ne doit pas être vide.");
}
if (!this.person.firstName) {
this.errors.push("Le prénom ne doit pas être vide.");
}
if (!this.person.gender) {
this.errors.push("Le genre doit être renseigné");
}
},
loadData() { loadData() {
getPerson(this.id) getPerson(this.id)
.then(person => new Promise((resolve, reject) => { .then(person => new Promise((resolve, reject) => {

View File

@ -96,7 +96,9 @@
'type': 'accompanying_period', 'type': 'accompanying_period',
'id': accompanyingCourse.id 'id': accompanyingCourse.id
} }
} %} }
}
%}
</span> </span>
</div> </div>
</div> </div>

View File

@ -11,7 +11,7 @@
<span class="badge rounded-pill bg-danger">{{- 'Emergency'|trans|upper -}}</span> <span class="badge rounded-pill bg-danger">{{- 'Emergency'|trans|upper -}}</span>
{% endif %} {% endif %}
{% if period.confidential %} {% if period.confidential %}
<span class="badge rounded-pill bg-danger">{{- 'Confidential'|trans|upper -}}</span> <span class="badge rounded-pill bg-confidential">{{- 'Confidential'|trans|upper -}}</span>
{% endif %} {% endif %}
</div> </div>
<div class="wh-col"> <div class="wh-col">
@ -45,7 +45,7 @@
{% if chill_accompanying_periods.fields.user == 'visible' %} {% if chill_accompanying_periods.fields.user == 'visible' %}
{% if period.user %} {% if period.user %}
<abbr class="referrer" title="{{ 'Referrer'|trans }}">{{ 'Referrer'|trans }}:</abbr> <abbr class="referrer" title="{{ 'Referrer'|trans }}">{{ 'Referrer'|trans }}:</abbr>
{{ period.user.username|chill_entity_render_box }} {{ period.user|chill_entity_render_box }}
{% else %} {% else %}
<span class="chill-no-data-statement">{{ 'No accompanying user'|trans }}</span> <span class="chill-no-data-statement">{{ 'No accompanying user'|trans }}</span>
{% endif %} {% endif %}

View File

@ -80,7 +80,14 @@
{% set app = person.findParticipationForPeriod(acp) %} {% set app = person.findParticipationForPeriod(acp) %}
<div class="wl-row separator"> <div class="wl-row separator">
<div class="wl-col title"> <div class="wl-col title">
<div>
{% if acp.emergency %}
<span class="badge rounded-pill bg-danger">{{- 'Emergency'|trans|upper -}}</span>
{% endif %}
{% if acp.confidential %}
<span class="badge rounded-pill bg-confidential">{{- 'Confidential'|trans|upper -}}</span>
{% endif %}
</div>
{% if acp.step == 'DRAFT' %} {% if acp.step == 'DRAFT' %}
<div class="is-draft"> <div class="is-draft">
<span class="course-draft badge bg-secondary" title="{{ 'course.draft'|trans }}">{{ 'course.draft'|trans }}</span> <span class="course-draft badge bg-secondary" title="{{ 'course.draft'|trans }}">{{ 'course.draft'|trans }}</span>

View File

@ -21,10 +21,7 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class AccompanyingPeriodWorkContext implements class AccompanyingPeriodWorkContext
DocGeneratorContextInterface,
DocGeneratorContextWithAdminFormInterface,
DocGeneratorContextWithPublicFormInterface
{ {
private NormalizerInterface $normalizer; private NormalizerInterface $normalizer;

View File

@ -16,6 +16,9 @@ The birthdate must be before %date%: La date de naissance doit être avant le %d
'Invalid phone number: it should begin with the international prefix starting with "+", hold only digits and be smaller than 20 characters. Ex: +33623456789': 'Numéro de téléphone invalide: il doit commencer par le préfixe international précédé de "+", ne comporter que des chiffres et faire moins de 20 caractères. Ex: +33623456789' 'Invalid phone number: it should begin with the international prefix starting with "+", hold only digits and be smaller than 20 characters. Ex: +33623456789': 'Numéro de téléphone invalide: il doit commencer par le préfixe international précédé de "+", ne comporter que des chiffres et faire moins de 20 caractères. Ex: +33623456789'
'The email is not valid': 'Le courriel n''est pas valide' 'The email is not valid': 'Le courriel n''est pas valide'
Two addresses has the same validFrom date: La date de validité est identique à celle d'une autre adresse Two addresses has the same validFrom date: La date de validité est identique à celle d'une autre adresse
The firstname cannot be empty: Le prénom ne peut pas être vide
The lastname cannot be empty: Le nom de famille ne peut pas être vide
The gender must be set: Le genre doit être renseigné
#export list #export list
You must select at least one element: Vous devez sélectionner au moins un élément You must select at least one element: Vous devez sélectionner au moins un élément