Merge branch 'onTheFly' into thirdparty

This commit is contained in:
Mathieu Jaumotte 2021-09-30 15:13:03 +02:00
commit 14539f6f88
74 changed files with 1490 additions and 804 deletions

View File

@ -7,6 +7,7 @@ charset = utf-8
end_of_line = LF end_of_line = LF
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
indent_size = 4
[*.{php,html,twig}] [*.{php,html,twig}]
indent_style = space indent_style = space

18
CHANGELOG.md Normal file
View File

@ -0,0 +1,18 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
* [Semantic Versioning](https://semver.org/spec/v2.0.0.html) for stable releases;
* date versioning for test releases
## Unreleased
* [Household editor][UI] Update how household suggestion and addresses are picked;
* [AddAddress] Handle address suggestion;
## Test release yyyy-mm-dd

View File

@ -22,6 +22,7 @@
"league/csv": "^9.7.1", "league/csv": "^9.7.1",
"nyholm/psr7": "^1.4", "nyholm/psr7": "^1.4",
"phpoffice/phpspreadsheet": "^1.16", "phpoffice/phpspreadsheet": "^1.16",
"ramsey/uuid-doctrine": "^1.7",
"sensio/framework-extra-bundle": "^5.5", "sensio/framework-extra-bundle": "^5.5",
"symfony/asset": "4.*", "symfony/asset": "4.*",
"symfony/browser-kit": "^5.2", "symfony/browser-kit": "^5.2",
@ -29,6 +30,7 @@
"symfony/expression-language": "4.*", "symfony/expression-language": "4.*",
"symfony/form": "4.*", "symfony/form": "4.*",
"symfony/intl": "4.*", "symfony/intl": "4.*",
"symfony/mime": "^4 || ^5",
"symfony/monolog-bundle": "^3.5", "symfony/monolog-bundle": "^3.5",
"symfony/security-bundle": "4.*", "symfony/security-bundle": "4.*",
"symfony/serializer": "^5.2", "symfony/serializer": "^5.2",

View File

@ -29,17 +29,19 @@ class ListenToActivityCreate
$activityData = $request->query->get('activityData'); $activityData = $request->query->get('activityData');
if (array_key_exists('calendarId', $activityData)) { if (array_key_exists('calendarId', $activityData)) {
$calendarId = $activityData['calendarId']; $calendarId = $activityData['calendarId'];
// Attach the activity to the calendar
$em = $event->getObjectManager();
$calendar = $em->getRepository(\Chill\CalendarBundle\Entity\Calendar::class)->find($calendarId);
$calendar->setActivity($activity);
$em->persist($calendar);
$em->flush();
} }
} }
// Attach the activity to the calendar
$em = $event->getObjectManager();
$calendar = $em->getRepository(\Chill\CalendarBundle\Entity\Calendar::class)->find($calendarId);
$calendar->setActivity($activity);
$em->persist($calendar);
$em->flush();
} }
} }

View File

@ -41,7 +41,7 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
'routeParameters' => [ 'routeParameters' => [
'accompanying_period_id' => $period->getId(), 'accompanying_period_id' => $period->getId(),
]]) ]])
->setExtras(['order' => 41]); ->setExtras(['order' => 35]);
} }
} }
} }

View File

@ -138,7 +138,7 @@ class DocGeneratorTemplateController extends AbstractController
$em->flush(); $em->flush();
return $this->redirectToRoute('chill_wopi_file_edit', [ return $this->redirectToRoute('chill_wopi_file_edit', [
'fileId' => $genDocName, 'fileId' => $storedObject->getUuid(),
'returnPath' => $request->query->get('returnPath', "/") 'returnPath' => $request->query->get('returnPath', "/")
]); ]);
} }

View File

@ -7,7 +7,10 @@ namespace Chill\DocStoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface; use ChampsLibres\AsyncUploaderBundle\Model\AsyncFileInterface;
use ChampsLibres\AsyncUploaderBundle\Validator\Constraints\AsyncFileExists; use ChampsLibres\AsyncUploaderBundle\Validator\Constraints\AsyncFileExists;
use ChampsLibres\WopiLib\Contract\Entity\Document;
use DateTimeInterface; use DateTimeInterface;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation as Serializer;
/** /**
@ -19,7 +22,7 @@ use Symfony\Component\Serializer\Annotation as Serializer;
* message="The file is not stored properly" * message="The file is not stored properly"
* ) * )
*/ */
class StoredObject implements AsyncFileInterface class StoredObject implements AsyncFileInterface, Document
{ {
/** /**
* @ORM\Id() * @ORM\Id()
@ -47,6 +50,12 @@ class StoredObject implements AsyncFileInterface
*/ */
private array $iv = []; private array $iv = [];
/**
* @ORM\Column(type="uuid", unique=true)
* @Serializer\Groups({"read"})
*/
private UuidInterface $uuid;
/** /**
* @ORM\Column(type="datetime", name="creation_date") * @ORM\Column(type="datetime", name="creation_date")
* @Serializer\Groups({"read"}) * @Serializer\Groups({"read"})
@ -68,6 +77,7 @@ class StoredObject implements AsyncFileInterface
public function __construct() public function __construct()
{ {
$this->creationDate = new \DateTime(); $this->creationDate = new \DateTime();
$this->uuid = Uuid::uuid4();
} }
public function getId() public function getId()
@ -155,5 +165,13 @@ class StoredObject implements AsyncFileInterface
return $this; return $this;
} }
public function getUuid(): UuidInterface
{
return $this->uuid;
}
public function getWopiDocId(): string
{
return (string) $this->uuid;
}
} }

View File

@ -3,9 +3,12 @@
"description": "A Chill bundle to store documents", "description": "A Chill bundle to store documents",
"type": "symfony-bundle", "type": "symfony-bundle",
"autoload": { "autoload": {
"psr-4": { "Chill\\DocStoreBundle\\" : "" } "psr-4": {
"Chill\\DocStoreBundle\\": ""
}
}, },
"require": { "require": {
"symfony/mime": "^4 || ^5"
}, },
"license": "AGPL-3.0" "license": "AGPL-3.0"
} }

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\DocStore;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20210928182542 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create UUID column on StoredObject table.';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
$this->addSql('ALTER TABLE chill_doc.stored_object ADD uuid UUID DEFAULT NULL');
$this->addSql('UPDATE chill_doc.stored_object SET uuid=uuid_generate_v4()');
$this->addSql('ALTER TABLE chill_doc.stored_object ALTER uuid SET NOT NULL');
$this->addSql('CREATE UNIQUE INDEX UNIQ_49604E36D17F50A6 ON chill_doc.stored_object (uuid)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_doc.stored_object DROP uuid');
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Entity\Address;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
class AddressApiController extends ApiController
{
/**
* Duplicate an existing address
*
* @Route("/api/1.0/main/address/{id}/duplicate.json", name="chill_api_main_address_duplicate",
* methods={"POST"})
*
* @param Address $address
*/
public function duplicate(Address $address): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_USER');
$new = Address::createFromAddress($address);
$em = $this->getDoctrine()->getManager();
$em->persist($new);
$em->flush();
return $this->json($new, Response::HTTP_OK, [], [
AbstractNormalizer::GROUPS => ['read']
]);
}
}

View File

@ -19,6 +19,7 @@
namespace Chill\MainBundle\DependencyInjection; namespace Chill\MainBundle\DependencyInjection;
use Chill\MainBundle\Controller\AddressApiController;
use Chill\MainBundle\Controller\UserController; use Chill\MainBundle\Controller\UserController;
use Chill\MainBundle\Doctrine\DQL\STContains; use Chill\MainBundle\Doctrine\DQL\STContains;
use Chill\MainBundle\Doctrine\DQL\StrictWordSimilarityOPS; use Chill\MainBundle\Doctrine\DQL\StrictWordSimilarityOPS;
@ -319,6 +320,7 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
'apis' => [ 'apis' => [
[ [
'class' => \Chill\MainBundle\Entity\Address::class, 'class' => \Chill\MainBundle\Entity\Address::class,
'controller' => AddressApiController::class,
'name' => 'address', 'name' => 'address',
'base_path' => '/api/1.0/main/address', 'base_path' => '/api/1.0/main/address',
'base_role' => 'ROLE_USER', 'base_role' => 'ROLE_USER',

View File

@ -376,10 +376,22 @@ class Address
public static function createFromAddress(Address $original) : Address public static function createFromAddress(Address $original) : Address
{ {
return (new Address()) return (new Address())
->setBuildingName($original->getBuildingName())
->setCorridor($original->getCorridor())
->setCustoms($original->getCustoms())
->setDistribution($original->getDistribution())
->setExtra($original->getExtra())
->setFlat($original->getFlat())
->setFloor($original->getFloor())
->setIsNoAddress($original->getIsNoAddress())
->setLinkedToThirdParty($original->getLinkedToThirdParty())
->setPoint($original->getPoint())
->setPostcode($original->getPostcode()) ->setPostcode($original->getPostcode())
->setStreetAddress1($original->getStreetAddress1()) ->setSteps($original->getSteps())
->setStreetAddress2($original->getStreetAddress2()) ->setStreet($original->getStreet())
->setStreetNumber($original->getStreetNumber())
->setValidFrom($original->getValidFrom()) ->setValidFrom($original->getValidFrom())
->setValidTo($original->getValidTo())
; ;
} }
@ -506,7 +518,7 @@ class Address
return $this->validTo; return $this->validTo;
} }
public function setValidTo(\DateTimeInterface $validTo): self public function setValidTo(?\DateTimeInterface $validTo = null): self
{ {
$this->validTo = $validTo; $this->validTo = $validTo;

View File

@ -7,6 +7,7 @@ namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\Address;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
final class AddressRepository implements ObjectRepository final class AddressRepository implements ObjectRepository
@ -47,4 +48,9 @@ final class AddressRepository implements ObjectRepository
public function getClassName() { public function getClassName() {
return Address::class; return Address::class;
} }
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
{
return $this->repository->createQueryBuilder($alias, $indexBy);
}
} }

View File

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

View File

@ -1,4 +1,4 @@
/* /**
* Endpoint chill_api_single_country__index * Endpoint chill_api_single_country__index
* method GET, get Country Object * method GET, get Country Object
* @returns {Promise} a promise containing all Country object * @returns {Promise} a promise containing all Country object
@ -14,7 +14,7 @@ const fetchCountries = () => {
}); });
}; };
/* /**
* Endpoint chill_api_single_postal_code__index * Endpoint chill_api_single_postal_code__index
* method GET, get Country Object * method GET, get Country Object
* @returns {Promise} a promise containing all Postal Code objects filtered with country * @returns {Promise} a promise containing all Postal Code objects filtered with country
@ -29,7 +29,7 @@ const fetchCities = (country) => {
}); });
}; };
/* /**
* Endpoint chill_api_single_address_reference__index * Endpoint chill_api_single_address_reference__index
* method GET, get AddressReference Object * method GET, get AddressReference Object
* @returns {Promise} a promise containing all AddressReference objects filtered with postal code * @returns {Promise} a promise containing all AddressReference objects filtered with postal code
@ -44,7 +44,7 @@ const fetchReferenceAddresses = (postalCode) => {
}); });
}; };
/* /**
* Endpoint chill_api_single_address_reference__index * Endpoint chill_api_single_address_reference__index
* method GET, get AddressReference Object * method GET, get AddressReference Object
* @returns {Promise} a promise containing all AddressReference objects filtered with postal code * @returns {Promise} a promise containing all AddressReference objects filtered with postal code
@ -60,7 +60,7 @@ const fetchAddresses = () => {
}); });
}; };
/* /**
* Endpoint chill_api_single_address__entity__create * Endpoint chill_api_single_address__entity__create
* method POST, post Address Object * method POST, post Address Object
* @returns {Promise} * @returns {Promise}
@ -81,8 +81,28 @@ const postAddress = (address) => {
}); });
}; };
/**
*
* @param address
* @returns {Promise<Response>}
*/
const duplicateAddress = (address) => {
const url = `/api/1.0/main/address/${address.address_id}/duplicate.json`;
return fetch(url, {
'method': 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
}).then(response => {
if (response.ok) {
return response.json();
}
throw Error('Error with request resource response');
});
};
/*
/**
* Endpoint chill_api_single_address__entity__create * Endpoint chill_api_single_address__entity__create
* method PATCH, patch Address Instance * method PATCH, patch Address Instance
* *
@ -142,6 +162,7 @@ const getAddress = (id) => {
}; };
export { export {
duplicateAddress,
fetchCountries, fetchCountries,
fetchCities, fetchCities,
fetchReferenceAddresses, fetchReferenceAddresses,

View File

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

View File

@ -34,6 +34,7 @@
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"
@pick-address="this.pickAddress"
ref="suggestAddress"> ref="suggestAddress">
</suggest-pane> </suggest-pane>
</template> </template>
@ -55,6 +56,7 @@
v-bind:entity="this.entity" v-bind:entity="this.entity"
v-bind:flag="this.flag" v-bind:flag="this.flag"
v-bind:insideModal="false" v-bind:insideModal="false"
@pick-address="this.pickAddress"
ref="suggestAddress"> ref="suggestAddress">
<template v-slot:before v-if="!bypassFirstStep"> <template v-slot:before v-if="!bypassFirstStep">
@ -217,7 +219,16 @@
<script> <script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal'; import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import { getAddress, fetchCountries, fetchCities, fetchReferenceAddresses, patchAddress, postAddress, postPostalCode } from '../api'; import {
duplicateAddress,
fetchCountries,
fetchCities,
fetchReferenceAddresses,
getAddress,
patchAddress,
postAddress,
postPostalCode,
} from '../api';
import { postAddressToPerson, postAddressToHousehold } from "ChillPersonAssets/vuejs/_api/AddAddress.js"; import { postAddressToPerson, postAddressToHousehold } from "ChillPersonAssets/vuejs/_api/AddAddress.js";
import ShowPane from './ShowPane.vue'; import ShowPane from './ShowPane.vue';
import SuggestPane from './SuggestPane.vue'; import SuggestPane from './SuggestPane.vue';
@ -234,6 +245,9 @@ export default {
EditPane, EditPane,
DatePane DatePane
}, },
emits: {
pickAddress: null
},
data() { data() {
return { return {
flag: { flag: {
@ -257,7 +271,7 @@ export default {
validFrom: false, validFrom: false,
validTo: false validTo: false
}, },
hideAddress: false onlyButton: false
}, },
entity: { entity: {
address: {}, // <== loaded and returned address: {}, // <== loaded and returned
@ -311,8 +325,10 @@ export default {
return (this.validFrom || this.validTo) ? true : false; return (this.validFrom || this.validTo) ? true : false;
}, },
hasSuggestions() { hasSuggestions() {
// TODO console.log(this.context.suggestions);
//return addressSuggestions.length > 0 if (typeof(this.context.suggestions) !== 'undefined') {
return this.context.suggestions.length > 0;
}
return false; return false;
}, },
displaySuggestions() { displaySuggestions() {
@ -647,9 +663,12 @@ export default {
this.flag.loading = false; this.flag.loading = false;
this.flag.success = true; this.flag.success = true;
resolve({ resolve({
address,
targetOrigin: this.context.target,
// for "legacy" use:
target: this.context.target.name, target: this.context.target.name,
targetId: this.context.target.id, targetId: this.context.target.id,
addressId: this.entity.address.address_id addressId: this.entity.address.address_id,
} }
); );
})) }))
@ -695,6 +714,9 @@ export default {
this.flag.loading = false; this.flag.loading = false;
this.flag.success = true; this.flag.success = true;
return resolve({ return resolve({
address,
targetOrigin: this.context.target,
// for "legacy" use:
target: this.context.target.name, target: this.context.target.name,
targetId: this.context.target.id, targetId: this.context.target.id,
addressId: this.entity.address.address_id addressId: this.entity.address.address_id
@ -707,18 +729,28 @@ export default {
}); });
}, },
/* /**
* Method just add closing pane to the callback method *
* (get out step1 show pane, submit button) * @param address the address selected
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);
}
*/ */
pickAddress(address) {
console.log('pickAddress', address);
duplicateAddress(address).then(newAddress => {
this.entity.address = newAddress;
this.flag.loading = false;
this.flag.success = true;
let payload = {
address: newAddress,
targetOrigin: this.context.target,
// for "legacy" use:
target: this.context.target.name,
targetId: this.context.target.id,
addressId: this.entity.address.address_id
};
this.addressChangedCallback(payload);
this.closeSuggestPane();
});
}
} }
} }
</script> </script>

View File

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

View File

@ -10,13 +10,14 @@
<h4 class="h3">{{ $t('address_suggestions') }}</h4> <h4 class="h3">{{ $t('address_suggestions') }}</h4>
<div class="flex-table AddressSuggestionList"> <div class="flex-table AddressSuggestionList">
<div class="item-bloc"> <div v-for="a in context.suggestions" class="item-bloc">
<div class="float-button bottom"> <div class="float-button bottom">
<div class="box"> <div class="box">
<div class="action"> <div class="action">
<!-- QUESTION normal que ça vienne avant l'adresse ? pourquoi pas après avoir affiché le address-render-box ? -->
<ul class="record_actions"> <ul class="record_actions">
<li> <li>
<button class="btn btn-sm btn-choose"> <button class="btn btn-sm btn-choose" @click="this.pickAddress(a)">
{{ $t('use_this_address') }} {{ $t('use_this_address') }}
</button> </button>
</li> </li>
@ -25,9 +26,7 @@
<ul class="list-content fa-ul"> <ul class="list-content fa-ul">
<li> <li>
<i class="fa fa-li fa-map-marker"></i> <i class="fa fa-li fa-map-marker"></i>
<!-- <address-render-box :address="a"></address-render-box>
<address-render-box></address-render-box>
-->
</li> </li>
</ul> </ul>
</div> </div>
@ -68,9 +67,14 @@ export default {
'flag', 'flag',
'entity', 'entity',
'errorMsg', 'errorMsg',
'insideModal' 'insideModal',
], ],
computed: {}, computed: {},
methods: {} methods: {
pickAddress(address) {
console.log('pickAddress in suggest pane', address);
this.$emit('pickAddress', address);
},
}
} }
</script> </script>

View File

@ -53,7 +53,7 @@ containers.forEach((container) => {
}, },
/// Don't display show renderbox Address: showPane display only a button /// 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

@ -42,16 +42,16 @@
</template> </template>
<template v-slot:footer> <template v-slot:footer>
<button v-if="action === 'show'" <a v-if="action === 'show'"
@click="goToLocation(id, type)" :href="buildLocation(id, type)"
:title="$t(titleMessage)" :title="$t(titleMessage)"
class="btn btn-show">{{ $t(buttonMessage) }} class="btn btn-show">{{ $t(buttonMessage) }}
</button> </a>
<button v-else <a v-else
class="btn btn-save" class="btn btn-save"
@click="saveAction"> @click="saveAction">
{{ $t('action.save')}} {{ $t('action.save')}}
</button> </a>
</template> </template>
</modal> </modal>
@ -134,6 +134,8 @@ export default {
}, },
methods: { methods: {
openModal() { openModal() {
console.log('## OPEN ON THE FLY MODAL');
console.log('## type:', this.type, ', action:', this.action);
this.modal.showModal = true; this.modal.showModal = true;
this.$nextTick(function() { this.$nextTick(function() {
//this.$refs.search.focus(); //this.$refs.search.focus();
@ -144,7 +146,6 @@ export default {
}, },
saveAction() { saveAction() {
console.log('saveAction button: create/edit action with', this.type); console.log('saveAction button: create/edit action with', this.type);
let let
type = this.type, type = this.type,
data = {} ; data = {} ;
@ -173,11 +174,12 @@ export default {
this.modal.showModal = false; this.modal.showModal = false;
}, },
goToLocation(id, type){ buildLocation(id, type) {
if(type == 'person'){ if (type == 'person') {
window.location = `../../person/${id}/general` // TODO i18n
} else if(type == 'thirdparty') { return `/fr/person/${id}/general`;
window.location = `../../thirdparty/thirdparty/${id}/show` } else if (type == 'thirdparty') {
return `/fr/thirdparty/thirdparty/${id}/show`;
} }
} }
} }

View File

@ -3,7 +3,7 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" :class="{ active: isActive('person') }"> <a class="nav-link" :class="{ active: isActive('person') }">
<label for="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') }} {{ $t('onthefly.create.person') }}
</label> </label>
</a> </a>
@ -11,7 +11,7 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" :class="{ active: isActive('thirdparty') }"> <a class="nav-link" :class="{ active: isActive('thirdparty') }">
<label for="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') }} {{ $t('onthefly.create.thirdparty') }}
</label> </label>
</a> </a>
@ -56,6 +56,7 @@ export default {
radioType: { radioType: {
set(type) { set(type) {
this.type = type; this.type = type;
console.log('## type:', type, ', action:', this.action);
}, },
get() { get() {
return this.type; return this.type;

View File

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

View File

@ -10,7 +10,7 @@
{% endblock %} {% endblock %}
{% block table_entities_tbody %} {% block table_entities_tbody %}
{% for entity in entities %} {% for entity in entities %}
<tr> <tr data-username="{{ entity.username|e('html_attr') }}">
<td> <td>
{% if entity.isEnabled %} {% if entity.isEnabled %}
<i class="fa fa-check chill-green"></i> <i class="fa fa-check chill-green"></i>

View File

@ -17,12 +17,11 @@
*/ */
namespace Chill\MainBundle\Test; namespace Chill\MainBundle\Test;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/** /**
* Prepare a client authenticated with a user * Prepare a client authenticated with a user
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/ */
trait PrepareClientTrait trait PrepareClientTrait
{ {
@ -37,7 +36,7 @@ trait PrepareClientTrait
public function getClientAuthenticated( public function getClientAuthenticated(
$username = 'center a_social', $username = 'center a_social',
$password = 'password' $password = 'password'
) { ): KernelBrowser {
if (!$this instanceof WebTestCase) { if (!$this instanceof WebTestCase) {
throw new \LogicException(sprintf("The current class does not " throw new \LogicException(sprintf("The current class does not "
. "implements %s", WebTestCase::class)); . "implements %s", WebTestCase::class));

View File

@ -0,0 +1,49 @@
<?php
namespace Controller;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Repository\AddressRepository;
use Chill\MainBundle\Test\PrepareClientTrait;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
class AddressControllerTest extends \Symfony\Bundle\FrameworkBundle\Test\WebTestCase
{
private KernelBrowser $client;
use PrepareClientTrait;
public function setUp()
{
self::bootKernel();
$this->client = $this->getClientAuthenticated();
}
/**
* @dataProvider generateAddressIds
* @param int $addressId
*/
public function testDuplicate(int $addressId)
{
$this->client->request('POST', "/api/1.0/main/address/$addressId/duplicate.json");
$this->assertResponseIsSuccessful('test that duplicate is successful');
}
public function generateAddressIds()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$qb = $em->createQueryBuilder();
$addresses = $qb->select('a')->from(Address::class, 'a')
->setMaxResults(2)
->getQuery()
->getResult();
foreach ($addresses as $a) {
yield [ $a->getId() ];
}
}
}

View File

@ -2,12 +2,17 @@
namespace Chill\MainBundle\Tests\Controller; namespace Chill\MainBundle\Tests\Controller;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserControllerTest extends WebTestCase class UserControllerTest extends WebTestCase
{ {
private $client; private $client;
private array $toDelete = [];
public function setUp() public function setUp()
{ {
self::bootKernel(); self::bootKernel();
@ -22,18 +27,14 @@ class UserControllerTest extends WebTestCase
public function testList() public function testList()
{ {
// get the list // get the list
$crawler = $this->client->request('GET', '/fr/admin/user/'); $crawler = $this->client->request('GET', '/fr/admin/main/user');
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(), $this->assertEquals(200, $this->client->getResponse()->getStatusCode(),
"Unexpected HTTP status code for GET /admin/user/"); "Unexpected HTTP status code for GET /admin/main/user");
$link = $crawler->selectLink('Ajouter un nouvel utilisateur')->link();
$this->assertInstanceOf('Symfony\Component\DomCrawler\Link', $link);
$this->assertRegExp('|/fr/admin/user/new$|', $link->getUri());
} }
public function testNew() public function testNew()
{ {
$crawler = $this->client->request('GET', '/fr/admin/user/new'); $crawler = $this->client->request('GET', '/fr/admin/main/user/new');
$username = 'Test_user'. uniqid(); $username = 'Test_user'. uniqid();
$password = 'Password1234!'; $password = 'Password1234!';
@ -54,22 +55,15 @@ class UserControllerTest extends WebTestCase
$this->assertGreaterThan(0, $crawler->filter('td:contains("Test_user")')->count(), $this->assertGreaterThan(0, $crawler->filter('td:contains("Test_user")')->count(),
'Missing element td:contains("Test user")'); 'Missing element td:contains("Test user")');
$update = $crawler->selectLink('Modifier')->link();
$this->assertInstanceOf('Symfony\Component\DomCrawler\Link', $update);
$this->assertRegExp('|/fr/admin/user/[0-9]{1,}/edit$|', $update->getUri());
//test the auth of the new client //test the auth of the new client
$this->isPasswordValid($username, $password); $this->isPasswordValid($username, $password);
return $update;
} }
protected function isPasswordValid($username, $password) protected function isPasswordValid($username, $password)
{ {
/* @var $passwordEncoder \Symfony\Component\Security\Core\Encoder\UserPasswordEncoder */ /* @var $passwordEncoder \Symfony\Component\Security\Core\Encoder\UserPasswordEncoder */
$passwordEncoder = self::$kernel->getContainer() $passwordEncoder = self::$container
->get('security.password_encoder'); ->get(UserPasswordEncoderInterface::class);
$user = self::$kernel->getContainer() $user = self::$kernel->getContainer()
->get('doctrine.orm.entity_manager') ->get('doctrine.orm.entity_manager')
@ -81,46 +75,33 @@ class UserControllerTest extends WebTestCase
/** /**
* *
* @param \Symfony\Component\DomCrawler\Link $update * @dataProvider dataGenerateUserId
* @depends testNew
*/ */
public function testUpdate(\Symfony\Component\DomCrawler\Link $update) public function testUpdate(int $userId, string $username)
{ {
$crawler = $this->client->click($update); $crawler = $this->client->request('GET', "/fr/admin/main/user/$userId/edit");
$username = 'Foo bar '.uniqid(); $username = 'Foo bar '.uniqid();
$form = $crawler->selectButton('Mettre à jour')->form(array( $form = $crawler->selectButton('Enregistrer & fermer')->form(array(
'chill_mainbundle_user[username]' => $username, 'chill_mainbundle_user[username]' => $username,
)); ));
$this->client->submit($form); $this->client->submit($form);
$crawler = $this->client->followRedirect(); $crawler = $this->client->followRedirect();
// Check the element contains an attribute with value equals "Foo" // Check the element contains an attribute with value equals "Foo"
$this->assertGreaterThan(0, $crawler->filter('[value="'.$username.'"]')->count(), $this->assertGreaterThan(0, $crawler->filter('[data-username="'.$username.'"]')->count(),
'Missing element [value="Foo bar"]'); 'Missing element [data-username="Foo bar"]');
$updatePassword = $crawler->selectLink('Modifier le mot de passe')->link();
$this->assertInstanceOf('Symfony\Component\DomCrawler\Link', $updatePassword);
$this->assertRegExp('|/fr/admin/user/[0-9]{1,}/edit_password$|',
$updatePassword->getUri());
return array('link' => $updatePassword, 'username' => $username);
} }
/** /**
* *
* @param \Symfony\Component\DomCrawler\Link $updatePassword * @dataProvider dataGenerateUserId
* @depends testUpdate
*/ */
public function testUpdatePassword(array $params) public function testUpdatePassword(int $userId, $username)
{ {
$link = $params['link']; $crawler = $this->client->request('GET', "/fr/admin/user/$userId/edit_password");
$username = $params['username'];
$newPassword = '1234Password!'; $newPassword = '1234Password!';
$crawler = $this->client->click($link);
$form = $crawler->selectButton('Changer le mot de passe')->form(array( $form = $crawler->selectButton('Changer le mot de passe')->form(array(
'chill_mainbundle_user_password[new_password][first]' => $newPassword, 'chill_mainbundle_user_password[new_password][first]' => $newPassword,
'chill_mainbundle_user_password[new_password][second]' => $newPassword, 'chill_mainbundle_user_password[new_password][second]' => $newPassword,
@ -130,10 +111,38 @@ class UserControllerTest extends WebTestCase
$this->assertTrue($this->client->getResponse()->isRedirect(), $this->assertTrue($this->client->getResponse()->isRedirect(),
"the response is a redirection"); "the response is a redirection");
$this->client->followRedirect();
$this->isPasswordValid($username, $newPassword); $this->isPasswordValid($username, $newPassword);
} }
protected function tearDown()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
foreach ($this->toDelete as list($class, $id)) {
$obj = $em->getRepository($class)->find($id);
$em->remove($obj);
}
$em->flush();
}
public function dataGenerateUserId()
{
self::bootKernel();
$em = self::$container->get(EntityManagerInterface::class);
$user = new User();
$user->setUsername('Test_user '.uniqid());
$user->setPassword(self::$container->get(UserPasswordEncoderInterface::class)->encodePassword($user,
'password'));
$em->persist($user);
$em->flush();
$this->toDelete[] = [User::class, $user->getId()];
yield [ $user->getId(), $user->getUsername() ];
}
} }

View File

@ -293,6 +293,32 @@ paths:
401: 401:
description: "Unauthorized" description: "Unauthorized"
/1.0/main/address/{id}/duplicate.json:
post:
tags:
- address
summary: Duplicate an existing address
parameters:
- name: id
in: path
required: true
description: The address id that will be duplicated
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/Address'
404:
description: "not found"
401:
description: "Unauthorized"
/1.0/main/address-reference.json: /1.0/main/address-reference.json:
get: get:
tags: tags:

View File

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

View File

@ -247,7 +247,6 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
if (\random_int(0, 10) > 3) { if (\random_int(0, 10) > 3) {
// always add social scope: // always add social scope:
$accompanyingPeriod->addScope($this->getReference('scope_social')); $accompanyingPeriod->addScope($this->getReference('scope_social'));
var_dump(count($accompanyingPeriod->getScopes()));
$accompanyingPeriod->setAddressLocation($this->createAddress()); $accompanyingPeriod->setAddressLocation($this->createAddress());
$manager->persist($accompanyingPeriod->getAddressLocation()); $manager->persist($accompanyingPeriod->getAddressLocation());

View File

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

View File

@ -229,7 +229,7 @@ class Household
)) ))
->andWhere($expr->orX( ->andWhere($expr->orX(
$expr->isNull('endDate'), $expr->isNull('endDate'),
$expr->gte('endDate', $date) $expr->gt('endDate', $date)
)); ));
return $criteria; return $criteria;
@ -306,7 +306,7 @@ class Household
) )
->orWhere( ->orWhere(
$expr->andX( $expr->andX(
$expr->lt('endDate', $date), $expr->lte('endDate', $date),
$expr->neq('endDate', null) $expr->neq('endDate', null)
) )
); );

View File

@ -199,7 +199,7 @@ final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterfac
} }
if (NULL !== $birthdate) { if (NULL !== $birthdate) {
$qb->andWhere($qb->expr()->eq('s.birthdate', ':birthdate')) $qb->andWhere($qb->expr()->eq('p.birthdate', ':birthdate'))
->setParameter('birthdate', $birthdate); ->setParameter('birthdate', $birthdate);
} }

View File

@ -87,8 +87,8 @@ export default {
} }
padding: 0em 0em; padding: 0em 0em;
margin: 1em 0; margin: 1em 0;
border: 1px dotted tint-color($chill-accourse-context, 10%);
border-radius: 5px; border-radius: 5px;
border: 1px dotted tint-color($chill-accourse-context, 10%);
border-left: 1px dotted tint-color($chill-accourse-context, 10%); border-left: 1px dotted tint-color($chill-accourse-context, 10%);
border-right: 1px dotted tint-color($chill-accourse-context, 10%); border-right: 1px dotted tint-color($chill-accourse-context, 10%);
dd { dd {
@ -96,10 +96,15 @@ export default {
} }
& > div { & > div {
margin: 1em 3em 0; margin: 1em 3em 0;
&.flex-table, &.flex-table,
&.flex-bloc { &.flex-bloc {
margin: 1em 0 0; margin: 1em 0 0;
} }
&.alert.to-confirm {
margin: 1em 0 0;
padding: 1em 3em;
}
} }
div.flex-table { div.flex-table {

View File

@ -91,7 +91,7 @@ export default {
}, },
scopes: { scopes: {
msg: 'confirm.set_a_scope', msg: 'confirm.set_a_scope',
anchor: '#section-65' anchor: '#section-60'
} }
} }
} }

View File

@ -27,19 +27,34 @@
</div> </div>
<div v-if="isTemporaryAddress" class="alert alert-warning separator"> <div v-if="isTemporaryAddress" class="alert alert-warning separator">
<p>{{ $t('courselocation.temporary_address_must_be_changed') }}</p> <p>
{{ $t('courselocation.temporary_address_must_be_changed') }}
<i class="fa fa-fw fa-map-marker"></i>
</p>
</div> </div>
</div> </div>
</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> <div>
<ul class="record_actions"> <ul class="record_actions">
<li> <li>
<add-address <add-address
v-if="!isPersonLocation" v-if="!isPersonLocation"
:key="key"
:context="context" :context="context"
:key="addAddress.type" :options="options"
:options="addAddress.options"
:addressChangedCallback="submitTemporaryAddress" :addressChangedCallback="submitTemporaryAddress"
ref="addAddress"> ref="addAddress">
</add-address> </add-address>
@ -55,11 +70,15 @@
</ul> </ul>
</div> </div>
<div v-if="!isLocationValid" class="alert alert-warning to-confirm">
{{ $t('courselocation.not_valid') }}
</div>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from "vuex"; import {mapGetters, mapState} from "vuex";
import AddAddress from 'ChillMainAssets/vuejs/Address/components/AddAddress.vue'; import AddAddress from 'ChillMainAssets/vuejs/Address/components/AddAddress.vue';
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue'; import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
@ -72,10 +91,7 @@ export default {
data() { data() {
return { return {
addAddress: { addAddress: {
type: 'accompanying_course_location',
options: { options: {
/// Options override default.
/// null value take default component value
button: { button: {
text: { text: {
create: 'courselocation.add_temporary_address', create: 'courselocation.add_temporary_address',
@ -86,13 +102,7 @@ export default {
create: 'courselocation.add_temporary_address', create: 'courselocation.add_temporary_address',
edit: 'courselocation.edit_temporary_address' edit: 'courselocation.edit_temporary_address'
}, },
/// Display each step in page or Modal onlyButton: true
openPanesInModal: true,
// Use Date fields
//useDate: {
// validFrom: true
//},
hideAddress: true
} }
} }
} }
@ -102,6 +112,16 @@ export default {
accompanyingCourse: state => state.accompanyingCourse, accompanyingCourse: state => state.accompanyingCourse,
context: state => state.addressContext context: state => state.addressContext
}), }),
...mapGetters([
'isLocationValid'
]),
options() {
return this.addAddress.options;
},
key() {
return (this.context.edit) ? 'address_' + this.context.addressId
: this.accompanyingCourse.type + '_' + this.accompanyingCourse.id ;
},
isTemporaryAddress() { isTemporaryAddress() {
return this.accompanyingCourse.locationStatus === 'address'; return this.accompanyingCourse.locationStatus === 'address';
}, },
@ -111,11 +131,40 @@ export default {
hasNoLocation() { hasNoLocation() {
return this.accompanyingCourse.locationStatus === 'none'; return this.accompanyingCourse.locationStatus === 'none';
}, },
currentParticipations() {
return this.accompanyingCourse.participations.filter(p => p.enddate !== null);
},
hasNoPersonLocation() {
let addressInParticipations_ = []
this.currentParticipations.forEach(p => {
addressInParticipations_.push(this.checkHouseholdAddressForParticipation(p));
});
const booleanReducer = (previousValue, currentValue) => previousValue || currentValue;
let addressInParticipations = (addressInParticipations_.length > 0) ?
addressInParticipations_.reduce(booleanReducer) : false;
//console.log(addressInParticipations_, addressInParticipations);
return (
this.accompanyingCourse.step !== 'DRAFT'
&& this.isTemporaryAddress
&& !addressInParticipations
)
;
},
isContextEdit() { isContextEdit() {
return this.context.edit; return this.context.edit;
} }
}, },
methods: { methods: {
checkHouseholdAddressForParticipation(participation) {
if (participation.person.current_household_id === null) {
return false;
}
return participation.person.current_household_address !== null;
},
initAddressContext() { initAddressContext() {
let context = { let context = {
target: { target: {
@ -153,20 +202,44 @@ export default {
created() { created() {
this.initAddressContext(); this.initAddressContext();
console.log('ac.locationStatus', this.accompanyingCourse.locationStatus); //console.log('ac.locationStatus', this.accompanyingCourse.locationStatus);
console.log('ac.location (temporary location)', this.accompanyingCourse.location); //console.log('ac.location (temporary location)', this.accompanyingCourse.location);
console.log('ac.personLocation', this.accompanyingCourse.personLocation); //console.log('ac.personLocation', this.accompanyingCourse.personLocation);
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
div.flex-table { div#accompanying-course {
div.item-bloc { div.vue-component {
div.alert { & > div.alert.no-person-location {
margin: 0 -0.9em -1em; margin: 1px 0 0;
}
div.no-person-location {
padding-top: 1.5em;
display: flex;
flex-direction: row;
& > i {
flex-basis: 1.5em; flex-grow: 0; flex-shrink: 0;
padding-top: 0.2em;
opacity: 0.75;
}
& > div {
flex-basis: auto;
div.action {
button.btn-update {
margin-right: 2em;
}
}
}
}
div.flex-table {
div.item-bloc {
div.alert {
margin: 0 -0.9em -1em;
}
}
} }
} }
} }
</style> </style>

View File

@ -26,7 +26,7 @@
:id="p.person.id" :id="p.person.id"
:value="p.person.id" :value="p.person.id"
/> />
<label class="form-check-label" for="hasNoHousehold"> <label class="form-check-label">
{{ p.person.text }} {{ p.person.text }}
</label> </label>
</div> </div>
@ -58,11 +58,14 @@
</add-persons> </add-persons>
</div> </div>
<div v-if="!isParticipationValid" class="alert alert-warning to-confirm">
{{ $t('persons_associated.participation_not_valid') }}
</div>
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex'; import {mapGetters, mapState} from 'vuex';
import ParticipationItem from "./PersonsAssociated/ParticipationItem.vue" import ParticipationItem from "./PersonsAssociated/ParticipationItem.vue"
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue' import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue'
@ -89,6 +92,9 @@ export default {
courseId: state => state.accompanyingCourse.id, courseId: state => state.accompanyingCourse.id,
participations: state => state.accompanyingCourse.participations participations: state => state.accompanyingCourse.participations
}), }),
...mapGetters([
'isParticipationValid'
]),
currentParticipations() { currentParticipations() {
return this.participations.filter(p => p.endDate === null) return this.participations.filter(p => p.endDate === null)
}, },
@ -126,7 +132,7 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
div#accompanying-course { div#accompanying-course {
div.vue-component { div.vue-component {
& > div.alert { & > div.alert.no-household {
margin: 0 0 -1em; margin: 0 0 -1em;
} }
div.no-household { div.no-household {

View File

@ -19,20 +19,8 @@
v-if="hasCurrentHouseholdAddress" v-if="hasCurrentHouseholdAddress"
v-bind:person="participation.person"> v-bind:person="participation.person">
</button-location> </button-location>
<li> <li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="show"></on-the-fly></li>
<on-the-fly <li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly"></on-the-fly></li>
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> <!-- <li>
<button class="btn btn-delete" <button class="btn btn-delete"
:title="$t('action.delete')" :title="$t('action.delete')"
@ -112,22 +100,13 @@ export default {
getAccompanyingCourseReturnPath() { getAccompanyingCourseReturnPath() {
return `fr/parcours/${this.$store.state.accompanyingCourse.id}/edit#section-10`; 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> </script>

View File

@ -10,7 +10,7 @@
{{ $t('requestor.is_anonymous') }} {{ $t('requestor.is_anonymous') }}
</label> </label>
<third-party-render-box v-if="accompanyingCourse.requestor.type == 'thirdparty'" <third-party-render-box v-if="accompanyingCourse.requestor.type === 'thirdparty'"
:thirdparty="accompanyingCourse.requestor" :thirdparty="accompanyingCourse.requestor"
:options="{ :options="{
addLink: false, addLink: false,
@ -23,14 +23,13 @@
> >
<template v-slot:record-actions> <template v-slot:record-actions>
<ul class="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="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> </ul>
</template> </template>
</third-party-render-box> </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" :person="accompanyingCourse.requestor"
:options="{ :options="{
addLink: false, addLink: false,
@ -44,9 +43,8 @@
> >
<template v-slot:record-actions> <template v-slot:record-actions>
<ul class="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="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> </ul>
</template> </template>
</person-render-box> </person-render-box>
@ -129,6 +127,11 @@ export default {
this.$store.dispatch('addRequestor', selected.shift()); this.$store.dispatch('addRequestor', selected.shift());
this.$refs.addPersons.resetSearch(); // to cast child method this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false; 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> <template v-slot:record-actions>
<ul class="record_actions"> <ul class="record_actions">
<!--
<button-location v-if="hasCurrentHouseholdAddress" :person="resource.resource"></button-location> <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="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> <li><button class="btn btn-sm btn-remove" :title="$t('action.remove')" @click.prevent="$emit('remove', resource)"></button></li>
</ul> </ul>
</template> </template>
@ -22,7 +24,7 @@
<template v-slot:record-actions> <template v-slot:record-actions>
<ul class="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="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> <li><button class="btn btn-sm btn-remove" :title="$t('action.remove')" @click.prevent="$emit('remove', resource)"></button></li>
</ul> </ul>
</template> </template>
@ -53,6 +55,13 @@ export default {
} }
return false; return false;
} }
},
methods: {
saveFormOnTheFly(payload) {
console.log('saveFormOnTheFly: type', payload.type, ', data', payload.data);
payload.target = 'resource';
this.$store.dispatch('patchOnTheFly', payload);
}
} }
} }
</script> </script>

View File

@ -1,18 +1,20 @@
<template> <template>
<div class="vue-component"> <div class="vue-component">
<h2><a id="section-60"></a>{{ $t('scopes.title') }}</h2> <h2><a id="section-60"></a>{{ $t('scopes.title') }}</h2>
<ul> <div class="mb-4">
<li v-for="s in scopes"> <div class="form-check" v-for="s in scopes">
<input type="checkbox" v-model="checkedScopes" :value="s" /> <input class="form-check-input" type="checkbox" v-model="checkedScopes" :value="s" />
{{ s.name.fr }} <label class="form-check-label">
</li> {{ s.name.fr }}
</ul> </label>
</div>
</div>
<div v-if="!isScopeValid" class="alert alert-warning separator"> <div v-if="!isScopeValid" class="alert alert-warning to-confirm">
{{ $t('scopes.add_at_least_one') }} {{ $t('scopes.add_at_least_one') }}
</div> </div>
</div> </div>
</template> </template>

View File

@ -21,13 +21,17 @@
</VueMultiselect> </VueMultiselect>
</div> </div>
<div v-if="!isSocialIssueValid" class="alert alert-warning to-confirm">
{{ $t('social_issue.not_valid') }}
</div>
</div> </div>
</template> </template>
<script> <script>
import VueMultiselect from 'vue-multiselect'; import VueMultiselect from 'vue-multiselect';
import { getSocialIssues } from '../api'; import { getSocialIssues } from '../api';
import { mapState } from 'vuex'; import {mapGetters, mapState} from 'vuex';
export default { export default {
name: "SocialIssue", name: "SocialIssue",
@ -41,6 +45,9 @@ export default {
...mapState({ ...mapState({
value: state => state.accompanyingCourse.socialIssues, value: state => state.accompanyingCourse.socialIssues,
}), }),
...mapGetters([
'isSocialIssueValid'
])
}, },
mounted() { mounted() {
this.getOptions(); this.getOptions();

View File

@ -1,4 +1,5 @@
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 { addressMessages } from 'ChillMainAssets/vuejs/Address/i18n';
const appMessages = { const appMessages = {
@ -48,8 +49,9 @@ const appMessages = {
ok: "Oui, l'usager quitte le parcours", ok: "Oui, l'usager quitte le parcours",
show_household_number: "Voir le ménage (n° {id})", show_household_number: "Voir le ménage (n° {id})",
show_household: "Voir le ménage", 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", update_household: "Modifier l'appartenance",
participation_not_valid: "Sélectionnez ou créez au minimum 1 usager",
}, },
requestor: { requestor: {
title: "Demandeur", title: "Demandeur",
@ -72,6 +74,7 @@ const appMessages = {
social_issue: { social_issue: {
title: "Problématiques sociales", title: "Problématiques sociales",
label: "Choisir les problématiques sociales", label: "Choisir les problématiques sociales",
not_valid: "Sélectionnez au minimum une problématique sociale",
}, },
courselocation: { courselocation: {
title: "Localisation du parcours", title: "Localisation du parcours",
@ -79,11 +82,13 @@ const appMessages = {
edit_temporary_address: "Modifier l'adresse temporaire", edit_temporary_address: "Modifier l'adresse temporaire",
assign_course_address: "Désigner comme l'adresse du parcours", assign_course_address: "Désigner comme l'adresse du parcours",
remove_button: "Enlever l'adresse", remove_button: "Enlever l'adresse",
temporary_address_must_be_changed: "Cette adresse est temporaire et devrait être remplacée par celle d'un usager de référence.", 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: "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: "Êtes-vous sûr ?",
sure_description: "Voulez-vous faire de cette adresse l'adresse du parcours ?", sure_description: "Voulez-vous faire de cette adresse l'adresse du parcours ?",
ok: "Désigner comme adresse du parcours", ok: "Désigner comme adresse du parcours",
person_locator: "Parcours localisé auprès de {0}", person_locator: "Parcours localisé auprès de {0}",
not_valid: "Indiquez au minimum une localisation temporaire du parcours",
no_address: "Il n'y a pas d'adresse associée au parcours" no_address: "Il n'y a pas d'adresse associée au parcours"
}, },
scopes: { scopes: {
@ -137,7 +142,7 @@ const appMessages = {
} }
}; };
Object.assign(appMessages.fr, personMessages.fr, addressMessages.fr); Object.assign(appMessages.fr, personMessages.fr, thirdpartyMessages.fr, addressMessages.fr);
export { export {
appMessages appMessages

View File

@ -11,6 +11,8 @@ import { getAccompanyingCourse,
addScope, addScope,
removeScope, removeScope,
} from '../api'; } from '../api';
import { patchPerson } from "ChillPersonAssets/vuejs/_api/OnTheFly";
import { patchThirdparty } from "ChillThirdPartyAssets/vuejs/_api/OnTheFly";
const debug = process.env.NODE_ENV !== 'production'; const debug = process.env.NODE_ENV !== 'production';
@ -48,7 +50,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
return state.accompanyingCourse.location !== null; return state.accompanyingCourse.location !== null;
}, },
isScopeValid(state) { 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; return state.accompanyingCourse.scopes.length > 0;
}, },
validationKeys(state, getters) { validationKeys(state, getters) {
@ -107,6 +109,36 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
//console.log('### mutation: addResource', resource); //console.log('### mutation: addResource', resource);
state.accompanyingCourse.resources.push(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) { toggleIntensity(state, value) {
state.accompanyingCourse.intensity = value; state.accompanyingCourse.intensity = value;
}, },
@ -239,6 +271,38 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
resolve(); resolve();
})).catch((error) => { commit('catchError', error) }); })).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) { toggleIntensity({ commit }, payload) {
//console.log(payload); //console.log(payload);
patchAccompanyingCourse(id, { type: "accompanying_period", intensity: payload }) patchAccompanyingCourse(id, { type: "accompanying_period", intensity: payload })

View File

@ -109,7 +109,7 @@ export default {
this.toggleEditEvaluation(); this.toggleEditEvaluation();
}, },
buildEditLink(storedObject) { buildEditLink(storedObject) {
return `/fr/chill_wopi/edit/${storedObject.filename}?returnPath=` + encodeURIComponent( return `/edit/${storedObject.uuid}?returnPath=` + encodeURIComponent(
window.location.pathname + window.location.search + window.location.hash); window.location.pathname + window.location.search + window.location.hash);
}, },
} }

View File

@ -2,171 +2,126 @@
<h2 class="mt-4">{{ $t('household_members_editor.household_part') }}</h2> <h2 class="mt-4">{{ $t('household_members_editor.household_part') }}</h2>
<div v-if="hasHousehold"> <div v-if="mode == null">
<div class="flex-table"> <div class="alert alert-info">{{ $t('household_members_editor.household.no_household_choose_one') }}</div>
<div class="item-bloc">
<household-render-box :household="household" :isAddressMultiline="true"></household-render-box>
</div>
</div>
<div v-if="isHouseholdNew && !hasHouseholdAddress"> <div class="flex-table householdSuggestionList">
<div v-if="isModeNewAllowed" class="item-bloc">
<div v-if="hasAddressSuggestion" class="householdAddressSuggestion my-5"> <div>
<h4 class="mb-3"> <section>
{{ $t('household_members_editor.household.where_live_the_household') }} <div class="item-row">
</h4> <div class="item-col">
<div class="accordion" id="addressSuggestions"> <div class="h4">
<div class="accordion-item"> <i class="fa fa-home"></i> {{ $t('household_members_editor.household.new_household') }}
<h2 class="accordion-header" id="heading_address_suggestions">
<button v-if="!showAddressSuggestion"
class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
aria-expanded="false"
@click="toggleAddressSuggestion">
{{ $tc('household_members_editor.show_household_suggestion', countAddressSuggestion) }}
</button>
<button v-if="showAddressSuggestion"
class="accordion-button"
type="button"
data-bs-toggle="collapse"
aria-expanded="true"
@click="toggleAddressSuggestion">
{{ $t('household_members_editor.hide_household_suggestion') }}
</button>
</h2>
<div class="accordion-collapse" id="collapse_address_suggestions"
aria-labelledby="heading_address_suggestions" data-bs-parent="#addressSuggestions">
<div v-if="showAddressSuggestion">
<div class="flex-table householdAddressSuggestionList">
<div v-for="a in filterAddressesSuggestion" class="item-bloc">
<div class="float-button bottom">
<div class="box">
<div class="action">
<ul class="record_actions">
<li>
<button class="btn btn-sm btn-choose" @click="setHouseholdAddress(a)">
{{ $t('household_members_editor.household.household_live_to_this_address') }}
</button>
</li>
</ul>
</div>
<ul class="list-content fa-ul">
<li>
<i class="fa fa-li fa-map-marker"></i>
<address-render-box :address="a"></address-render-box>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div>
</section>
<ul class="record_actions">
<li>
<button @click="setModeNew" class="btn btn-sm btn-create">{{ $t('household_members_editor.household.create_household') }}</button>
</li>
</ul>
</div>
</div>
<!-- if allow leave household -->
<div v-if="isModeLeaveAllowed" class="item-bloc">
<div>
<section>
<div class="item-row">
<div class="item-col">
<div class="h4">
<span class="fa-stack fa-lg">
<i class="fa fa-home fa-stack-1x"></i>
<i class="fa fa-ban fa-stack-2x text-danger"></i>
</span>
{{ $t('household_members_editor.household.leave_without_household') }}
</div>
</div>
</div>
<div class="item-row">
{{ $t('household_members_editor.household.will_leave_any_household_explanation')}}
</div>
</section>
<ul class="record_actions">
<li>
<button @click="setModeLeave" class="btn btn-sm">
<i class="fa fa-sign-out"></i>
{{ $t('household_members_editor.household.leave') }}
</button>
</li>
</ul>
</div>
</div>
<div v-for="item in getSuggestions">
<div class="item-bloc">
<household-render-box :household="item.household"></household-render-box>
<ul class="record_actions">
<li>
<button class="btn btn-sm btn-choose" @click="selectHousehold(item.household)">
{{ $t('household_members_editor.select_household') }}
</button>
</li>
</ul>
</div> </div>
</div> </div>
</div>
</div>
<ul class="record_actions"> <div v-else>
<li > <div class="flex-table">
<add-address <div class="item-bloc">
:context="addAddress.context" <template v-if="isModeLeave">
<section>
<div class="item-row">
<div class="item-col">
<div class="h4">
<span class="fa-stack fa-lg">
<i class="fa fa-home fa-stack-1x"></i>
<i class="fa fa-ban fa-stack-2x text-danger"></i>
</span>
{{ $t('household_members_editor.household.leave_without_household') }}
</div>
</div>
</div>
<div class="item-row">
{{ $t('household_members_editor.household.will_leave_any_household_explanation')}}
</div>
</section>
</template>
<template v-else>
<household-render-box :household="household" :isAddressMultiline="true"></household-render-box>
<ul class="record_actions">
<li>
<add-address
:context="getAddressContext"
:key="addAddress.key" :key="addAddress.key"
:options="addAddress.options" :options="addAddress.options"
:result="addAddress.result" :addressChangedCallback="addressChanged"
@addressChangedCallback="setHouseholdCreatedAddress" ></add-address>
ref="addAddress"> </li>
</add-address> <li v-if="hasHouseholdAddress">
</li> <button class="btn btn-remove"
</ul> @click="removeHouseholdAddress">
</div> {{ $t('household_members_editor.household.remove_address') }}
</button>
<div v-if="isHouseholdNew && hasHouseholdAddress"> </li>
<ul class="record_actions"> </ul>
<li > </template>
<button class="btn btn-misc" @click="removeHouseholdAddress">
{{ $t('household_members_editor.household.delete_this_address') }}
</button>
</li>
</ul>
</div>
</div>
<div v-else-if="isForceLeaveWithoutHousehold">
{{ $t('household_members_editor.household.will_leave_any_household') }}
</div>
<div v-else class="alert alert-info">{{ $t('household_members_editor.household.no_household_choose_one') }}</div>
<ul v-if="allowChangeHousehold" class="record_actions">
<li v-if="allowHouseholdCreate">
<button class="btn btn-create" @click="createHousehold">
{{ $t('household_members_editor.household.create_household') }}
</button>
</li>
<li v-if="allowHouseholdSearch">
<button class="btn btn-misc">
<i class="fa fa-search"></i>{{ $t('household_members_editor.household.search_household') }}
</button>
</li>
<li v-if="allowLeaveWithoutHousehold" >
<button @click="forceLeaveWithoutHousehold" class="btn btn-orange">
<i class="fa fa-sign-out"></i>{{ $t('household_members_editor.household.leave_without_household') }}
</button>
</li>
<li v-if="allowRemoveHousehold">
<button @click="removeHousehold" class="btn">
{{ $t('household_members_editor.household.change') }}
</button>
</li>
</ul>
<div v-if="hasHouseholdSuggestion" class="householdSuggestions my-5">
<h4 class="mb-3">
{{ $t('household_members_editor.household_for_participants_accompanying_period') }} :
</h4>
<div class="accordion" id="householdSuggestions">
<div class="accordion-item">
<h2 class="accordion-header" id="heading_household_suggestions">
<button v-if="!showHouseholdSuggestion"
class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
aria-expanded="false"
@click="toggleHouseholdSuggestion">
{{ $tc('household_members_editor.show_household_suggestion', countHouseholdSuggestion) }}
</button>
<button v-if="showHouseholdSuggestion"
class="accordion-button"
type="button"
data-bs-toggle="collapse"
aria-expanded="true"
@click="toggleHouseholdSuggestion">
{{ $t('household_members_editor.hide_household_suggestion') }}
</button>
<!-- disabled bootstrap behaviour: data-bs-target="#collapse_household_suggestions" aria-controls="collapse_household_suggestions" -->
</h2>
<div class="accordion-collapse" id="collapse_household_suggestions"
aria-labelledby="heading_household_suggestions" data-bs-parent="#householdSuggestions">
<div v-if="showHouseholdSuggestion">
<div class="flex-table householdSuggestionList">
<div v-for="h in filterHouseholdSuggestionByAccompanyingPeriod" class="item-bloc">
<household-render-box :household="h"></household-render-box>
<ul class="record_actions">
<li>
<button class="btn btn-sm btn-choose" @click="selectHousehold(h)">
{{ $t('household_members_editor.select_household') }}
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</div>
<ul v-if="isModeNewAllowed || isModeLeaveAllowed || getModeSuggestions.length > 0" class="record_actions">
<li>
<button class="btn btn-sm btn-chill-beige" @click="resetMode">
{{ $t('household_members_editor.household.reset_mode') }}
</button>
</li>
</ul>
</div>
</div>
</template> </template>
<script> <script>
@ -185,35 +140,32 @@ export default {
data() { data() {
return { return {
addAddress: { addAddress: {
context: {
target: {
name: 'household_create',
id: 0
},
edit: false,
addressId: null
},
key: 'household_new', key: 'household_new',
options: { options: {
useDate: { useDate: {
validFrom: true validFrom: false,
validTo: false,
}, },
onlyButton: true,
button: { button: {
text: { text: {
create: 'household_members_editor.household.or_create_new_address', create: 'household_members_editor.household.set_address',
edit: null, edit: 'household_members_editor.household.update_address',
} }
}, },
title: { title: {
create: 'household_members_editor.household.create_new_address', create: 'household_members_editor.household.create_new_address',
edit: null, edit: 'household_members_editor.household.update_address_title',
}, },
} }
} }
} };
}, },
computed: { computed: {
...mapGetters([ ...mapGetters([
'isModeNewAllowed',
'isModeLeaveAllowed',
'getSuggestions',
'hasHousehold', 'hasHousehold',
'isHouseholdNew', 'isHouseholdNew',
'hasHouseholdSuggestion', 'hasHouseholdSuggestion',
@ -223,68 +175,38 @@ export default {
'countAddressSuggestion', 'countAddressSuggestion',
'filterAddressesSuggestion', 'filterAddressesSuggestion',
'hasHouseholdAddress', 'hasHouseholdAddress',
'isModeLeave',
'getAddressContext',
]), ]),
...mapState([ ...mapState([
'showHouseholdSuggestion', 'showHouseholdSuggestion',
'showAddressSuggestion' 'showAddressSuggestion',
'mode',
]), ]),
household() { household() {
return this.$store.state.household; return this.$store.state.household;
}, },
allowHouseholdCreate() {
return this.$store.state.allowHouseholdCreate && !this.$store.getters.hasHousehold;
},
allowHouseholdSearch() { allowHouseholdSearch() {
return false; return false;
return this.$store.state.allowHouseholdSearch && !this.$store.getters.hasHousehold; return this.$store.state.allowHouseholdSearch && !this.$store.getters.hasHousehold;
}, },
allowLeaveWithoutHousehold() {
return this.$store.state.allowLeaveWithoutHousehold && !this.$store.getters.hasHousehold;
},
allowRemoveHousehold() {
return this.$store.getters.hasHousehold &&
(
this.allowHouseholdCreate || this.allowHouseholdSearch ||
this.allowLeaveWithoutHousehold
)
;
},
allowChangeHousehold() {
return this.allowHouseholdCreate || this.allowHouseholdSearch ||
this.allowLeaveWithoutHousehold;
},
isForceLeaveWithoutHousehold() {
return this.$store.state.forceLeaveWithoutHousehold;
}
}, },
methods: { methods: {
createHousehold() { setModeNew() {
this.$store.dispatch('createHousehold'); this.$store.dispatch('createHousehold');
}, },
forceLeaveWithoutHousehold() { setModeLeave() {
this.$store.dispatch('forceLeaveWithoutHousehold'); this.$store.dispatch('forceLeaveWithoutHousehold');
}, },
toggleHouseholdSuggestion() { resetMode() {
this.$store.commit('toggleHouseholdSuggestion'); this.$store.commit('resetMode');
}, },
toggleAddressSuggestion() { addressChanged(payload) {
this.$store.commit('toggleAddressSuggestion'); console.log("addressChanged", payload);
this.$store.dispatch('setHouseholdNewAddress', payload.address);
}, },
selectHousehold(h) { selectHousehold(h) {
this.$store.dispatch('selectHousehold', h); this.$store.dispatch('selectHousehold', h);
this.toggleHouseholdSuggestion();
},
removeHousehold() {
this.$store.dispatch('removeHousehold');
},
setHouseholdAddress(a) {
let payload = this.$refs.addAddress.submitNewAddress();
console.log('setHouseholdAddress', a);
this.$store.commit('setHouseholdAddress', a);
},
setHouseholdCreatedAddress(payload) {
console.log('setHouseholdAddress', payload);
this.$store.dispatch('setHouseholdNewAddress', payload);
}, },
removeHouseholdAddress() { removeHouseholdAddress() {
this.$store.commit('removeHouseholdAddress'); this.$store.commit('removeHouseholdAddress');
@ -310,28 +232,4 @@ div.householdSuggestionList {
} }
} }
} }
/*
div.householdAddressSuggestionList {
display: flex;
list-style-type: none;
padding: 0;
& > li {}
}
.householdSuggestionList {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
& > .item {
margin-bottom: 0.8rem;
width: calc(50% - 1rem);
border: 1px solid var(--chill-light-gray);
padding: 0.5rem 0.5rem 0 0.5rem;
ul.record_actions {
margin-bottom: 0;
}
}
}
*/
</style> </style>

View File

@ -7,16 +7,27 @@ const appMessages = {
household: { household: {
no_household_choose_one: "Aucun ménage de destination. Choisissez un ménage. Les usagers concernés par la modification apparaitront ensuite.", no_household_choose_one: "Aucun ménage de destination. Choisissez un ménage. Les usagers concernés par la modification apparaitront ensuite.",
new_household: "Nouveau ménage", new_household: "Nouveau ménage",
create_household: "Créer un nouveau ménage de destination", create_household: "Créer",
search_household: "Chercher un ménage", search_household: "Chercher un ménage",
will_leave_any_household: "Ne rejoignent pas de ménage", will_leave_any_household: "Les usagers ne rejoignent pas de ménage",
leave: "Quitter",
will_leave_any_household_explanation: "Les usagers quitteront leur ménage actuel, et ne seront pas associés à un autre ménage. Par ailleurs, ils seront enregistrés comme étant sans adresse connue.",
leave_without_household: "Sans nouveau ménage", leave_without_household: "Sans nouveau ménage",
set_address: "Indiquer une adresse",
reset_mode: "Modifier la destination",
remove_address: "Supprimer l'adresse",
update_address: "Mettre à jour l'adresse",
// remove ?
/*
where_live_the_household: "À quelle adresse habite ce ménage ?", where_live_the_household: "À quelle adresse habite ce ménage ?",
household_live_to_this_address: "Sélectionner l'adresse", household_live_to_this_address: "Sélectionner l'adresse",
no_suggestions: "Aucune adresse à suggérer", no_suggestions: "Aucune adresse à suggérer",
delete_this_address: "Supprimer cette adresse", delete_this_address: "Supprimer cette adresse",
create_new_address: "Créer une nouvelle adresse", create_new_address: "Créer une nouvelle adresse",
or_create_new_address: "Ou créer une nouvelle adresse", or_create_new_address: "Ou créer une nouvelle adresse",
*/
// end remove ?
}, },
concerned: { concerned: {
title: "Nouveaux membres du ménage", title: "Nouveaux membres du ménage",
@ -34,7 +45,7 @@ const appMessages = {
is_not_holder: "N'est pas titulaire", is_not_holder: "N'est pas titulaire",
remove_position: "Retirer des {position}", remove_position: "Retirer des {position}",
remove_concerned: "Ne plus transférer", remove_concerned: "Ne plus transférer",
household_part: "Ménage de destination", household_part: "Destination",
suggestions: "Suggestions", suggestions: "Suggestions",
hide_household_suggestion: "Masquer les suggestions", hide_household_suggestion: "Masquer les suggestions",
show_household_suggestion: 'Aucune suggestion | Afficher une suggestion | Afficher {count} suggestions', show_household_suggestion: 'Aucune suggestion | Afficher une suggestion | Afficher {count} suggestions',

View File

@ -29,6 +29,15 @@ const store = createStore({
return 0; return 0;
}), }),
startDate: new Date(), startDate: new Date(),
/**
* Indicates if the destination is:
*
* * "new" => a new household
* * "existing" => an existing household
* * "leave" => leave without household
* * null if not set
*/
mode: window.household_members_editor_data.household === null ? null : "existing",
allowHouseholdCreate: window.household_members_editor_data.allowHouseholdCreate, allowHouseholdCreate: window.household_members_editor_data.allowHouseholdCreate,
allowHouseholdSearch: window.household_members_editor_data.allowHouseholdSearch, allowHouseholdSearch: window.household_members_editor_data.allowHouseholdSearch,
allowLeaveWithoutHousehold: window.household_members_editor_data.allowLeaveWithoutHousehold, allowLeaveWithoutHousehold: window.household_members_editor_data.allowLeaveWithoutHousehold,
@ -41,11 +50,70 @@ const store = createStore({
errors: [] errors: []
}, },
getters: { getters: {
/**
* return true if this page allow to create a new household
*
* @returns {boolean}
*/
isModeNewAllowed(state) {
return state.allowHouseholdCreate;
},
/**
* return true if this page allow to "leave without household"
*
* @returns {boolean}
*/
isModeLeaveAllowed(state) {
return state.allowLeaveWithoutHousehold;
},
/**
* return true if the mode "leave" is selected
*
* @returns {boolean}
*/
isModeLeave(state) {
return state.mode === "leave";
},
getSuggestions(state) {
let suggestions = [];
state.householdSuggestionByAccompanyingPeriod.forEach(h => {
console.log(h);
suggestions.push({household: h});
});
return suggestions;
},
isHouseholdNew(state) { isHouseholdNew(state) {
return state.mode === "new";
/*
if (state.household === null) { if (state.household === null) {
return false; return false;
} }
return !Number.isInteger(state.household.id); return !Number.isInteger(state.household.id);
*/
},
getAddressContext(state, getters) {
if (!getters.hasHouseholdAddress) {
return {
edit: false,
addressId: null,
target: {
name: state.household.type,
id: state.household.id
},
suggestions: state.addressesSuggestion
};
} else {
return {
edit: true,
addressId: state.household.current_address.id,
target: {
name: state.household.type,
id: state.household.id
},
};
}
}, },
hasHouseholdAddress(state) { hasHouseholdAddress(state) {
if (null === state.household) { if (null === state.household) {
@ -180,6 +248,11 @@ const store = createStore({
}, },
}, },
mutations: { mutations: {
resetMode(state) {
state.mode = null;
state.household = null;
state.forceLeaveWithoutHousehold = false;
},
addConcerned(state, person) { addConcerned(state, person) {
let persons = state.concerned.map(conc => conc.person.id); let persons = state.concerned.map(conc => conc.person.id);
if (!persons.includes(person.id)) { if (!persons.includes(person.id)) {
@ -222,6 +295,7 @@ const store = createStore({
current_address: null, current_address: null,
current_members_id: [] current_members_id: []
}; };
state.mode = "new";
state.forceLeaveWithoutHousehold = false; state.forceLeaveWithoutHousehold = false;
}, },
removeHousehold(state) { removeHousehold(state) {
@ -229,6 +303,7 @@ const store = createStore({
state.forceLeaveWithoutHousehold = false; state.forceLeaveWithoutHousehold = false;
}, },
setHouseholdAddress(state, address) { setHouseholdAddress(state, address) {
console.log('setHouseholdAddress commit', address);
if (null === state.household) { if (null === state.household) {
console.error("no household"); console.error("no household");
throw new Error("No household"); throw new Error("No household");
@ -246,10 +321,12 @@ const store = createStore({
}, },
forceLeaveWithoutHousehold(state) { forceLeaveWithoutHousehold(state) {
state.household = null; state.household = null;
state.mode = "leave";
state.forceLeaveWithoutHousehold = true; state.forceLeaveWithoutHousehold = true;
}, },
selectHousehold(state, household) { selectHousehold(state, household) {
state.household = household; state.household = household;
state.mode = "existing";
state.forceLeaveWithoutHousehold = false; state.forceLeaveWithoutHousehold = false;
}, },
setHouseholdSuggestionByAccompanyingPeriod(state, households) { setHouseholdSuggestionByAccompanyingPeriod(state, households) {
@ -320,18 +397,8 @@ const store = createStore({
commit('createHousehold'); commit('createHousehold');
dispatch('computeWarnings'); dispatch('computeWarnings');
}, },
setHouseholdNewAddress({ commit }, payload) { setHouseholdNewAddress({ commit }, address) {
let url = `/api/1.0/main/address/${payload.addressId}.json`; commit('setHouseholdAddress', address);
window.fetch(url).then(r => {
if (r.ok) {
return r.json();
}
throw new Error("error while fetch address");
}).then(data => {
commit('setHouseholdAddress', data);
}).catch(e => {
console.error(e);
});
}, },
forceLeaveWithoutHousehold({ commit, dispatch }) { forceLeaveWithoutHousehold({ commit, dispatch }) {
commit('forceLeaveWithoutHousehold'); commit('forceLeaveWithoutHousehold');

View File

@ -28,7 +28,26 @@ const postPerson = (body) => {
}); });
}; };
/*
* 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 { export {
getPerson, getPerson,
postPerson postPerson,
patchPerson
}; };

View File

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

@ -27,7 +27,7 @@
</div> </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> <i :class="'fa fa-fw ' + getGenderIcon" title="{{ getGender }}"></i>
<time v-if="person.birthdate && !person.deathdate" datetime="{{ person.birthdate }}" title="{{ birthdate }}"> <time v-if="person.birthdate && !person.deathdate" datetime="{{ person.birthdate }}" title="{{ birthdate }}">
{{ $t(getGenderTranslation) + ' ' + $d(birthdate, 'text') }} {{ $t(getGenderTranslation) + ' ' + $d(birthdate, 'text') }}
@ -142,7 +142,7 @@ export default {
props: ['person', 'options', 'render', 'returnPath'], props: ['person', 'options', 'render', 'returnPath'],
computed: { computed: {
getGenderTranslation: function() { 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() { isMultiline: function() {
if(this.options.isMultiline){ if(this.options.isMultiline){
@ -152,7 +152,7 @@ export default {
} }
}, },
getGenderIcon: function() { 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(){ birthdate: function(){
if(this.person.birthdate !== null){ if(this.person.birthdate !== null){

View File

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

View File

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

View File

@ -17,11 +17,19 @@
{%- endif -%} {%- endif -%}
</div> </div>
<div class="text-md-end"> <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"> <span class="open_sansbold">
{{ 'Center'|trans|upper}} : {{ 'Center'|trans|upper}} :
</span> </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 -%} {%- endif -%}
</div> </div>
</div> </div>

View File

@ -298,6 +298,35 @@ paths:
$ref: "#/components/schemas/Person" $ref: "#/components/schemas/Person"
403: 403:
description: "Unauthorized" 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: /1.0/person/person.json:
post: post:

View File

@ -61,20 +61,10 @@ class ThirdPartyController extends Controller
$repository = $this->getDoctrine()->getManager() $repository = $this->getDoctrine()->getManager()
->getRepository(ThirdParty::class); ->getRepository(ThirdParty::class);
$centers = $this->authorizationHelper $nbThirdParties = $repository->count([]); //$repository->countByMemberOfCenters($centers);
->getReachableCenters(
$this->getUser(),
new Role(ThirdPartyVoter::SHOW)
);
$nbThirdParties = $repository->countByMemberOfCenters($centers); //
$pagination = $this->paginatorFactory->create($nbThirdParties); $pagination = $this->paginatorFactory->create($nbThirdParties);
$thirdParties = $repository->findByMemberOfCenters( $thirdParties = $repository->findAll();
$centers,
$pagination->getCurrentPage()->getFirstItemNumber(),
$pagination->getItemsPerPage()
);
return $this->render('ChillThirdPartyBundle:ThirdParty:index.html.twig', array( return $this->render('ChillThirdPartyBundle:ThirdParty:index.html.twig', array(
'third_parties' => $thirdParties, 'third_parties' => $thirdParties,
@ -89,16 +79,7 @@ class ThirdPartyController extends Controller
{ {
$this->denyAccessUnlessGranted(ThirdPartyVoter::CREATE); $this->denyAccessUnlessGranted(ThirdPartyVoter::CREATE);
/* $centers = $this->authorizationHelper $centers = [];
->getReachableCenters(
$this->getUser(),
new Role(ThirdPartyVoter::CREATE)
);
if ($centers === []) { //
throw new \LogicException("There should be at least one center reachable "
. "if role ".ThirdPartyVoter::CREATE." is granted");
} */
$thirdParty = new ThirdParty(); $thirdParty = new ThirdParty();
$thirdParty->setCenters(new ArrayCollection($centers)); $thirdParty->setCenters(new ArrayCollection($centers));
@ -142,16 +123,10 @@ class ThirdPartyController extends Controller
{ {
$this->denyAccessUnlessGranted(ThirdPartyVoter::CREATE); $this->denyAccessUnlessGranted(ThirdPartyVoter::CREATE);
/* $centers = $this->authorizationHelper $repository = $this->getDoctrine()->getManager()
->getReachableCenters( ->getRepository(ThirdParty::class);
$this->getUser(),
new Role(ThirdPartyVoter::CREATE)
);
if ($centers === []) { $centers = $repository->findAll();
throw new \LogicException("There should be at least one center reachable "
. "if role ".ThirdPartyVoter::CREATE." is granted");
} */
// we want to keep centers the users has no access to. So we will add them // 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 // later if they are removed. (this is a ugly hack but it will works

View File

@ -59,27 +59,23 @@ class ChillThirdPartyExtension extends Extension implements PrependExtensionInte
'class' => \Chill\ThirdPartyBundle\Entity\ThirdParty::class, 'class' => \Chill\ThirdPartyBundle\Entity\ThirdParty::class,
'name' => 'thirdparty', 'name' => 'thirdparty',
'base_path' => '/api/1.0/thirdparty/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, //'controller' => \Chill\ThirdPartyBundle\Controller\ThirdPartyApiController::class,
'actions' => [ 'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
Request::METHOD_POST => true,
],
],
'_entity' => [ '_entity' => [
'methods' => [ 'methods' => [
Request::METHOD_GET => true, Request::METHOD_GET => true,
Request::METHOD_HEAD => true, Request::METHOD_HEAD => true,
Request::METHOD_POST=> true, Request::METHOD_POST => true,
Request::METHOD_PUT => true,
Request::METHOD_PATCH => true
], ],
'roles' => [ 'roles' => [
Request::METHOD_GET => \Chill\ThirdPartyBundle\Security\Voter\ThirdPartyVoter::SHOW, Request::METHOD_GET => \Chill\ThirdPartyBundle\Security\Voter\ThirdPartyVoter::SHOW,
Request::METHOD_HEAD => \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_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 * @var string
* @ORM\Column(name="name", type="string", length=255) * @ORM\Column(name="name", type="string", length=255)
* @Assert\Length(min="2") * @Assert\Length(min="2")
* @Groups({"read"}) * @Groups({"read", "write"})
*/ */
private $name; private $name;
@ -69,6 +69,7 @@ class ThirdParty
* @var string * @var string
* @ORM\Column(name="name_company", type="string", length=255, nullable=true) * @ORM\Column(name="name_company", type="string", length=255, nullable=true)
* @Assert\Length(min="3") * @Assert\Length(min="3")
* @Groups({"read", "write"})
*/ */
private $nameCompany; private $nameCompany;
@ -77,6 +78,7 @@ class ThirdParty
* @var string * @var string
* @ORM\Column(name="acronym", type="string", length=64, nullable=true) * @ORM\Column(name="acronym", type="string", length=64, nullable=true)
* @Assert\Length(min="2") * @Assert\Length(min="2")
* @Groups({"read", "write"})
*/ */
private $acronym; private $acronym;
@ -94,7 +96,7 @@ class ThirdParty
* @ORM\Column(name="types", type="json", nullable=true) * @ORM\Column(name="types", type="json", nullable=true)
* @Assert\Count(min=1) * @Assert\Count(min=1)
*/ */
private $type; private $types;
/** /**
* Contact Persons: One Institutional ThirdParty has Many Contact Persons * Contact Persons: One Institutional ThirdParty has Many Contact Persons
@ -130,7 +132,7 @@ class ThirdParty
* @Assert\Regex("/^([\+{1}])([0-9\s*]{4,20})$/", * @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" * 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; private $telephone;
@ -138,7 +140,7 @@ class ThirdParty
* @var string|null * @var string|null
* @ORM\Column(name="email", type="string", length=255, nullable=true) * @ORM\Column(name="email", type="string", length=255, nullable=true)
* @Assert\Email(checkMX=false) * @Assert\Email(checkMX=false)
* @Groups({"read"}) * @Groups({"read", "write"})
*/ */
private $email; private $email;
@ -147,7 +149,7 @@ class ThirdParty
* @ORM\ManyToOne(targetEntity="\Chill\MainBundle\Entity\Address", * @ORM\ManyToOne(targetEntity="\Chill\MainBundle\Entity\Address",
* cascade={"persist", "remove"}) * cascade={"persist", "remove"})
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL") * @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
* @Groups({"read"}) * @Groups({"read", "write"})
*/ */
private $address; private $address;
@ -168,7 +170,6 @@ class ThirdParty
* @var Collection * @var Collection
* @ORM\ManyToMany(targetEntity="\Chill\MainBundle\Entity\Center") * @ORM\ManyToMany(targetEntity="\Chill\MainBundle\Entity\Center")
* @ORM\JoinTable(name="chill_3party.party_center") * @ORM\JoinTable(name="chill_3party.party_center")
* @Assert\Count(min=1)
*/ */
private $centers; private $centers;
@ -325,7 +326,7 @@ class ThirdParty
* @param array|null $type * @param array|null $type
* @return ThirdParty * @return ThirdParty
*/ */
public function setType(array $type = null) public function setTypes(array $type = null)
{ {
// remove all keys from the input data // remove all keys from the input data
$this->type = \array_values($type); $this->type = \array_values($type);
@ -338,9 +339,9 @@ class ThirdParty
* *
* @return array|null * @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) { if (count($types) === 1) {
$builder $builder
->add('type', HiddenType::class, [ ->add('types', HiddenType::class, [
'data' => array_values($types) 'data' => array_values($types)
]) ])
->get('type') ->get('types')
->addModelTransformer(new CallbackTransformer( ->addModelTransformer(new CallbackTransformer(
function (?array $typeArray): ?string { function (?array $typeArray): ?string {
if (null === $typeArray) { if (null === $typeArray) {
@ -84,7 +84,7 @@ class ThirdPartyType extends AbstractType
)) ))
; ;
} else { } else {
$builder->add('type', ChoiceType::class, [ $builder->add('types', ChoiceType::class, [
'choices' => $types, 'choices' => $types,
'expanded' => true, 'expanded' => true,
'multiple' => true, 'multiple' => true,

View File

@ -13,7 +13,7 @@ const getThirdparty = (id) => {
}; };
/* /*
* POST a new person * POST a new thirdparty
*/ */
const postThirdparty = (body) => { const postThirdparty = (body) => {
const url = `/api/1.0/thirdparty/thirdparty.json`; const url = `/api/1.0/thirdparty/thirdparty.json`;
@ -30,7 +30,26 @@ const postThirdparty = (body) => {
}); });
}; };
/*
* 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 { export {
getThirdparty, getThirdparty,
postThirdparty postThirdparty,
patchThirdparty
}; };

View File

@ -8,25 +8,17 @@
<div :class="'denomination h' + options.hLevel"> <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> <span class="name">{{ thirdparty.text }}</span>
</a> </a>
<span class="name">{{ thirdparty.text }}</span> <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.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> </div>
<p v-if="this.options.addInfo == true" class="moreinfo"> <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> </p>
</div> </div>
</div> </div>
@ -42,9 +34,9 @@
<i class="fa fa-li fa-map-marker"></i> <i class="fa fa-li fa-map-marker"></i>
<address-render-box :address="thirdparty.address" :isMultiline="isMultiline"></address-render-box> <address-render-box :address="thirdparty.address" :isMultiline="isMultiline"></address-render-box>
</li> </li>
<li v-if="thirdparty.telephone"> <li v-if="thirdparty.phonenumber">
<i class="fa fa-li fa-mobile"></i> <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>
<li v-if="thirdparty.email"> <li v-if="thirdparty.email">
<i class="fa fa-li fa-envelope-o"></i> <i class="fa fa-li fa-envelope-o"></i>
@ -78,21 +70,7 @@ export default {
} else { } else {
return false 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> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div v-if="action === 'show'"> <div v-if="action === 'show'">
<div class="flex-table"> <div class="flex-table">
<third-party-render-box <third-party-render-box
:thirdparty="thirdparty" :thirdparty="thirdparty"
@ -21,13 +21,8 @@
<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="firstname" v-model="thirdparty.firstName" v-bind:placeholder="$t('thirdparty.firstname')" /> <input class="form-control form-control-lg" id="name" v-model="thirdparty.text" v-bind:placeholder="$t('thirdparty.name')" />
<label for="firstname">{{ $t('thirdparty.firstname') }}</label> <label for="name">{{ $t('thirdparty.name') }}</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>
</div> </div>
<div class="input-group mb-3"> <div class="input-group mb-3">
@ -47,44 +42,82 @@
v-bind:aria-label="$t('thirdparty.phonenumber')" v-bind:aria-label="$t('thirdparty.phonenumber')"
aria-describedby="phonenumber" /> aria-describedby="phonenumber" />
</div> </div>
<!--
<add-address
:context="this.context"
:options="this.addAddress.options"
:address-changed-callback="submitAddress">
</add-address>
-->
</div> </div>
</template> </template>
<script> <script>
import ThirdPartyRenderBox from '../Entity/ThirdPartyRenderBox.vue'; 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 { export default {
name: "OnTheFlyThirdParty", name: "OnTheFlyThirdParty",
props: ['id', 'type', 'action'], props: ['id', 'type', 'action'],
components: { components: {
ThirdPartyRenderBox, ThirdPartyRenderBox,
AddAddress
}, },
data: function() { data() {
return { return {
thirdparty: { thirdparty: {
type: 'thirdparty' type: 'thirdparty'
},
addAddress: {
options: {
openPanesInModal: true,
onlyButton: false,
/*
button: {
text: {
create: 'courselocation.add_temporary_address',
edit: 'courselocation.edit_temporary_address'
}
},
title: {
create: 'courselocation.add_temporary_address',
edit: 'courselocation.edit_temporary_address'
} */
}
} }
} }
}, },
computed: {
context() {
let context = {
target: {
name: this.type,
id: this.id
},
edit: false,
addressId: null
};
console.log('context', context);
return context;
},
},
methods: { methods: {
loadThirdparty(){ loadData(){
getThirdparty(this.id).then(thirdparty => new Promise((resolve, reject) => { getThirdparty(this.id).then(thirdparty => new Promise((resolve, reject) => {
this.thirdparty = thirdparty; this.thirdparty = thirdparty;
console.log('get thirdparty', thirdparty);
resolve(); resolve();
})); }));
}, },
postData() { submitAddress(payload) {
postThirdparty(this.thirdparty).then(thirdparty => new Promise((resolve, reject) => { console.log('submitAddress', payload);
this.thirdparty = thirdparty;
resolve();
}))
} }
}, },
mounted() { mounted() {
if (this.action !== 'create'){ if (this.action !== 'create') {
this.loadThirdparty(); 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> <th>{{ (tp.active ? '<i class="fa fa-check chill-green">' : '<i class="fa fa-times chill-red">')|raw }}</th>
<td>{{ tp.name }}</td> <td>{{ tp.name }}</td>
{% set types = [] %} {% set types = [] %}
{% for t in tp.type %} {% for t in tp.types %}
{% set types = types|merge( [ ('chill_3party.key_label.'~t)|trans ] ) %} {% set types = types|merge( [ ('chill_3party.key_label.'~t)|trans ] ) %}
{% endfor %} {% endfor %}
<td>{{ types|join(', ') }}</td> <td>{{ types|join(', ') }}</td>

View File

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

View File

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

View File

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

View File

@ -64,6 +64,8 @@ class ThirdPartyVoter extends AbstractChillVoter implements ProvideRoleHierarchy
return false; return false;
} }
return true;
$centers = $this->authorizationHelper $centers = $this->authorizationHelper
->getReachableCenters($user, new Role($attribute)); ->getReachableCenters($user, new Role($attribute));

View File

@ -8,17 +8,57 @@ servers:
- url: "/api" - url: "/api"
description: "Your current dev server" 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: paths:
/1.0/thirdparty/thirdparty.json: /1.0/thirdparty/thirdparty.json:
get: post:
tags: tags:
- thirdparty - 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: responses:
200: 200:
description: "ok" description: "OK"
401: content:
application/json:
schema:
$ref: "#/components/schemas/Thirdparty"
403:
description: "Unauthorized" description: "Unauthorized"
422:
description: "Invalid data"
/1.0/thirdparty/thirdparty/{id}.json: /1.0/thirdparty/thirdparty/{id}.json:
get: get:
@ -41,3 +81,32 @@ paths:
description: "not found" description: "not found"
401: 401:
description: "Unauthorized" 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"

View File

@ -19,7 +19,7 @@
"php": ">= 7.4", "php": ">= 7.4",
"champs-libres/wopi-bundle": "dev-master", "champs-libres/wopi-bundle": "dev-master",
"nyholm/psr7": "^1.4", "nyholm/psr7": "^1.4",
"php-opencloud/openstack": "^3.2.1" "symfony/mime": "^4 || ^5"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

View File

@ -9,9 +9,10 @@ declare(strict_types=1);
namespace Chill\WopiBundle\Controller; namespace Chill\WopiBundle\Controller;
use ChampsLibres\WopiLib\Configuration\WopiConfigurationInterface; use ChampsLibres\WopiLib\Contract\Service\Configuration\ConfigurationInterface;
use ChampsLibres\WopiLib\Discovery\WopiDiscoveryInterface; use ChampsLibres\WopiLib\Contract\Service\Discovery\DiscoveryInterface;
use Chill\DocStoreBundle\Repository\StoredObjectRepository; use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\WopiBundle\Service\Controller\ResponderInterface; use Chill\WopiBundle\Service\Controller\ResponderInterface;
use Exception; use Exception;
use loophp\psr17\Psr17Interface; use loophp\psr17\Psr17Interface;
@ -23,11 +24,11 @@ use Symfony\Component\Security\Core\Security;
final class Test final class Test
{ {
private StoredObjectRepository $storedObjectRepository; private DiscoveryInterface $wopiDiscovery;
private WopiDiscoveryInterface $wopiDiscovery; private DocumentManagerInterface $documentManager;
private WopiConfigurationInterface $wopiConfiguration; private ConfigurationInterface $wopiConfiguration;
private ResponderInterface $responder; private ResponderInterface $responder;
@ -38,15 +39,15 @@ final class Test
private RouterInterface $router; private RouterInterface $router;
public function __construct( public function __construct(
StoredObjectRepository $storedObjectRepository, ConfigurationInterface $wopiConfiguration,
WopiConfigurationInterface $wopiConfiguration, DiscoveryInterface $wopiDiscovery,
WopiDiscoveryInterface $wopiDiscovery, DocumentManagerInterface $documentManager,
ResponderInterface $responder, ResponderInterface $responder,
Security $security, Security $security,
Psr17Interface $psr17, Psr17Interface $psr17,
RouterInterface $router RouterInterface $router
) { ) {
$this->storedObjectRepository = $storedObjectRepository; $this->documentManager = $documentManager;
$this->wopiConfiguration = $wopiConfiguration; $this->wopiConfiguration = $wopiConfiguration;
$this->wopiDiscovery = $wopiDiscovery; $this->wopiDiscovery = $wopiDiscovery;
$this->responder = $responder; $this->responder = $responder;
@ -58,11 +59,11 @@ final class Test
public function __invoke(string $fileId): Response public function __invoke(string $fileId): Response
{ {
$configuration = $this->wopiConfiguration->jsonSerialize(); $configuration = $this->wopiConfiguration->jsonSerialize();
/** @var StoredObject $storedObject */
$storedObject = $this->storedObjectRepository->findOneBy(['filename' => $fileId]); $storedObject = $this->documentManager->findByDocumentId($fileId);
if (null === $storedObject) { if (null === $storedObject) {
throw new NotFoundHttpException(sprintf('Unable to find object named %s', $fileId)); throw new NotFoundHttpException(sprintf('Unable to find object %s', $fileId));
} }
if ([] === $discoverExtension = $this->wopiDiscovery->discoverMimeType($storedObject->getType())) { if ([] === $discoverExtension = $this->wopiDiscovery->discoverMimeType($storedObject->getType())) {
@ -83,7 +84,7 @@ final class Test
->generate( ->generate(
'checkFileInfo', 'checkFileInfo',
[ [
'fileId' => $storedObject->getFilename(), 'fileId' => $this->documentManager->getDocumentId($storedObject),
], ],
UrlGeneratorInterface::ABSOLUTE_URL UrlGeneratorInterface::ABSOLUTE_URL
), ),

View File

@ -10,8 +10,10 @@ declare(strict_types=1);
namespace Symfony\Component\DependencyInjection\Loader\Configurator; namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface; use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
use ChampsLibres\WopiLib\Service\Contract\WopiInterface;
use Chill\WopiBundle\Service\Wopi\ChillWopi; use Chill\WopiBundle\Service\Wopi\ChillWopi;
use ChampsLibres\WopiBundle\Service\Wopi as CLWopi;
use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface;
use Chill\WopiBundle\Service\Wopi\ChillDocumentManager;
return static function (ContainerConfigurator $container) { return static function (ContainerConfigurator $container) {
$services = $container $services = $container
@ -30,8 +32,14 @@ return static function (ContainerConfigurator $container) {
->tag('controller.service_arguments'); ->tag('controller.service_arguments');
$services $services
->alias(WopiInterface::class, ChillWopi::class); ->set(ChillWopi::class)
->decorate(CLWopi::class)
->arg('$wopi', service('.inner'));
$services
->alias(DocumentManagerInterface::class, ChillDocumentManager::class);
// TODO: Move this into the async bundle (low priority)
$services $services
->alias(TempUrlGeneratorInterface::class, 'async_uploader.temp_url_generator'); ->alias(TempUrlGeneratorInterface::class, 'async_uploader.temp_url_generator');
}; };

View File

@ -0,0 +1,244 @@
<?php
/**
* 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\WopiBundle\Service\Wopi;
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface;
use ChampsLibres\WopiLib\Contract\Entity\Document;
use ChampsLibres\WopiLib\Contract\Service\DocumentLockManagerInterface;
use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
use DateTimeInterface;
use Doctrine\ORM\EntityManagerInterface;
use Error;
use loophp\psr17\Psr17Interface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Ramsey\Uuid\Uuid;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Mime\MimeTypes;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;
final class ChillDocumentManager implements DocumentManagerInterface
{
private DocumentLockManagerInterface $documentLockManager;
private EntityManagerInterface $entityManager;
private HttpClientInterface $httpClient;
private Psr17Interface $psr17;
private RequestInterface $request;
private StoredObjectRepository $storedObjectRepository;
private TempUrlGeneratorInterface $tempUrlGenerator;
public function __construct(
DocumentLockManagerInterface $documentLockManager,
EntityManagerInterface $entityManager,
HttpClientInterface $httpClient,
Psr17Interface $psr17,
StoredObjectRepository $storedObjectRepository,
TempUrlGeneratorInterface $tempUrlGenerator,
HttpMessageFactoryInterface $httpMessageFactory,
RequestStack $requestStack
) {
$this->entityManager = $entityManager;
$this->psr17 = $psr17;
$this->storedObjectRepository = $storedObjectRepository;
$this->documentLockManager = $documentLockManager;
$this->tempUrlGenerator = $tempUrlGenerator;
$this->httpClient = $httpClient;
$this->request = $httpMessageFactory->createRequest($requestStack->getCurrentRequest());
}
public function create(array $data): Document
{
/** @var StoredObject $document */
$document = (new ObjectNormalizer())->denormalize([], StoredObject::class);
// Mime types / extension handling.
$mimeTypes = new MimeTypes();
$mimeTypes->getMimeTypes($data['extension']);
$document->setType(reset($mimeTypes));
$document->setFilename($data['name']);
$this->entityManager->persist($document);
$this->entityManager->flush($document);
// TODO : Ask proper mapping.
// Available: basename, name, extension, content, size
$this->setContent($document, $data['content']);
return $document;
}
public function deleteLock(Document $document): void {
$this->documentLockManager->deleteLock($document, $this->request);
}
/**
* @param string $documentFilename without extension !
*/
public function findByDocumentFilename(string $documentFilename): ?Document {
return $this->storedObjectRepository->findOneBy(
[
'filename' => $documentFilename,
]
);
}
public function findByDocumentId(string $documentId): ?Document {
return $this->storedObjectRepository->findOneBy(
[
'uuid' => Uuid::fromString($documentId),
]
);
}
/**
* @param StoredObject $document
*/
public function getCreationDate(Document $document): DateTimeInterface
{
return $document->getCreationDate();
}
/**
* @param StoredObject $document
*/
public function getLastModifiedDate(Document $document): DateTimeInterface
{
// TODO: Add column 'LastModifiedDate' in StoredObject entity
return $document->getCreationDate();
}
public function getLock(Document $document): string {
return $this->documentLockManager->getLock($document, $this->request);
}
public function getVersion(Document $document): string {
// TODO ?
return '0';
}
public function hasLock(Document $document): bool {
return $this->documentLockManager->hasLock($document, $this->request);
}
public function lock(Document $document, string $lock): void {
$this->documentLockManager->setLock($document, $lock, $this->request);
}
public function remove(Document $document): void {
$entityIsDeleted = false;
try {
$this->entityManager->remove($document);
$entityIsDeleted = true;
} catch (Throwable $e) {
$entityIsDeleted = false;
}
if ($entityIsDeleted === true) {
$this->deleteContent($document);
}
}
public function write(Document $document, array $properties = []): void
{
$this->setContent($document, $properties['content']);
}
/**
* @param StoredObject $document
*
* @return string The document filename with its extension.
*/
public function getBasename(Document $document): string {
$exts = (new MimeTypes())->getExtensions($document->getType());
if ([] === $exts) {
throw new Error('Unknown mimetype for stored document.');
}
return sprintf('%s.%s', $document->getFilename(), reset($exts));
}
/**
* @param StoredObject $document
*/
public function getDocumentId(Document $document): string {
return (string) $document->getUuid();
}
public function getSha256(Document $document): string {
return base64_encode(hash('sha256', $this->getContent($document)));
}
public function getSize(Document $document): int {
return strlen($this->getContent());
}
public function read(Document $document): StreamInterface {
return $this
->psr17
->createStream($this->getContent($document));
}
private function deleteContent(StoredObject $storedObject): void
{
/** @var StdClass $object */
$object = $this->tempUrlGenerator->generate('DELETE', $storedObject->getFilename());
$response = $this->httpClient->request('DELETE', $object->url);
if (200 !== $response->getStatusCode())
{
throw new Error('Unable to delete stored object.');
}
}
private function getContent(StoredObject $storedObject): string
{
/** @var StdClass $object */
$object = $this->tempUrlGenerator->generate('GET', $storedObject->getFilename());
$response = $this->httpClient->request('GET', $object->url);
if (200 !== $response->getStatusCode())
{
throw new Error('Unable to retrieve stored object.');
}
return $response->getContent();
}
private function setContent(StoredObject $storedObject, string $content): void
{
// TODO: Add strict typing in champs-libres/async-uploader-bundle
/** @var StdClass $object */
$object = $this->tempUrlGenerator->generate('PUT', $storedObject->getFilename());
$response = $this->httpClient->request('PUT', $object->url, ['body' => $content]);
if (201 !== $response->getStatusCode())
{
throw new Error('Unable to save stored object.');
}
}
}

View File

@ -9,13 +9,10 @@ declare(strict_types=1);
namespace Chill\WopiBundle\Service\Wopi; namespace Chill\WopiBundle\Service\Wopi;
use ChampsLibres\AsyncUploaderBundle\TempUrl\TempUrlGeneratorInterface; use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface;
use ChampsLibres\WopiLib\Discovery\WopiDiscoveryInterface; use ChampsLibres\WopiLib\Contract\Service\WopiInterface;
use ChampsLibres\WopiLib\Service\Contract\WopiInterface;
use Chill\DocStoreBundle\Repository\StoredObjectRepository;
use Exception;
use loophp\psr17\Psr17Interface; use loophp\psr17\Psr17Interface;
use Psr\Http\Client\ClientInterface; use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
@ -23,32 +20,28 @@ use Symfony\Component\Security\Core\User\UserProviderInterface;
final class ChillWopi implements WopiInterface final class ChillWopi implements WopiInterface
{ {
private CacheItemPoolInterface $cache;
private DocumentManagerInterface $documentManager;
private Psr17Interface $psr17; private Psr17Interface $psr17;
private WopiDiscoveryInterface $wopiDiscovery;
private StoredObjectRepository $storedObjectRepository;
private ClientInterface $httpClient;
private TempUrlGeneratorInterface $tempUrlGenerator;
private UserProviderInterface $userProvider; private UserProviderInterface $userProvider;
private WopiInterface $wopi;
public function __construct( public function __construct(
CacheItemPoolInterface $cache,
DocumentManagerInterface $documentManager,
Psr17Interface $psr17, Psr17Interface $psr17,
WopiDiscoveryInterface $wopiDiscovery, UserProviderInterface $userProvider,
StoredObjectRepository $storedObjectRepository, WopiInterface $wopi
ClientInterface $httpClient,
TempUrlGeneratorInterface $tempUrlGenerator,
UserProviderInterface $userProvider
) { ) {
$this->cache = $cache;
$this->documentManager = $documentManager;
$this->psr17 = $psr17; $this->psr17 = $psr17;
$this->wopiDiscovery = $wopiDiscovery;
$this->storedObjectRepository = $storedObjectRepository;
$this->httpClient = $httpClient;
$this->tempUrlGenerator = $tempUrlGenerator;
$this->userProvider = $userProvider; $this->userProvider = $userProvider;
$this->wopi = $wopi;
} }
public function checkFileInfo( public function checkFileInfo(
@ -64,17 +57,10 @@ final class ChillWopi implements WopiInterface
->createResponse(401); ->createResponse(401);
} }
$storedObject = $this->storedObjectRepository->findOneBy(['filename' => $fileId]); // @ TODO : Replace this with a call to decorated object once authentication is done.
$document = $this->documentManager->findByDocumentId($fileId);
if (null === $storedObject) { $userIdentifier = $user->getUsername();
throw new Exception(sprintf('Unable to find object named %s', $fileId)); $userCacheKey = sprintf('wopi_putUserInfo_%s', $userIdentifier);
}
$mimeType = $storedObject->getType();
if ([] === $this->wopiDiscovery->discoverMimeType($mimeType)) {
throw new Exception(sprintf('Unable to find mime type %s', $mimeType));
}
return $this return $this
->psr17 ->psr17
@ -82,33 +68,38 @@ final class ChillWopi implements WopiInterface
->withHeader('Content-Type', 'application/json') ->withHeader('Content-Type', 'application/json')
->withBody($this->psr17->createStream((string) json_encode( ->withBody($this->psr17->createStream((string) json_encode(
[ [
'BaseFileName' => $storedObject->getFilename(), 'BaseFileName' => $this->documentManager->getBasename($document),
'OwnerId' => uniqid(), 'OwnerId' => 'Symfony',
'Size' => 0, 'Size' => $this->documentManager->getSize($document),
'UserId' => uniqid(), 'UserId' => $userIdentifier,
// 'Version' => 'v' . uniqid(),
'ReadOnly' => false, 'ReadOnly' => false,
'UserCanAttend' => true,
'UserCanPresent' => true,
'UserCanRename' => true,
'UserCanWrite' => true, 'UserCanWrite' => true,
'UserCanNotWriteRelative' => true, 'UserCanNotWriteRelative' => false,
'SupportsLocks' => false, 'SupportsUserInfo' => true,
'UserFriendlyName' => sprintf('User %s', $user->getUsername()), 'SupportsDeleteFile' => true,
'UserExtraInfo' => [], 'SupportsLocks' => true,
'LastModifiedTime' => date('Y-m-d\TH:i:s.u\Z', $storedObject->getCreationDate()->getTimestamp()), 'SupportsGetLock' => true,
'CloseButtonClosesWindow' => true, 'SupportsExtendedLockLength' => true,
'EnableInsertRemoteImage' => true, 'UserFriendlyName' => $userIdentifier,
'EnableShare' => false,
'SupportsUpdate' => true, 'SupportsUpdate' => true,
'SupportsRename' => false, 'SupportsRename' => true,
'DisablePrint' => false, 'DisablePrint' => false,
'DisableExport' => false, 'AllowExternalMarketplace' => true,
'DisableCopy' => false, 'SupportedShareUrlTypes' => [
'ReadOnly',
],
'SHA256' => $this->documentManager->getSha256($document),
'UserInfo' => (string) $this->cache->getItem($userCacheKey)->get(),
] ]
))); )));
} }
public function deleteFile(string $fileId, ?string $accessToken, RequestInterface $request): ResponseInterface public function deleteFile(string $fileId, ?string $accessToken, RequestInterface $request): ResponseInterface
{ {
return $this->getDebugResponse(__FUNCTION__, $request); return $this->wopi->deleteFile($fileId, $accessToken, $request);
} }
public function enumerateAncestors( public function enumerateAncestors(
@ -116,57 +107,17 @@ final class ChillWopi implements WopiInterface
?string $accessToken, ?string $accessToken,
RequestInterface $request RequestInterface $request
): ResponseInterface { ): ResponseInterface {
return $this->getDebugResponse(__FUNCTION__, $request); return $this->wopi->enumerateAncestors($fileId, $accessToken, $request);
} }
public function getFile(string $fileId, ?string $accessToken, RequestInterface $request): ResponseInterface public function getFile(string $fileId, ?string $accessToken, RequestInterface $request): ResponseInterface
{ {
try { return $this->wopi->getFile($fileId, $accessToken, $request);
$user = $this->userProvider->loadUserByUsername($accessToken);
} catch (UsernameNotFoundException $e) {
return $this
->psr17
->createResponse(401);
}
$storedObject = $this->storedObjectRepository->findOneBy(['filename' => $fileId]);
if (null === $storedObject) {
return $this
->psr17
->createResponse(404);
}
// TODO: Add strict typing in champs-libres/async-uploader-bundle
/** @var StdClass $object */
$object = $this->tempUrlGenerator->generate('GET', $storedObject->getFilename());
$response = $this->httpClient->sendRequest($this->psr17->createRequest('GET', $object->url));
if (200 !== $response->getStatusCode())
{
return $this
->psr17
->createResponse(500);
}
return $this
->psr17
->createResponse()
->withHeader(
'Content-Type',
'application/octet-stream',
)
->withHeader(
'Content-Disposition',
sprintf('attachment; filename=%s', $storedObject->getFilename())
)
->withBody($response->getBody());
} }
public function getLock(string $fileId, ?string $accessToken, RequestInterface $request): ResponseInterface public function getLock(string $fileId, ?string $accessToken, RequestInterface $request): ResponseInterface
{ {
return $this->getDebugResponse(__FUNCTION__, $request); return $this->wopi->getLock($fileId, $accessToken, $request);
} }
public function getShareUrl( public function getShareUrl(
@ -174,7 +125,7 @@ final class ChillWopi implements WopiInterface
?string $accessToken, ?string $accessToken,
RequestInterface $request RequestInterface $request
): ResponseInterface { ): ResponseInterface {
return $this->getDebugResponse(__FUNCTION__, $request); return $this->wopi->getShareUrl($fileId, $accessToken, $request);
} }
public function lock( public function lock(
@ -183,7 +134,7 @@ final class ChillWopi implements WopiInterface
string $xWopiLock, string $xWopiLock,
RequestInterface $request RequestInterface $request
): ResponseInterface { ): ResponseInterface {
return $this->getDebugResponse(__FUNCTION__, $request); return $this->wopi->lock($fileId, $accessToken, $xWopiLock, $request);
} }
public function putFile( public function putFile(
@ -193,49 +144,17 @@ final class ChillWopi implements WopiInterface
string $xWopiEditors, string $xWopiEditors,
RequestInterface $request RequestInterface $request
): ResponseInterface { ): ResponseInterface {
try { return $this->wopi->putFile($fileId, $accessToken, $xWopiLock, $xWopiEditors, $request);
$user = $this->userProvider->loadUserByUsername($accessToken);
} catch (UsernameNotFoundException $e) {
return $this
->psr17
->createResponse(401);
}
$storedObject = $this->storedObjectRepository->findOneBy(['filename' => $fileId]);
if (null === $storedObject) {
throw new Exception(sprintf('Unable to find object named %s', $fileId));
}
// TODO: Add strict typing in champs-libres/async-uploader-bundle
/** @var StdClass $object */
$object = $this->tempUrlGenerator->generate('PUT', $storedObject->getFilename());
$response = $this->httpClient->sendRequest($this->psr17->createRequest('PUT', $object->url)->withBody($request->getBody()));
if (201 !== $response->getStatusCode())
{
return $this
->psr17
->createResponse(500);
}
return $this
->psr17
->createResponse()
->withHeader('Content-Type', 'application/json')
->withAddedHeader('X-WOPI-Lock', $xWopiLock)
->withBody($this->psr17->createStream((string) json_encode([])));
} }
public function putRelativeFile(string $fileId, string $accessToken, ?string $suggestedTarget, ?string $relativeTarget, bool $overwriteRelativeTarget, int $size, RequestInterface $request): ResponseInterface public function putRelativeFile(string $fileId, string $accessToken, ?string $suggestedTarget, ?string $relativeTarget, bool $overwriteRelativeTarget, int $size, RequestInterface $request): ResponseInterface
{ {
return $this->getDebugResponse(__FUNCTION__, $request); return $this->wopi->putRelativeFile($fileId, $accessToken, $suggestedTarget, $relativeTarget, $overwriteRelativeTarget, $size, $request);
} }
public function putUserInfo(string $fileId, ?string $accessToken, RequestInterface $request): ResponseInterface public function putUserInfo(string $fileId, ?string $accessToken, RequestInterface $request): ResponseInterface
{ {
return $this->getDebugResponse(__FUNCTION__, $request); return $this->wopi->putUserInfo($fileId, $accessToken, $request);
} }
public function refreshLock( public function refreshLock(
@ -244,7 +163,7 @@ final class ChillWopi implements WopiInterface
string $xWopiLock, string $xWopiLock,
RequestInterface $request RequestInterface $request
): ResponseInterface { ): ResponseInterface {
return $this->getDebugResponse(__FUNCTION__, $request); return $this->wopi->refreshLock($fileId, $accessToken, $xWopiLock, $request);
} }
public function renameFile( public function renameFile(
@ -254,7 +173,7 @@ final class ChillWopi implements WopiInterface
string $xWopiRequestedName, string $xWopiRequestedName,
RequestInterface $request RequestInterface $request
): ResponseInterface { ): ResponseInterface {
return $this->getDebugResponse(__FUNCTION__, $request); return $this->wopi->renameFile($fileId, $accessToken, $xWopiLock, $xWopiRequestedName, $request);
} }
public function unlock( public function unlock(
@ -263,7 +182,7 @@ final class ChillWopi implements WopiInterface
string $xWopiLock, string $xWopiLock,
RequestInterface $request RequestInterface $request
): ResponseInterface { ): ResponseInterface {
return $this->getDebugResponse(__FUNCTION__, $request); return $this->wopi->unlock($fileId, $accessToken, $xWopiLock, $request);
} }
public function unlockAndRelock( public function unlockAndRelock(
@ -273,33 +192,6 @@ final class ChillWopi implements WopiInterface
string $xWopiOldLock, string $xWopiOldLock,
RequestInterface $request RequestInterface $request
): ResponseInterface { ): ResponseInterface {
return $this->getDebugResponse(__FUNCTION__, $request); return $this->wopi->unlockAndRelock($fileId, $accessToken, $xWopiLock, $xWopiOldLock, $request);
}
private function getDebugResponse(string $method, RequestInterface $request): ResponseInterface
{
$params = [];
parse_str($request->getUri()->getQuery(), $params);
$data = (string) json_encode(array_merge(
['method' => $method],
$params,
$request->getHeaders()
));
return $this
->psr17
->createResponse()
->withHeader('content', 'application/json')
->withBody($this->psr17->createStream($data));
}
private function getLockFilepath(string $fileId): string
{
return sprintf(
'%s/%s.lock',
$this->filesRepository,
$fileId
);
} }
} }