Merge branch 'master' into 'feature/add-requestor-in-search-results'

# Conflicts:
#   CHANGELOG.md
This commit is contained in:
Julien Fastré 2021-10-03 19:16:17 +00:00
commit 5f0238f614
58 changed files with 868 additions and 439 deletions

View File

@ -21,7 +21,13 @@ and this project adheres to
* https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/13
* https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/199
* [Person form] "accept sms" not required:
https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/37
https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/221
## Test release yyyy-mm-dd
* On-The-Fly modale works for showing, editing and creating person and thirdparty ;
* AccompanyingCourse Resume page: list associated persons by household, see household when hover, and show on-the-fly modale when clicking on person ;

View File

@ -4,9 +4,9 @@ const fetchScopes = () => {
return response.json();
}
}).then(data => {
console.log(data);
//console.log(data);
return new Promise((resolve, reject) => {
console.log(data);
//console.log(data);
resolve(data.results);
});
});

View File

@ -1,17 +1,18 @@
<template>
<ul class="record_actions"
<ul class="record_actions" v-if="!options.onlyButton"
:class="{ 'sticky-form-buttons': isStickyForm }">
<li v-if="isStickyForm" class="cancel">
<slot name="before"></slot>
</li>
<slot name="action"></slot>
<li>
<slot name="action"></slot>
</li>
<li v-if="isStickyForm">
<slot name="after"></slot>
</li>
</ul>
<slot v-else name="action"></slot>
</template>
<script>
@ -23,9 +24,6 @@ export default {
return (typeof this.options.stickyActions !== 'undefined') ?
this.options.stickyActions : this.defaultz.stickyActions;
},
},
methods: {
}
}
</script>

View File

@ -271,7 +271,7 @@ export default {
validFrom: false,
validTo: false
},
hideAddress: false
onlyButton: false
},
entity: {
address: {}, // <== loaded and returned
@ -325,11 +325,10 @@ export default {
return (this.validFrom || this.validTo) ? true : false;
},
hasSuggestions() {
console.log(this.context.suggestions);
if (typeof(this.context.suggestions) !== 'undefined') {
return this.context.suggestions.length > 0;
console.log('hasSuggestions', this.context.suggestions);
return this.context.suggestions.length > 0;
}
//return addressSuggestions.length > 0
return false;
},
displaySuggestions() {
@ -354,9 +353,9 @@ export default {
},
mounted() {
console.log('validFrom', this.validFrom);
console.log('validTo', this.validTo);
console.log('useDatePane', this.useDatePane);
//console.log('validFrom', this.validFrom);
//console.log('validTo', this.validTo);
//console.log('useDatePane', this.useDatePane);
console.log('Mounted now !');
if (this.context.edit) {
@ -752,19 +751,6 @@ export default {
this.closeSuggestPane();
});
}
/*
* Method just add closing pane to the callback method
* (get out step1 show pane, submit button)
closePaneAndCallbackSubmit(payload)
{
//this.initForm();
//this.resetPane(); // because parent callback will cast afterLastPaneAction()
console.log('will call parent callback method', payload);
// callback props method from parent
this.addressChangedCallback(payload);
}
*/
}
}
</script>

View File

@ -1,6 +1,6 @@
<template>
<div v-if="!hideAddress">
<div v-if="!onlyButton">
<div class="loading">
<i v-if="flag.loading" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw"></i>
<span class="sr-only">{{ $t('loading') }}</span>
@ -28,13 +28,11 @@
:options="this.options"
:defaultz="this.defaultz">
<template v-slot:action>
<li>
<button @click.prevent="$emit('openEditPane')"
class="btn" :class="getClassButton"
type="button" name="button" :title="$t(getTextButton)">
<span v-if="displayTextButton">{{ $t(getTextButton) }}</span>
</button>
</li>
<button @click.prevent="$emit('openEditPane')"
class="btn" :class="getClassButton"
type="button" name="button" :title="$t(getTextButton)">
<span v-if="displayTextButton">{{ $t(getTextButton) }}</span>
</button>
</template>
</action-buttons>
@ -86,9 +84,9 @@ export default {
getSuccessText() {
return (this.context.edit) ? 'address_edit_success' : 'address_new_success';
},
hideAddress() {
return (typeof this.options.hideAddress !== 'undefined') ?
this.options.hideAddress : this.defaultz.hideAddress;
onlyButton() {
return (typeof this.options.onlyButton !== 'undefined') ?
this.options.onlyButton : this.defaultz.onlyButton;
},
forceRedirect() {
return (!(this.context.backUrl === null || typeof this.context.backUrl === 'undefined'));

View File

@ -53,7 +53,7 @@ containers.forEach((container) => {
},
/// Don't display show renderbox Address: showPane display only a button
hideAddress: container.dataset.hideAddress === 'true' //boolean, default: false
onlyButton: container.dataset.onlyButton === 'true' //boolean, default: false
}
}
}

View File

@ -0,0 +1,41 @@
<template>
<on-the-fly
:type="context.type"
:id="context.id"
:action="context.action"
:buttonText="options.buttonText"
:displayBadge="options.displayBadge === 'true'"
@saveFormOnTheFly="saveFormOnTheFly">
</on-the-fly>
</template>
<script>
import OnTheFly from './components/OnTheFly.vue';
export default {
name: "App",
components: {
OnTheFly
},
props: ['onTheFly'],
computed: {
context() {
return this.onTheFly.context;
},
options() {
return this.onTheFly.options;
}
},
mounted() {
console.log('OnTheFly mounted');
console.log('OnTheFly: data context', this.context);
console.log('OnTheFly: data options', this.options);
},
methods: {
saveFormOnTheFly(payload) {
console.log('saveFormOnTheFly', payload);
}
}
}
</script>

View File

@ -3,7 +3,7 @@
<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">
<input type="radio" name="person" id="person" v-model="radioType" value="person">
{{ $t('onthefly.create.person') }}
</label>
</a>
@ -11,7 +11,7 @@
<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">
<input type="radio" name="thirdparty" id="thirdparty" v-model="radioType" value="thirdparty">
{{ $t('onthefly.create.thirdparty') }}
</label>
</a>
@ -56,6 +56,7 @@ export default {
radioType: {
set(type) {
this.type = type;
console.log('## type:', type, ', action:', this.action);
},
get() {
return this.type;
@ -71,7 +72,10 @@ export default {
case 'person':
return this.$refs.castPerson.$data.person;
case 'thirdparty':
return this.$refs.castThirdparty.$data.thirdparty;
let data = this.$refs.castThirdparty.$data.thirdparty;
data.name = data.text;
data.address = { id: data.address.address_id }
return data;
default:
throw Error('Invalid type of entity')
}

View File

@ -1,6 +1,11 @@
<template>
<a class="btn btn-sm" target="_blank"
<a v-if="isDisplayBadge" @click="openModal">
<span class="chill-entity" :class="badgeType">
{{ buttonText }}
</span>
</a>
<a v-else class="btn btn-sm" target="_blank"
:class="classAction"
:title="$t(titleAction)"
@click="openModal">
@ -42,16 +47,16 @@
</template>
<template v-slot:footer>
<button v-if="action === 'show'"
@click="goToLocation(id, type)"
<a v-if="action === 'show'"
:href="buildLocation(id, type)"
:title="$t(titleMessage)"
class="btn btn-show">{{ $t(buttonMessage) }}
</button>
<button v-else
</a>
<a v-else
class="btn btn-save"
@click="saveAction">
{{ $t('action.save')}}
</button>
</a>
</template>
</modal>
@ -61,7 +66,7 @@
<script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue';
import OnTheFlyCreate from './OnTheFly/Create.vue';
import OnTheFlyCreate from './Create.vue';
import OnTheFlyPerson from 'ChillPersonAssets/vuejs/_components/OnTheFly/Person.vue';
import OnTheFlyThirdparty from 'ChillThirdPartyAssets/vuejs/_components/OnTheFly/ThirdParty.vue';
@ -73,7 +78,7 @@ export default {
OnTheFlyThirdparty,
OnTheFlyCreate
},
props: ['type', 'id', 'action', 'buttonText'],
props: ['type', 'id', 'action', 'buttonText', 'displayBadge'],
emits: ['saveFormOnTheFly'],
data() {
return {
@ -123,17 +128,25 @@ export default {
return 'action.redirect.' + this.type;
}
},
buttonMessage(){
buttonMessage() {
switch (this.type){
case 'person':
return 'onthefly.show.file_' + this.type;
case 'thirdparty':
return 'onthefly.show.file_' + this.type;
}
},
isDisplayBadge() {
return (this.displayBadge === true && this.buttonText !== null);
},
badgeType() {
return 'entity-' + this.type + ' badge-' + this.type;
}
},
methods: {
openModal() {
console.log('## OPEN ON THE FLY MODAL');
console.log('## type:', this.type, ', action:', this.action);
this.modal.showModal = true;
this.$nextTick(function() {
//this.$refs.search.focus();
@ -144,7 +157,6 @@ export default {
},
saveAction() {
console.log('saveAction button: create/edit action with', this.type);
let
type = this.type,
data = {} ;
@ -159,10 +171,9 @@ export default {
break;
default:
if (typeof this.type === 'undefined') {
if (typeof this.type === 'undefined') { // action=create
type = this.$refs.castNew.radioType;
data = this.$refs.castNew.castDataByType();
} else {
throw 'error with object type';
}
@ -173,11 +184,12 @@ export default {
this.modal.showModal = false;
},
goToLocation(id, type){
if(type == 'person'){
window.location = `../../person/${id}/general`
} else if(type == 'thirdparty') {
window.location = `../../thirdparty/thirdparty/${id}/show`
buildLocation(id, type) {
if (type === 'person') {
// TODO i18n
return `/fr/person/${id}/general`;
} else if (type === 'thirdparty') {
return `/fr/thirdparty/thirdparty/${id}/show`;
}
}
}
@ -188,5 +200,4 @@ export default {
a {
cursor: pointer;
}
</style>

View File

@ -0,0 +1,24 @@
const ontheflyMessages = {
fr: {
onthefly: {
show: {
person: "Détails de l'usager",
thirdparty: "Détails du tiers",
file_person: "Ouvrir la fiche de l'usager",
file_thirdparty: "Voir le Tiers",
},
edit: {
person: "Modifier un usager",
thirdparty: "Modifier un tiers"
},
create: {
button: "Créer \"{q}\"",
title: "Création d'un nouvel usager ou d'un tiers professionnel",
person: "un nouvel usager",
thirdparty: "un nouveau tiers professionnel"
},
}
}
}
export { ontheflyMessages };

View File

@ -0,0 +1,35 @@
import { createApp } from "vue";
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
import { ontheflyMessages } from './i18n.js';
import App from "./App.vue";
const i18n = _createI18n( ontheflyMessages );
let containers = document.querySelectorAll('.onthefly-container');
containers.forEach((container) => {
const app = createApp({
template: `<app :onTheFly="this.onTheFly" ></app>`,
data() {
return {
onTheFly: {
context: {
action: container.dataset.action,
type: container.dataset.targetName,
id: parseInt(container.dataset.targetId),
},
options: {
buttonText: container.dataset.buttonText || null,
displayBadge: container.dataset.displayBadge || false
}
}
}
}
})
.use(i18n)
.component('app', App)
.mount(container);
//console.log('container dataset', container.dataset);
});

View File

@ -53,24 +53,6 @@ const messages = {
top: "Haut",
bottom: "Bas",
},
onthefly: {
show: {
person: "Détails de l'usager",
thirdparty: "Détails du tiers",
file_person: "Ouvrir la fiche de l'usager",
file_thirdparty: "Voir le Tiers",
},
edit: {
person: "Modifier un usager",
thirdparty: "Modifier un tiers"
},
create: {
button: "Créer \"{q}\"",
title: "Création d'un nouvel usager ou d'un tiers professionnel",
person: "un nouvel usager",
thirdparty: "un nouveau tiers professionnel"
},
},
renderbox: {
person: "Usager",
birthday: {

View File

@ -18,7 +18,7 @@
* stickyActions bool (default: false)
* useValidFrom bool (default: false)
* useValidTo bool (default: false)
* hideAddress bool (default: false)
* onlyButton bool (default: false)
#}
<div class="address-container"
@ -69,7 +69,7 @@
data-use-valid-to="true"
{% endif %}
{% if hideAddress is defined and hideAddress == 1 %}
{% if onlyButton is defined and onlyButton == 1 %}
data-hide-address="true"
{% endif %}
></div>

View File

@ -0,0 +1,37 @@
{#
This Twig template include load vue_onthefly component.
It push all variables from context in OnTheFly/App.vue.
OPTIONS
* targetEntity {
name: string 'person', 'thirdparty'
id: integer
}
* action string 'show', 'edit', 'create'
* buttonText string
* displayBadge boolean (default: false) replace button by badge, need to define buttonText for content
#}
<span class="onthefly-container"
data-target-name="{{ targetEntity.name|e('html_attr') }}"
data-target-id="{{ targetEntity.id|e('html_attr') }}"
{% if action is defined %}
data-action="{{ action|e('html_attr') }}"
{% else %}
data-action="show"
{% endif %}
{% if buttonText is defined %}
data-button-text="{{ buttonText|e('html_attr') }}"
{% endif %}
{% if displayBadge is defined and displayBadge == 1 %}
data-display-badge="true"
{% endif %}
></span>
{{ encore_entry_script_tags('vue_onthefly') }}
{{ encore_entry_link_tags('vue_onthefly') }}

View File

@ -61,5 +61,6 @@ module.exports = function(encore, entries)
// Vue entrypoints
encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js');
encore.addEntry('vue_onthefly', __dirname + '/Resources/public/vuejs/OnTheFly/index.js');
};

View File

@ -118,6 +118,7 @@ class AccompanyingCourseController extends Controller
return $this->render('@ChillPerson/AccompanyingCourse/index.html.twig', [
'accompanyingCourse' => $accompanyingCourse,
'withoutHousehold' => $withoutHousehold,
'participationsByHousehold' => $accompanyingCourse->actualParticipationsByHousehold(),
'works' => $works,
'activities' => $activities
]);

View File

@ -45,9 +45,9 @@ class PersonApiController extends ApiController
$person = parent::createEntity($action, $request);
// TODO temporary hack to allow creation of person with fake center
$centers = $this->authorizationHelper->getReachableCenters($this->getUser(),
/* $centers = $this->authorizationHelper->getReachableCenters($this->getUser(),
new Role(PersonVoter::CREATE));
$person->setCenter($centers[0]);
$person->setCenter($centers[0]); */
return $person;
}

View File

@ -640,12 +640,13 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
Request::METHOD_POST=> true,
Request::METHOD_PATCH => true
],
'roles' => [
Request::METHOD_GET => \Chill\PersonBundle\Security\Authorization\PersonVoter::SEE,
Request::METHOD_HEAD => \Chill\PersonBundle\Security\Authorization\PersonVoter::SEE,
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\PersonVoter::CREATE,
Request::METHOD_PATCH => \Chill\PersonBundle\Security\Authorization\PersonVoter::CREATE,
],
],
'address' => [

View File

@ -513,6 +513,44 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
);
}
/**
* Return an array with open participations sorted by household
* [
* [
* "household" => Household x,
* "members" => [
* Participation y , Participation z, ...
* ]
* ],
* ]
*
*/
public function actualParticipationsByHousehold(): array
{
$participations = $this->getOPenParticipations()->toArray();
$households = [];
foreach ($participations as $p) {
$households[] = $p->getPerson()->getCurrentHousehold();
}
$households = array_unique($households, SORT_REGULAR);
$array = [];
foreach ($households as $household) {
$members = [];
foreach ($participations as $p) {
if ($household === $p->getPerson()->getCurrentHousehold()) {
$members[] = array_shift($participations);
} else {
$participations[] = array_shift($participations);
}
}
$array[] = [ 'household' => $household, 'members' => $members ];
}
return $array;
}
/**
* Return true if the accompanying period contains a person.
*

View File

@ -129,8 +129,7 @@ class PersonType extends AbstractType
$builder
->add('mobilenumber', TelType::class, array('required' => false))
->add('acceptSMS', CheckboxType::class, array(
'value' => false,
'required' => true
'required' => false
));
}

View File

@ -247,3 +247,28 @@ span.fa-holder {
font-family: "Open Sans Extrabold";
}
}
div.accompanyingcourse-resume {
div.associated-persons {
span.household {
display: inline-block;
border-radius: 8px;
border: 1px solid $white;
&:hover {
border: 1px solid $chill-beige;
i {
display: inline-block;
}
}
&.no-household:hover {
border: 1px solid $white;
}
i {
color: $chill-beige;
display: none;
}
padding: 0.3em;
margin-right: 2px;
}
}
}

View File

@ -14,18 +14,6 @@
</label>
</div>
<div v-if="hasNoPersonLocation" class="alert alert-danger no-person-location">
<i class="fa fa-warning fa-2x"></i>
<div>
<p>
{{ $t('courselocation.associate_at_least_one_person_with_one_household_with_address') }}
<a href="#section-10">
<i class="fa fa-level-up fa-fw"></i>
</a>
</p>
</div>
</div>
<div class="flex-table" v-if="accompanyingCourse.location">
<div class="item-bloc">
<address-render-box
@ -47,6 +35,18 @@
</div>
</div>
<div v-if="hasNoPersonLocation" class="alert alert-danger no-person-location">
<i class="fa fa-warning fa-2x"></i>
<div>
<p>
{{ $t('courselocation.associate_at_least_one_person_with_one_household_with_address') }}
<a href="#section-10">
<i class="fa fa-level-up fa-fw"></i>
</a>
</p>
</div>
</div>
<div>
<ul class="record_actions">
<li>
@ -102,7 +102,7 @@ export default {
create: 'courselocation.add_temporary_address',
edit: 'courselocation.edit_temporary_address'
},
hideAddress: true
onlyButton: true
}
}
}
@ -202,9 +202,9 @@ export default {
created() {
this.initAddressContext();
console.log('ac.locationStatus', this.accompanyingCourse.locationStatus);
console.log('ac.location (temporary location)', this.accompanyingCourse.location);
console.log('ac.personLocation', this.accompanyingCourse.personLocation);
//console.log('ac.locationStatus', this.accompanyingCourse.locationStatus);
//console.log('ac.location (temporary location)', this.accompanyingCourse.location);
//console.log('ac.personLocation', this.accompanyingCourse.personLocation);
}
}
</script>
@ -213,10 +213,10 @@ export default {
div#accompanying-course {
div.vue-component {
& > div.alert.no-person-location {
margin: 0 0 -1em;
margin: 1px 0 0;
}
div.no-person-location {
padding-bottom: 1.5em;
padding-top: 1.5em;
display: flex;
flex-direction: row;
& > i {

View File

@ -19,20 +19,8 @@
v-if="hasCurrentHouseholdAddress"
v-bind:person="participation.person">
</button-location>
<li>
<on-the-fly
v-bind:type="participation.person.type"
v-bind:id="participation.person.id"
action="show">
</on-the-fly>
</li>
<li>
<on-the-fly
v-bind:type="participation.person.type"
v-bind:id="participation.person.id"
action="edit">
</on-the-fly>
</li>
<li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="show"></on-the-fly></li>
<li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly"></on-the-fly></li>
<!-- <li>
<button class="btn btn-delete"
:title="$t('action.delete')"
@ -69,7 +57,7 @@
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
import ButtonLocation from '../ButtonLocation.vue';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
@ -112,22 +100,13 @@ export default {
getAccompanyingCourseReturnPath() {
return `fr/parcours/${this.$store.state.accompanyingCourse.id}/edit#section-10`;
}
},
methods: {
saveFormOnTheFly(payload) {
console.log('saveFormOnTheFly: type', payload.type, ', data', payload.data);
payload.target = 'participation';
this.$store.dispatch('patchOnTheFly', payload);
}
}
}
/*
* dates of participation
*
*
*
* <tr>
* <td><span v-if="participation.startDate">
* {{ $d(participation.startDate.datetime, 'short') }}</span>
* </td>
* <td><span v-if="participation.endDate">
* {{ $d(participation.endDate.datetime, 'short') }}</span>
* </td>
* </tr>
*
*/
</script>

View File

@ -10,7 +10,7 @@
{{ $t('requestor.is_anonymous') }}
</label>
<third-party-render-box v-if="accompanyingCourse.requestor.type == 'thirdparty'"
<third-party-render-box v-if="accompanyingCourse.requestor.type === 'thirdparty'"
:thirdparty="accompanyingCourse.requestor"
:options="{
addLink: false,
@ -23,14 +23,13 @@
>
<template v-slot:record-actions>
<ul class="record_actions">
<button-location v-if="hasCurrentHouseholdAddress" :thirdparty="accompanyingCourse.requestor"></button-location>
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="show"></on-the-fly></li>
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="edit"></on-the-fly></li>
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly"></on-the-fly></li>
</ul>
</template>
</third-party-render-box>
<person-render-box render="bloc" v-else-if="accompanyingCourse.requestor.type == 'person'"
<person-render-box render="bloc" v-else-if="accompanyingCourse.requestor.type === 'person'"
:person="accompanyingCourse.requestor"
:options="{
addLink: false,
@ -44,9 +43,8 @@
>
<template v-slot:record-actions>
<ul class="record_actions">
<button-location v-if="hasCurrentHouseholdAddress" :person="accompanyingCourse.requestor"></button-location>
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="show"></on-the-fly></li>
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="edit"></on-the-fly></li>
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly"></on-the-fly></li>
</ul>
</template>
</person-render-box>
@ -81,7 +79,7 @@
<script>
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
import PersonRenderBox from '../../_components/Entity/PersonRenderBox.vue';
import ThirdPartyRenderBox from 'ChillThirdPartyAssets/vuejs/_components/Entity/ThirdPartyRenderBox.vue';
@ -129,6 +127,11 @@ export default {
this.$store.dispatch('addRequestor', selected.shift());
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
},
saveFormOnTheFly(payload) {
console.log('saveFormOnTheFly: type', payload.type, ', data', payload.data);
payload.target = 'requestor';
this.$store.dispatch('patchOnTheFly', payload);
}
}
}

View File

@ -6,9 +6,11 @@
>
<template v-slot:record-actions>
<ul class="record_actions">
<!--
<button-location v-if="hasCurrentHouseholdAddress" :person="resource.resource"></button-location>
-->
<li><on-the-fly :type="resource.resource.type" :id="resource.resource.id" action="show"></on-the-fly></li>
<li><on-the-fly :type="resource.resource.type" :id="resource.resource.id" action="edit"></on-the-fly></li>
<li><on-the-fly :type="resource.resource.type" :id="resource.resource.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly"></on-the-fly></li>
<li><button class="btn btn-sm btn-remove" :title="$t('action.remove')" @click.prevent="$emit('remove', resource)"></button></li>
</ul>
</template>
@ -22,7 +24,7 @@
<template v-slot:record-actions>
<ul class="record_actions">
<li><on-the-fly :type="resource.resource.type" :id="resource.resource.id" action="show"></on-the-fly></li>
<li><on-the-fly :type="resource.resource.type" :id="resource.resource.id" action="edit"></on-the-fly></li>
<li><on-the-fly :type="resource.resource.type" :id="resource.resource.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly"></on-the-fly></li>
<li><button class="btn btn-sm btn-remove" :title="$t('action.remove')" @click.prevent="$emit('remove', resource)"></button></li>
</ul>
</template>
@ -31,7 +33,7 @@
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
import ButtonLocation from '../ButtonLocation.vue';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import ThirdPartyRenderBox from 'ChillThirdPartyAssets/vuejs/_components/Entity/ThirdPartyRenderBox.vue';
@ -53,6 +55,13 @@ export default {
}
return false;
}
},
methods: {
saveFormOnTheFly(payload) {
console.log('saveFormOnTheFly: type', payload.type, ', data', payload.data);
payload.target = 'resource';
this.$store.dispatch('patchOnTheFly', payload);
}
}
}
</script>

View File

@ -1,5 +1,7 @@
import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n'
import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n';
import { thirdpartyMessages } from 'ChillThirdPartyAssets/vuejs/_js/i18n';
import { addressMessages } from 'ChillMainAssets/vuejs/Address/i18n';
import { ontheflyMessages } from 'ChillMainAssets/vuejs/OnTheFly/i18n';
const appMessages = {
fr: {
@ -48,7 +50,7 @@ const appMessages = {
ok: "Oui, l'usager quitte le parcours",
show_household_number: "Voir le ménage (n° {id})",
show_household: "Voir le ménage",
person_without_household_warning: "Certaines personnes n'appartiennent à aucun ménage actuellement. Renseignez leur appartenance à un ménage dès que possible.",
person_without_household_warning: "Certaines usagers n'appartiennent actuellement à aucun ménage. Renseignez leur appartenance dès que possible.",
update_household: "Modifier l'appartenance",
participation_not_valid: "Sélectionnez ou créez au minimum 1 usager",
},
@ -82,7 +84,7 @@ const appMessages = {
assign_course_address: "Désigner comme l'adresse du parcours",
remove_button: "Enlever l'adresse",
temporary_address_must_be_changed: "Cette adresse est temporaire. Le parcours devrait être localisé auprès d'un usager concerné.",
associate_at_least_one_person_with_one_household_with_address: "Associez au moins un membre du parcours à un ménage, et indiquez une adresse à ce ménage.",
associate_at_least_one_person_with_one_household_with_address: "Commencez d'abord par associer un membre du parcours à un ménage, et indiquez une adresse à ce ménage.",
sure: "Êtes-vous sûr ?",
sure_description: "Voulez-vous faire de cette adresse l'adresse du parcours ?",
ok: "Désigner comme adresse du parcours",
@ -141,7 +143,7 @@ const appMessages = {
}
};
Object.assign(appMessages.fr, personMessages.fr, addressMessages.fr);
Object.assign(appMessages.fr, personMessages.fr, thirdpartyMessages.fr, addressMessages.fr, ontheflyMessages.fr);
export {
appMessages

View File

@ -11,6 +11,8 @@ import { getAccompanyingCourse,
addScope,
removeScope,
} from '../api';
import { patchPerson } from "ChillPersonAssets/vuejs/_api/OnTheFly";
import { patchThirdparty } from "ChillThirdPartyAssets/vuejs/_api/OnTheFly";
const debug = process.env.NODE_ENV !== 'production';
@ -48,7 +50,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
return state.accompanyingCourse.location !== null;
},
isScopeValid(state) {
console.log('is scope valid', state.accompanyingCourse.scopes.length > 0);
//console.log('is scope valid', state.accompanyingCourse.scopes.length > 0);
return state.accompanyingCourse.scopes.length > 0;
},
validationKeys(state, getters) {
@ -107,6 +109,36 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
//console.log('### mutation: addResource', resource);
state.accompanyingCourse.resources.push(resource);
},
updatePerson(state, payload) {
console.log('### mutation: updatePerson', payload);
let i = null;
switch (payload.target) {
case 'participation':
i = state.accompanyingCourse.participations.findIndex(e => e.person.id === payload.person.id );
state.accompanyingCourse.participations[i].person = payload.person;
break;
case 'requestor':
state.accompanyingCourse.requestor = payload.person;
break;
case 'resource':
i = state.accompanyingCourse.resources.findIndex(e => e.resource.id === payload.person.id );
state.accompanyingCourse.resources[i].resource = payload.person;
break;
}
},
updateThirdparty(state, payload) {
console.log('### mutation: updateThirdparty', payload);
let i = null;
switch (payload.target) {
case 'requestor':
state.accompanyingCourse.requestor = payload.thirdparty;
break;
case 'resource':
i = state.accompanyingCourse.resources.findIndex(e => e.resource.id === payload.thirdparty.id );
state.accompanyingCourse.resources[i].resource = payload.thirdparty;
break;
}
},
toggleIntensity(state, value) {
state.accompanyingCourse.intensity = value;
},
@ -239,6 +271,38 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
resolve();
})).catch((error) => { commit('catchError', error) });
},
patchOnTheFly({ commit }, payload) {
console.log('## action: patch OnTheFly', payload);
let body = { type: payload.type };
if (payload.type === 'person') {
body.firstName = payload.data.firstName;
body.lastName = payload.data.lastName;
if (payload.data.birthdate !== null) { body.birthdate = payload.data.birthdate; }
body.phonenumber = payload.data.phonenumber;
body.mobilenumber = payload.data.mobilenumber;
body.gender = payload.data.gender;
console.log('id', payload.data.id, 'and body', body);
patchPerson(payload.data.id, body)
.then(person => new Promise((resolve, reject) => {
console.log('patch person', person);
commit('updatePerson', { target: payload.target, person: person });
resolve();
}));
}
else if (payload.type === 'thirdparty') {
body.name = payload.data.text;
body.email = payload.data.email;
body.telephone = payload.data.phonenumber;
body.address = { id: payload.data.address.address_id };
console.log('id', payload.data.id, 'and body', body);
patchThirdparty(payload.data.id, body)
.then(thirdparty => new Promise((resolve, reject) => {
console.log('patch thirdparty', thirdparty);
commit('updateThirdparty', { target: payload.target, thirdparty: thirdparty });
resolve();
}));
}
},
toggleIntensity({ commit }, payload) {
//console.log(payload);
patchAccompanyingCourse(id, { type: "accompanying_period", intensity: payload })

View File

@ -146,7 +146,7 @@ export default {
validFrom: false,
validTo: false,
},
hideAddress: true,
onlyButton: true,
button: {
text: {
create: 'household_members_editor.household.set_address',

View File

@ -27,8 +27,27 @@ const postPerson = (body) => {
throw Error('Error with request resource response');
});
};
export {
getPerson,
postPerson
/*
* PATCH an existing person
*/
const patchPerson = (id, body) => {
const url = `/api/1.0/person/person/${id}.json`;
return fetch(url, {
method: 'PATCH',
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,
patchPerson
};

View File

@ -88,10 +88,11 @@
<script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
import PersonSuggestion from './AddPersons/PersonSuggestion';
import { searchPersons, searchPersons_2 } from 'ChillPersonAssets/vuejs/_api/AddPersons';
import { postPerson } from "ChillPersonAssets/vuejs/_api/OnTheFly";
import { postThirdparty } from "ChillThirdPartyAssets/vuejs/_api/OnTheFly";
export default {
name: 'AddPersons',
@ -229,7 +230,7 @@ export default {
return item.result.type + item.result.id;
},
addPriorSuggestion() {
console.log('echo', this.hasPriorSuggestion);
//console.log('addPriorSuggestion', this.hasPriorSuggestion);
if (this.hasPriorSuggestion) {
console.log('addPriorSuggestion',);
this.suggested.unshift(this.priorSuggestion);
@ -248,31 +249,31 @@ export default {
result: entity
}
this.search.priorSuggestion = suggestion;
console.log('ici', this.search.priorSuggestion);
console.log('search priorSuggestion', this.search.priorSuggestion);
} else {
this.search.priorSuggestion = {};
}
},
saveFormOnTheFly({ type, data }) {
console.log('saveFormOnTheFly from addPersons', { type, data });
// create/edit person
console.log('saveFormOnTheFly from addPersons, type', type, ', data', data);
if (type === 'person') {
console.log('type person with', data);
postPerson(data)
.then(person => new Promise((resolve, reject) => {
//this.person = person;
console.log('onthefly create: post person', person);
this.newPriorSuggestion(person);
resolve();
}));
}
// create/edit thirdparty
else if (type === 'thirdparty') {
console.log('not yet implemented: type thirdparty with', type, data);
console.log('type thirdparty with', data);
postThirdparty(data)
.then(thirdparty => new Promise((resolve, reject) => {
console.log('onthefly create: post thirdparty', thirdparty);
this.newPriorSuggestion(thirdparty);
resolve();
}));
}
}
},
}

View File

@ -28,7 +28,7 @@
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
export default {
name: 'SuggestionPerson',

View File

@ -25,7 +25,7 @@
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
export default {
name: 'SuggestionThirdParty',

View File

@ -27,7 +27,7 @@
</div>
<p v-if="options.addInfo == true" class="moreinfo">
<p v-if="options.addInfo === true" class="moreinfo">
<i :class="'fa fa-fw ' + getGenderIcon" title="{{ getGender }}"></i>
<time v-if="person.birthdate && !person.deathdate" datetime="{{ person.birthdate }}" title="{{ birthdate }}">
{{ $t(getGenderTranslation) + ' ' + $d(birthdate, 'text') }}
@ -142,7 +142,7 @@ export default {
props: ['person', 'options', 'render', 'returnPath'],
computed: {
getGenderTranslation: function() {
return this.person.gender == 'woman' ? 'renderbox.birthday.woman' : 'renderbox.birthday.man';
return this.person.gender === 'woman' ? 'renderbox.birthday.woman' : 'renderbox.birthday.man';
},
isMultiline: function() {
if(this.options.isMultiline){
@ -152,7 +152,7 @@ export default {
}
},
getGenderIcon: function() {
return this.person.gender == 'woman' ? 'fa-venus' : this.person.gender == 'man' ? 'fa-mars' : 'fa-neuter';
return this.person.gender === 'woman' ? 'fa-venus' : this.person.gender === 'man' ? 'fa-mars' : 'fa-neuter';
},
birthdate: function(){
if(this.person.birthdate !== null){

View File

@ -40,7 +40,7 @@
<option value="man">{{ $t('person.gender.man') }}</option>
<option value="neuter">{{ $t('person.gender.neuter') }}</option>
</select>
<label for="gender">{{ $t('person.gender.title') }}</label>
<label>{{ $t('person.gender.title') }}</label>
</div>
<div class="input-group mb-3">
@ -75,7 +75,7 @@
</template>
<script>
import { getPerson, postPerson } from '../../_api/OnTheFly';
import { getPerson } from '../../_api/OnTheFly';
import PersonRenderBox from '../Entity/PersonRenderBox.vue';
export default {
@ -159,7 +159,7 @@ export default {
getPerson(this.id)
.then(person => new Promise((resolve, reject) => {
this.person = person;
//console.log('get person', this.person);
console.log('get person', this.person);
resolve();
}));
}

View File

@ -1,41 +1,16 @@
<div class="border border-warning">
<div class="alert alert-warning alert-with-actions mb-0">
<div class="message">
{{ 'Some peoples does not belong to any household currently. Add them to an household soon'|trans }}
</div>
<ul class="record_actions">
<li>
<button class="btn btn-chill-beige" data-bs-toggle="collapse" href="#withoutHouseholdList">
<i class="fa fa-fw fa-caret-down"></i><span class="">{{ 'Add to household now'|trans }}</span>
</button>
</li>
</ul>
</div>
<div id="withoutHouseholdList" class="collapse p-3">
<form method="GET"
action="{{ path('chill_person_household_members_editor') }}">
<h3>{{ 'household.Select people to move'|trans }}</h3>
<ul>
{% for p in withoutHousehold %}
<li>
<input type="checkbox" name="persons[]" value="{{ p.id }}" checked />
{{ p|chill_entity_render_box }}
</li>
{% endfor %}
</ul>
<input type="hidden" name="expand_suggestions" value="true" />
<input type="hidden" name="returnPath" value="{{ app.request.requestUri|escape('html_attr') }}" />
<input type="hidden" name="accompanying_period_id" value="{{ accompanyingCourse.id }}" />
<ul class="record_actions mb-0">
<div class="alert alert-warning alert-with-actions">
<div class="float-button bottom"><div class="box">
<div class="action">
<ul class="record_actions">
<li>
<button type="submit" class="btn btn-edit">
{{ 'household.Household editor'|trans }}
</button>
<a class="btn btn-sm btn-update change-icon"
href="{{ path('chill_person_accompanying_course_edit', { 'accompanying_period_id': accompanyingCourse.id, '_fragment': 'section-10' }) }}">
<i class="fa fa-fw fa-crosshairs"></i>
Corriger
</a>
</li>
</ul>
</form>
</div>
</div>
{{ 'Some peoples does not belong to any household currently. Add them to an household soon'|trans }}
</div></div>
</div>

View File

@ -0,0 +1,16 @@
<div class="alert alert-warning">
<div class="float-button bottom"><div class="box">
<div class="action">
<ul class="record_actions">
<li>
<a class="btn btn-sm btn-update change-icon"
href="{{ path('chill_person_accompanying_course_edit', { 'accompanying_period_id': accompanyingCourse.id, '_fragment': 'section-100' } ) }}">
<i class="fa fa-fw fa-crosshairs"></i>
{{ 'Edit & activate accompanying course'|trans }}
</a>
</li>
</ul>
</div>
<p>{{ 'This accompanying course is still a draft'|trans }}</p>
</div></div>
</div>

View File

@ -1,61 +1,23 @@
{%- set countPersonLocation = accompanyingCourse.availablePersonLocation|length -%}
{%- set hasPersonLocation = countPersonLocation|length > 0 -%}
{% macro quickLocationForm(accompanyingCourse, person, whichButton) %}
<form method="PATCH" action="{{ path('chill_api_single_accompanying_course__entity', {'id': accompanyingCourse.id, '_format': 'json'}) }}" class="quickLocationForm">
<input type="hidden" name="personLocation" value="{{ person.id }}" />
<input type="hidden" name="periodId" value="{{ accompanyingCourse.id }}" />
{% if whichButton == 'string' %}
<button type="submit" class="btn btn-chill-pink">
<span class="text-light">{{ 'Locate by'|trans }} {{ person|chill_entity_render_string }}</span>
</button>
{% elseif whichButton == 'icon' %}
<button type="submit" class="btn btn-sm btn-secondary">
<i class="fa fa-map-marker"></i>
</button>
{% endif %}
</form>
{% endmacro %}
<div class="border border-danger">
<div class="alert alert-danger {% if hasPersonLocation %}alert-with-actions{% endif %} mb-0">
<div class="message">
{{ 'This course is located at a temporarily address. You should locate this course to an user'|trans }}
{% if not hasPersonLocation %}
{{ 'Associate at least one member with an household, and set an address to this household'|trans }}
{% endif %}
</div>
{% if 1 == countPersonLocation %}
{%- set hasPersonLocation = countPersonLocation > 0 -%}
<div class="alert alert-danger {% if hasPersonLocation %}alert-with-actions{% endif %}">
<div class="float-button bottom"><div class="box">
<div class="action">
<ul class="record_actions">
<li>
{{ _self.quickLocationForm(accompanyingCourse, accompanyingCourse.availablePersonLocation.first, 'string') }}
<a class="btn btn-sm btn-update change-icon"
href="{{ path('chill_person_accompanying_course_edit', { 'accompanying_period_id': accompanyingCourse.id, '_fragment': 'section-20' }) }}">
<i class="fa fa-fw fa-crosshairs"></i>
Corriger
</a>
</li>
</ul>
{% elseif 1 < countPersonLocation %}
<ul class="record_actions">
<li>
<button class="btn btn-chill-pink" data-bs-toggle="collapse" href="#locateAtPerson">
<i class="fa fa-fw fa-caret-down"></i><span class="text-light">{{ 'Choose a person to locate by'|trans }}</span>
</button>
</li>
</ul>
{% endif %}
</div>
{% if 1 < countPersonLocation %}
<div id="locateAtPerson" class="collapse">
<p>{{ 'Locate by'|trans }}:</p>
<div class="flex-table mb-3">
{% for p in accompanyingCourse.availablePersonLocation %}
<div class="item-bloc">
{{ p|chill_entity_render_box({
'render': 'bloc', 'addLink': false, 'addInfo': true, 'addAltNames': false, 'customButtons': {
'replace': _self.quickLocationForm(accompanyingCourse, p, 'icon')
}
}) }}
</div>
{% endfor %}
</div>
</div>
{% endif %}
<p>
{{ 'This course is located at a temporarily address. You should locate this course to an user'|trans }}</p>
{% if not hasPersonLocation %}
<p>
{{ 'Associate at least one member with an household, and set an address to this household'|trans }}</p>
{% endif %}
</div></div>
</div>

View File

@ -21,22 +21,69 @@
{% endblock %}
{% block content %}
<div class="accompanyingcourse-resume">
<div class="accompanyingcourse-resume row">
<div class="associated-persons mb-5">
{% for h in participationsByHousehold %}
{% set householdClass = (h.household is not null) ? 'household-' ~ h.household.id : 'no-household alert alert-warning' %}
{% set householdTitle = (h.household is not null) ?
'household.Household number'|trans({'household_num': h.household.id }) : 'household.Never in any household'|trans %}
<span class="household {{ householdClass }}" title="{{ householdTitle }}">
{% if h.household is not null %}
<a href="{{ path('chill_person_household_summary', { 'household_id': h.household.id }) }}"
title="{{ 'household.Household number'|trans({'household_num': h.household.id }) }}"
><i class="fa fa-home fa-fw"></i></a>
{% endif %}
{% for p in h.members %}
{# include vue_onthefly component #}
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
targetEntity: { name: 'person', id: p.person.id },
action: 'show',
displayBadge: true,
buttonText: p.person|chill_entity_render_string
} %}
{% endfor %}
</span>
{% endfor %}
</div>
<div class="col-md-6 location mb-5">
{% if accompanyingCourse.locationStatus == 'person' %}
<h5>{{ 'This course is located by'|trans }}</h5>
<h4>{{ accompanyingCourse.personLocation|chill_entity_render_string }}</h4>
{% elseif accompanyingCourse.locationStatus == 'address' %}
<h4>{{ 'This course has a temporarily location'|trans }}</h4>
{% endif %}
{% if accompanyingCourse.locationStatus != 'none' %}
{{ accompanyingCourse.location|chill_entity_render_box }}
{% endif %}
</div>
{% if 'DRAFT' == accompanyingCourse.step %}
<div class="alert alert-danger flash_message mb-5">
<span>
{{ 'This accompanying course is still a draft'|trans }}
<a href="{{ path('chill_person_accompanying_course_edit', { 'accompanying_period_id': accompanyingCourse.id } ) }}">
{{ 'Edit & activate accompanying course'|trans }}
</a>
</span>
<div class="col-md-6 warnings mb-5">
{% include '@ChillPerson/AccompanyingCourse/_still_draft.html.twig' %}
</div>
{% endif %}
<pre>WIP .. AccompanyingCourse Resume dashboard</pre>
{% if accompanyingCourse.locationStatus == 'address' or accompanyingCourse.locationStatus == 'none' %}
<div class="col-md-6 warnings mb-5">
{% include '@ChillPerson/AccompanyingCourse/_warning_address.html.twig' %}
</div>
{% endif %}
{#
{% if 'DRAFT' != accompanyingCourse.step %}
{% if withoutHousehold|length > 0 %}
<div class="col-md-6 warnings mb-5">
{% include '@ChillPerson/AccompanyingCourse/_join_household.html.twig' %}
</div>
{% endif %}
{% endif %}
{# DISABLED
<h1>{{ 'Resume Accompanying Course'|trans }}</h1>
<div class="associated-persons mb-5">
@ -53,13 +100,6 @@
{% endif %}
{% endfor %}
</div>
#}
{% if 'DRAFT' != accompanyingCourse.step %}
{% if withoutHousehold|length > 0 %}
{% include '@ChillPerson/AccompanyingCourse/_join_household.html.twig' with {} %}
{% endif %}
{% endif %}
{#
</div>
<div class="location mb-5">
@ -80,11 +120,6 @@
</div>
</div>
{% endif %}
#}
{% if accompanyingCourse.locationStatus == 'address' or accompanyingCourse.locationStatus == 'none' %}
{% include '@ChillPerson/AccompanyingCourse/_warning_address.html.twig' with {} %}
{% endif %}
{#
</div>
<div class="requestor mb-5">

View File

@ -32,7 +32,7 @@
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'household', id: household.id },
backUrl: path('chill_person_household_summary', { 'household_id': household.id }),
hideAddress: true,
onlyButton: true,
mode: 'new',
buttonSize: 'btn-sm',
buttonText: 'Move household',

View File

@ -17,11 +17,19 @@
{%- endif -%}
</div>
<div class="text-md-end">
{% if person|chill_resolve_center is not null%}
{% if person|chill_resolve_center is not null %}
<span class="open_sansbold">
{{ 'Center'|trans|upper}} :
</span>
{{ person|chill_resolve_center.name|upper }}
{% if person|chill_resolve_center is iterable %}
{% for c in person|chill_resolve_center %}
{{ c.name|upper }}{% if not loop.last %}, {% endif %}
{% endfor %}
{% else %}
{{ person|chill_resolve_center.name|upper }}
{% endif %}
{%- endif -%}
</div>
</div>

View File

@ -298,6 +298,35 @@ paths:
$ref: "#/components/schemas/Person"
403:
description: "Unauthorized"
patch:
tags:
- person
summary: "Alter a person"
parameters:
- name: id
in: path
required: true
description: The person's id
schema:
type: integer
format: integer
minimum: 1
requestBody:
description: "A person"
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Person"
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "Object with validation errors"
/1.0/person/person.json:
post:

View File

@ -186,7 +186,7 @@ No accompanying user: Aucun accompagnant
No data given: Pas d'information
Participants: Personnes impliquées
Create an accompanying course: Créer un parcours
This accompanying course is still a draft: Ce parcours est à l'état brouillon
This accompanying course is still a draft: Ce parcours est encore à l'état brouillon.
Associated peoples: Usagers concernés
Resources: Interlocuteurs privilégiés
Any requestor to this accompanying course: Aucun demandeur pour ce parcours

View File

@ -18,7 +18,7 @@ use Chill\MainBundle\Pagination\PaginatorFactory;
/**
* Routes for operations on ThirdParties.
*
*
* @Route("/{_locale}/thirdparty/thirdparty")
*/
class ThirdPartyController extends Controller
@ -28,21 +28,21 @@ class ThirdPartyController extends Controller
* @var AuthorizationHelper
*/
protected $authorizationHelper;
/**
*
* @var TranslatorInterface
*/
protected $translator;
/**
*
* @var PaginatorFactory
*/
protected $paginatorFactory;
public function __construct(
AuthorizationHelper $authorizationHelper,
AuthorizationHelper $authorizationHelper,
TranslatorInterface $translator,
PaginatorFactory $paginatorFactory
) {
@ -51,7 +51,7 @@ class ThirdPartyController extends Controller
$this->paginatorFactory = $paginatorFactory;
}
/**
* @Route("/index", name="chill_3party_3party_index")
*/
@ -60,22 +60,12 @@ class ThirdPartyController extends Controller
$this->denyAccessUnlessGranted(ThirdPartyVoter::SHOW);
$repository = $this->getDoctrine()->getManager()
->getRepository(ThirdParty::class);
$centers = $this->authorizationHelper
->getReachableCenters(
$this->getUser(),
new Role(ThirdPartyVoter::SHOW)
);
$nbThirdParties = $repository->countByMemberOfCenters($centers);
$nbThirdParties = $repository->count([]); //$repository->countByMemberOfCenters($centers);
$pagination = $this->paginatorFactory->create($nbThirdParties);
$thirdParties = $repository->findByMemberOfCenters(
$centers,
$pagination->getCurrentPage()->getFirstItemNumber(),
$pagination->getItemsPerPage()
);
$thirdParties = $repository->findAll();
return $this->render('ChillThirdPartyBundle:ThirdParty:index.html.twig', array(
'third_parties' => $thirdParties,
'pagination' => $pagination
@ -88,46 +78,37 @@ class ThirdPartyController extends Controller
public function newAction(Request $request)
{
$this->denyAccessUnlessGranted(ThirdPartyVoter::CREATE);
$centers = $this->authorizationHelper
->getReachableCenters(
$this->getUser(),
new Role(ThirdPartyVoter::CREATE)
);
if (count($centers) === 0) {
throw new \LogicException("There should be at least one center reachable "
. "if role ".ThirdPartyVoter::CREATE." is granted");
}
$centers = [];
$thirdParty = new ThirdParty();
$thirdParty->setCenters(new ArrayCollection($centers));
$form = $this->createForm(ThirdPartyType::class, $thirdParty, [
'usage' => 'create'
]);
$form->add('submit', SubmitType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($thirdParty);
$em->flush();
$this->addFlash('success',
$this->translator->trans("Third party created")
);
return $this->redirectToRoute('chill_3party_3party_show', [
'thirdparty_id' => $thirdParty->getId()
]);
} elseif ($form->isSubmitted()) {
$msg = $this->translator->trans('This form contains errors');
$this->addFlash('error', $msg);
}
return $this->render('@ChillThirdParty/ThirdParty/new.html.twig', [
'form' => $form->createView(),
'thirdParty' => $thirdParty
@ -141,59 +122,53 @@ class ThirdPartyController extends Controller
public function updateAction(ThirdParty $thirdParty, Request $request)
{
$this->denyAccessUnlessGranted(ThirdPartyVoter::CREATE);
$centers = $this->authorizationHelper
->getReachableCenters(
$this->getUser(),
new Role(ThirdPartyVoter::CREATE)
);
if (count($centers) === 0) {
throw new \LogicException("There should be at least one center reachable "
. "if role ".ThirdPartyVoter::CREATE." is granted");
}
$repository = $this->getDoctrine()->getManager()
->getRepository(ThirdParty::class);
$centers = $repository->findAll();
// we want to keep centers the users has no access to. So we will add them
// later if they are removed. (this is a ugly hack but it will works
$centersAssociatedNotForUsers = \array_diff(
$thirdParty->getCenters()->toArray(),
$thirdParty->getCenters()->toArray(),
$centers);
$form = $this->createForm(ThirdPartyType::class, $thirdParty, [
'usage' => 'create'
]);
$form->add('submit', SubmitType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// re-add centers the user has no accesses:
foreach ($centersAssociatedNotForUsers as $c) {
$thirdParty->addCenter($c);
}
$em = $this->getDoctrine()->getManager();
$em->flush();
$this->addFlash('success',
$this->translator->trans("Third party updated")
);
return $this->redirectToRoute('chill_3party_3party_show', [
'thirdparty_id' => $thirdParty->getId()
]);
} elseif ($form->isSubmitted()) {
$msg = $this->translator->trans('This form contains errors');
$this->addFlash('error', $msg);
}
return $this->render('@ChillThirdParty/ThirdParty/update.html.twig', [
'form' => $form->createView(),
'thirdParty' => $thirdParty
]);
}
/**
* @Route("/{thirdparty_id}/show", name="chill_3party_3party_show")
* @ParamConverter("thirdParty", options={"id": "thirdparty_id"})
@ -201,7 +176,7 @@ class ThirdPartyController extends Controller
public function showAction(ThirdParty $thirdParty, Request $request)
{
$this->denyAccessUnlessGranted(ThirdPartyVoter::SHOW, $thirdParty);
return $this->render('@ChillThirdParty/ThirdParty/show.html.twig', [
'thirdParty' => $thirdParty
]);

View File

@ -59,27 +59,23 @@ class ChillThirdPartyExtension extends Extension implements PrependExtensionInte
'class' => \Chill\ThirdPartyBundle\Entity\ThirdParty::class,
'name' => 'thirdparty',
'base_path' => '/api/1.0/thirdparty/thirdparty',
'base_role' => \Chill\ThirdPartyBundle\Security\Authorization\ThirdPartyVoter::class,
//'base_role' => \Chill\ThirdPartyBundle\Security\Authorization\ThirdPartyVoter::SHOW,
//'controller' => \Chill\ThirdPartyBundle\Controller\ThirdPartyApiController::class,
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
Request::METHOD_POST => true,
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
Request::METHOD_POST=> true,
Request::METHOD_POST => true,
Request::METHOD_PUT => true,
Request::METHOD_PATCH => true
],
'roles' => [
Request::METHOD_GET => \Chill\ThirdPartyBundle\Security\Voter\ThirdPartyVoter::SHOW,
Request::METHOD_HEAD => \Chill\ThirdPartyBundle\Security\Voter\ThirdPartyVoter::SHOW,
Request::METHOD_POST => \Chill\ThirdPartyBundle\Security\Voter\ThirdPartyVoter::CREATE,
Request::METHOD_PUT => \Chill\ThirdPartyBundle\Security\Voter\ThirdPartyVoter::CREATE,
Request::METHOD_PATCH => \Chill\ThirdPartyBundle\Security\Voter\ThirdPartyVoter::CREATE
],
]
]

View File

@ -60,7 +60,7 @@ class ThirdParty
* @var string
* @ORM\Column(name="name", type="string", length=255)
* @Assert\Length(min="2")
* @Groups({"read"})
* @Groups({"read", "write"})
*/
private $name;
@ -69,6 +69,7 @@ class ThirdParty
* @var string
* @ORM\Column(name="name_company", type="string", length=255, nullable=true)
* @Assert\Length(min="3")
* @Groups({"read", "write"})
*/
private $nameCompany;
@ -77,6 +78,7 @@ class ThirdParty
* @var string
* @ORM\Column(name="acronym", type="string", length=64, nullable=true)
* @Assert\Length(min="2")
* @Groups({"read", "write"})
*/
private $acronym;
@ -94,7 +96,7 @@ class ThirdParty
* @ORM\Column(name="types", type="json", nullable=true)
* @Assert\Count(min=1)
*/
private $type;
private $types;
/**
* Contact Persons: One Institutional ThirdParty has Many Contact Persons
@ -130,7 +132,7 @@ class ThirdParty
* @Assert\Regex("/^([\+{1}])([0-9\s*]{4,20})$/",
* message="Invalid phone number: it should begin with the international prefix starting with ""+"", hold only digits and be smaller than 20 characters. Ex: +33123456789"
* )
* @Groups({"read"})
* @Groups({"read", "write"})
*/
private $telephone;
@ -138,7 +140,7 @@ class ThirdParty
* @var string|null
* @ORM\Column(name="email", type="string", length=255, nullable=true)
* @Assert\Email(checkMX=false)
* @Groups({"read"})
* @Groups({"read", "write"})
*/
private $email;
@ -147,7 +149,7 @@ class ThirdParty
* @ORM\ManyToOne(targetEntity="\Chill\MainBundle\Entity\Address",
* cascade={"persist", "remove"})
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
* @Groups({"read"})
* @Groups({"read", "write"})
*/
private $address;
@ -168,7 +170,6 @@ class ThirdParty
* @var Collection
* @ORM\ManyToMany(targetEntity="\Chill\MainBundle\Entity\Center")
* @ORM\JoinTable(name="chill_3party.party_center")
* @Assert\Count(min=1)
*/
private $centers;
@ -325,7 +326,7 @@ class ThirdParty
* @param array|null $type
* @return ThirdParty
*/
public function setType(array $type = null)
public function setTypes(array $type = null)
{
// remove all keys from the input data
$this->type = \array_values($type);
@ -338,9 +339,9 @@ class ThirdParty
*
* @return array|null
*/
public function getType()
public function getTypes()
{
return $this->type;
return $this->types;
}
/**

View File

@ -64,10 +64,10 @@ class ThirdPartyType extends AbstractType
}
if (count($types) === 1) {
$builder
->add('type', HiddenType::class, [
->add('types', HiddenType::class, [
'data' => array_values($types)
])
->get('type')
->get('types')
->addModelTransformer(new CallbackTransformer(
function (?array $typeArray): ?string {
if (null === $typeArray) {
@ -84,7 +84,7 @@ class ThirdPartyType extends AbstractType
))
;
} else {
$builder->add('type', ChoiceType::class, [
$builder->add('types', ChoiceType::class, [
'choices' => $types,
'expanded' => true,
'multiple' => true,

View File

@ -0,0 +1,23 @@
<?php
namespace Chill\ThirdPartyBundle\Repository;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
/**
* @Author Mathieu Jaumotte mathieu.jaumotte@champs-libres.coop
*/
class ThirdPartyACLAwareRepository implements ThirdPartyACLAwareRepositoryInterface
{
public function findByThirdparty(
ThirdParty $thirdparty,
string $role,
?array $orderBy = [],
int $limit = null,
int $offset = null
): array {
// TODO: Implement findByThirdparty() method.
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Chill\ThirdPartyBundle\Repository;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
interface ThirdPartyACLAwareRepositoryInterface
{
public function findByThirdparty(
ThirdParty $thirdparty,
string $role,
?array $orderBy = [],
int $limit = null,
int $offset = null
): array;
}

View File

@ -13,7 +13,7 @@ const getThirdparty = (id) => {
};
/*
* POST a new person
* POST a new thirdparty
*/
const postThirdparty = (body) => {
const url = `/api/1.0/thirdparty/thirdparty.json`;
@ -29,8 +29,27 @@ const postThirdparty = (body) => {
throw Error('Error with request resource response');
});
};
export {
getThirdparty,
postThirdparty
};
/*
* PATCH an existing thirdparty
*/
const patchThirdparty = (id, body) => {
const url = `/api/1.0/thirdparty/thirdparty/${id}.json`;
return fetch(url, {
method: 'PATCH',
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 {
getThirdparty,
postThirdparty,
patchThirdparty
};

View File

@ -8,25 +8,17 @@
<div :class="'denomination h' + options.hLevel">
<a v-if="this.options.addLink == true" href="#">
<a v-if="this.options.addLink === true" href="#">
<span class="name">{{ thirdparty.text }}</span>
</a>
<span class="name">{{ thirdparty.text }}</span>
<span v-if="options.addId == true" class="id-number" :title="'n° ' + thirdparty.id">{{ thirdparty.id }}</span>
<span v-if="options.addEntity == true && thirdparty.type == 'thirdparty'" class="badge rounded-pill bg-secondary">{{ $t('renderbox.type.thirdparty') }}</span>
<span v-if="options.addEntity == true && thirdparty.type === 'thirdparty'" class="badge rounded-pill bg-secondary">{{ $t('renderbox.type.thirdparty') }}</span>
</div>
<p v-if="this.options.addInfo == true" class="moreinfo">
<i v-if="thirdparty.birthdate" :class="'fa fa-fw ' + getGenderIcon" title="{{ getGender }}"></i>
<time v-if="thirdparty.birthdate" datetime="{{ thirdparty.birthdate.datetime }}" title="{{ birthdate }}">
{{ $t(getGender) + ' ' + $d(birthdate, 'short') }}
</time>
<time v-else-if="thirdparty.deathdate" datetime="{{ thirdparty.deathdate.datetime }}" title="{{ thirdparty.deathdate }}">
{{ birthdate }} - {{ deathdate }}
</time>
<span v-if="options.addAge == true" class="age">{{ thirdparty.age }}</span>
<p v-if="this.options.addInfo === true" class="moreinfo">
</p>
</div>
</div>
@ -42,9 +34,9 @@
<i class="fa fa-li fa-map-marker"></i>
<address-render-box :address="thirdparty.address" :isMultiline="isMultiline"></address-render-box>
</li>
<li v-if="thirdparty.telephone">
<li v-if="thirdparty.phonenumber">
<i class="fa fa-li fa-mobile"></i>
<a :href="'tel: ' + thirdparty.telephone">{{ thirdparty.telephone }}</a>
<a :href="'tel: ' + thirdparty.phonenumber">{{ thirdparty.phonenumber }}</a>
</li>
<li v-if="thirdparty.email">
<i class="fa fa-li fa-envelope-o"></i>
@ -78,21 +70,7 @@ export default {
} else {
return false
}
},
getGender: function() {
return this.thirdparty.gender == 'woman' ? 'renderbox.birthday.woman' : 'renderbox.birthday.man';
},
getGenderIcon: function() {
return this.thirdparty.gender == 'woman' ? 'fa-venus' : this.thirdparty.gender == 'man' ? 'fa-mars' : 'fa-neuter';
},
birthdate: function(){
var date = new Date(this.thirdparty.birthdate.datetime);
return dateToISO(date);
},
deathdate: function(){
var date = new Date(this.thirdparty.deathdate.datetime);
return dateToISO(date);
},
}
}
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<div v-if="action === 'show'">
<div v-if="action === 'show'">
<div class="flex-table">
<third-party-render-box
:thirdparty="thirdparty"
@ -19,17 +19,20 @@
</div>
</div>
<div v-else-if="action === 'edit' || action === 'create'">
<div class="form-floating mb-3">
<input class="form-control form-control-lg" id="firstname" v-model="thirdparty.firstName" v-bind:placeholder="$t('thirdparty.firstname')" />
<label for="firstname">{{ $t('thirdparty.firstname') }}</label>
</div>
<div class="form-floating mb-3">
<input class="form-control form-control-lg" id="lastname" v-model="thirdparty.lastName" v-bind:placeholder="$t('thirdparty.lastname')" />
<label for="lastname">{{ $t('thirdparty.lastname') }}</label>
<input class="form-control form-control-lg" id="name" v-model="thirdparty.text" v-bind:placeholder="$t('thirdparty.name')" />
<label for="name">{{ $t('thirdparty.name') }}</label>
</div>
<add-address
key="thirdparty"
:context="context"
:options="addAddress.options"
:address-changed-callback="submitAddress"
ref="addAddress">
</add-address>
<div class="input-group mb-3">
<span class="input-group-text" id="email"><i class="fa fa-fw fa-envelope"></i></span>
<input class="form-control form-control-lg"
@ -53,38 +56,83 @@
<script>
import ThirdPartyRenderBox from '../Entity/ThirdPartyRenderBox.vue';
import { getThirdparty, postThirdparty } from '../../_api/OnTheFly';
import AddAddress from 'ChillMainAssets/vuejs/Address/components/AddAddress';
import { getThirdparty } from '../../_api/OnTheFly';
export default {
name: "OnTheFlyThirdParty",
props: ['id', 'type', 'action'],
components: {
ThirdPartyRenderBox,
AddAddress
},
data: function() {
data() {
return {
//context: {}, <--
thirdparty: {
type: 'thirdparty'
},
addAddress: {
options: {
openPanesInModal: true,
onlyButton: false,
button: {
size: 'btn-sm'
},
title: {
create: 'add_an_address_title',
edit: 'edit_address'
}
}
}
}
},
computed: {
context() {
let context = {
target: {
name: this.type,
id: this.id
},
edit: false,
addressId: null
};
if ( typeof this.thirdparty.address !== 'undefined'
&& this.thirdparty.address.address_id !== null
) { // to complete
context.addressId = this.thirdparty.address.address_id;
context.edit = true;
}
console.log('context', context);
//this.context = context; <--
return context;
},
},
methods: {
loadThirdparty(){
loadData(){
getThirdparty(this.id).then(thirdparty => new Promise((resolve, reject) => {
this.thirdparty = thirdparty;
console.log('get thirdparty', thirdparty);
if (this.action !== 'show') {
// bof! we force getInitialAddress because addressId not available when mounted
this.$refs.addAddress.getInitialAddress(thirdparty.address.address_id);
}
resolve();
}));
},
postData() {
postThirdparty(this.thirdparty).then(thirdparty => new Promise((resolve, reject) => {
this.thirdparty = thirdparty;
resolve();
}))
submitAddress(payload) {
console.log('submitAddress', payload);
if (typeof payload.addressId !== 'undefined') { // <--
this.context.edit = true;
this.context.addressId = payload.addressId; // bof! use legacy and not legacy in payload
this.thirdparty.address = payload.address; // <--
console.log('switch address to edit mode', this.context);
}
}
},
mounted() {
if (this.action !== 'create'){
this.loadThirdparty();
if (this.action !== 'create') {
this.loadData();
}
},
}

View File

@ -0,0 +1,11 @@
const thirdpartyMessages = {
fr: {
thirdparty: {
name: "Dénomination",
email: "Courriel",
phonenumber: "Téléphone",
}
}
};
export { thirdpartyMessages };

View File

@ -51,7 +51,7 @@
<th>{{ (tp.active ? '<i class="fa fa-check chill-green">' : '<i class="fa fa-times chill-red">')|raw }}</th>
<td>{{ tp.name }}</td>
{% set types = [] %}
{% for t in tp.type %}
{% for t in tp.types %}
{% set types = types|merge( [ ('chill_3party.key_label.'~t)|trans ] ) %}
{% endfor %}
<td>{{ types|join(', ') }}</td>

View File

@ -26,7 +26,7 @@
{{ form_row(form.profession) }}
{% endif %}
{{ form_row(form.type) }}
{{ form_row(form.types) }}
{{ form_row(form.categories) }}
{{ form_row(form.telephone) }}

View File

@ -48,7 +48,7 @@
<dt>{{ 'Type'|trans }}</dt>
{% set types = [] %}
{% for t in thirdParty.type %}
{% for t in thirdParty.types %}
{% set types = types|merge( [ ('chill_3party.key_label.'~t)|trans ] ) %}
{% endfor %}
<dd>

View File

@ -43,7 +43,7 @@
{{ form_row(form.profession) }}
{% endif %}
{{ form_row(form.type) }}
{{ form_row(form.types) }}
{{ form_row(form.categories) }}
{{ form_row(form.telephone) }}

View File

@ -56,11 +56,15 @@ class ThirdPartyVoter extends AbstractChillVoter implements ProvideRoleHierarchy
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
return true;
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
return true;
$centers = $this->authorizationHelper
->getReachableCenters($user, new Role($attribute));

View File

@ -8,17 +8,57 @@ servers:
- url: "/api"
description: "Your current dev server"
components:
schemas:
Thirdparty:
type: object
properties:
id:
type: integer
readOnly: true
type:
type: string
enum:
- "thirdparty"
name:
type: string
email:
type: string
telephone:
type: string
address:
$ref: "#/components/schemas/Address"
Address:
type: object
properties:
id:
type: integer
paths:
/1.0/thirdparty/thirdparty.json:
get:
post:
tags:
- thirdparty
summary: Return a list of all thirdparty items
summary: Create a single thirdparty
requestBody:
description: "A thirdparty"
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Thirdparty"
responses:
200:
description: "ok"
401:
description: "OK"
content:
application/json:
schema:
$ref: "#/components/schemas/Thirdparty"
403:
description: "Unauthorized"
422:
description: "Invalid data"
/1.0/thirdparty/thirdparty/{id}.json:
get:
@ -41,3 +81,32 @@ paths:
description: "not found"
401:
description: "Unauthorized"
patch:
tags:
- thirdparty
summary: "Alter a thirdparty"
parameters:
- name: id
in: path
required: true
description: The thirdparty's id
schema:
type: integer
format: integer
minimum: 1
requestBody:
description: "A thirdparty"
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Thirdparty"
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "Object with validation errors"