Add feature to set concerned persons in a ticket

This commit adds the functionality to set and change the concerned persons in a ticket within the ChillTicketBundle. New vuejs components, serializers, and store modules have been introduced to achieve this. Moreover, necessary changes have been made in existing components and store index to support this functionality.
This commit is contained in:
Julien Fastré 2024-06-03 22:30:12 +02:00
parent 631f047338
commit 166a6fde20
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
8 changed files with 254 additions and 3 deletions

View File

@ -44,6 +44,8 @@ final readonly class SetPersonsController
$command = $this->serializer->deserialize($request->getContent(), SetPersonsCommand::class, 'json', [AbstractNormalizer::GROUPS => ['read']]);
dump($command);
return $this->registerSetPersons($command, $ticket);
}

View File

@ -8,7 +8,7 @@
</label>
</div>
<form @submit.prevent="submitAction">
<form v-if="activeTab !== 'set_persons'" @submit.prevent="submitAction">
<add-comment-component
v-model="content"
v-if="activeTab === 'add_comment'"
@ -37,12 +37,15 @@
</button>
</li>
<li>
<button class="btn btn-create" type="submit">
<button class="btn btn-save" type="submit">
{{ $t("ticket.save") }}
</button>
</li>
</ul>
</form>
<template v-else>
<persons-selector-component @closeRequested="closeAllActions()" />
</template>
</div>
</div>
<div class="footer-ticket-main">
@ -107,6 +110,24 @@
{{ $t("add_addressee.title") }}
</button>
</li>
<li class="nav-item p-2">
<button
type="button"
:class="`btn ${
activeTab === 'set_persons'
? 'btn-primary'
: 'btn-light'
}`"
@click="
activeTab === 'set_persons'
? (activeTab = '')
: (activeTab = 'set_persons')
"
>
<i :class="actionIcons['set_persons']"></i>
Patients concernés
</button>
</li>
<li class="nav-item p-2">
<button
@ -140,10 +161,12 @@ import { Comment, Motive, Ticket } from "../../../types";
import MotiveSelectorComponent from "./MotiveSelectorComponent.vue";
import AddresseeSelectorComponent from "./AddresseeSelectorComponent.vue";
import AddCommentComponent from "./AddCommentComponent.vue";
import PersonsSelectorComponent from "./PersonsSelectorComponent.vue";
export default defineComponent({
name: "ActionToolbarComponent",
components: {
PersonsSelectorComponent,
AddCommentComponent,
MotiveSelectorComponent,
AddresseeSelectorComponent,
@ -153,7 +176,7 @@ export default defineComponent({
const { t } = useI18n();
const toast = inject("toast") as any;
const activeTab = ref(
"" as "" | "add_comment" | "set_motive" | "add_addressee"
"" as "" | "add_comment" | "set_motive" | "add_addressee" | "set_persons"
);
const ticket = computed(() => store.getters.getTicket as Ticket);
@ -239,6 +262,9 @@ export default defineComponent({
alert("Sera disponible plus tard");
}
const closeAllActions = function() {
activeTab.value = "";
}
return {
actionIcons: ref(store.getters.getActionIcons),
@ -254,6 +280,7 @@ export default defineComponent({
handleClick,
hasReturnPath,
returnPath,
closeAllActions,
};
},
});

View File

@ -0,0 +1,131 @@
<script setup lang="ts">
import {useStore} from "vuex";
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
import {computed, inject, reactive} from "vue";
import {Ticket} from "../../../types";
import {Person} from "../../../../../../../ChillPersonBundle/Resources/public/types"
import OnTheFly from "ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue";
import {ToastPluginApi} from "vue-toast-notification";
const emit = defineEmits<{
(e: 'closeRequested'): void
}>()
const store = useStore();
const toast = inject("toast") as ToastPluginApi;
const ticket = computed<Ticket>(() => store.getters.getTicket);
const persons = computed(() => ticket.value.currentPersons);
const addPersonsOptions = {
uniq: false,
type: ['person'],
priority: null,
button: {
class: 'btn-submit',
},
};
const added: Person[] = reactive([]);
const removed: Person[] = reactive([]);
const computeCurrentPersons = (initial: Person[], added: Person[], removed: Person[]): Person[] => {
for (let p of added) {
if (initial.findIndex((element) => element.id === p.id) === -1) {
initial.push(p);
}
}
return initial.filter((p) => removed.findIndex((element) => element.id === p.id) === -1);
}
const currentPersons = computed((): Person[] => {
return computeCurrentPersons(persons.value, added, removed);
})
const removePerson = (p: Person) => {
removed.push(p);
}
const addNewEntity = (n: {modal: {showModal: boolean}, selected: Array<{result: Person}>}) => {
n.modal.showModal = false;
for (let p of n.selected) {
added.push(p.result);
}
}
const save = async function(): Promise<void> {
try {
await store.dispatch("setPersons", { persons: computeCurrentPersons(persons.value, added, removed) });
toast.success("Patients concernés sauvegardés");
} catch (e: any) {
console.error("error while saving", e);
toast.error((e as Error).message);
return Promise.resolve();
}
emit("closeRequested");
}
</script>
<template>
<div>
<ul v-if="currentPersons.length > 0" class="person-list">
<li v-for="person in currentPersons" :key="person.id">
<on-the-fly :type="person.type" :id="person.id" :buttonText="person.textAge" :displayBadge="'true' === 'true'" action="show"></on-the-fly>
<button type="button" class="btn btn-delete remove-person" @click="removePerson(person)"></button>
</li>
</ul>
<p v-else class="chill-no-data-statement">Aucun patient</p>
</div>
<ul class="record_actions">
<li class="cancel">
<button
class="btn btn-cancel"
type="button"
@click="emit('closeRequested')"
>
{{ $t("ticket.cancel") }}
</button>
</li>
<li>
<add-persons
:options="addPersonsOptions"
key="add-person-ticket"
buttonTitle="set_persons.user_label"
modalTitle="set_persons.user_label"
ref="addPersons"
@addNewPersons="addNewEntity"
/>
</li>
<li>
<button class="btn btn-save" type="submit" @click.prevent="save">
{{ $t("ticket.save") }}
</button>
</li>
</ul>
</template>
<style scoped lang="scss">
ul.person-list {
list-style-type: none;
& > li {
display: inline-block;
border: 1px solid transparent;
border-radius: 6px;
button.remove-person {
opacity: 10%;
}
}
& > li:hover {
border: 1px solid white;
button.remove-person {
opacity: 100%;
}
}
}
</style>

View File

@ -33,6 +33,10 @@ const messages = {
success: "Attribution effectuée",
error: "Aucun destinataire sélectionné",
},
set_persons: {
title: "Patients concernés",
user_label: "Ajouter un patient",
},
banner: {
concerned_patient: "Patient concerné",
speaker: "Attribué à",

View File

@ -3,12 +3,14 @@ import { State as MotiveStates, moduleMotive } from "./modules/motive";
import { State as TicketStates, moduleTicket } from "./modules/ticket";
import { State as CommentStates, moduleComment } from "./modules/comment";
import { State as AddresseeStates, moduleAddressee } from "./modules/addressee";
import { State as PersonsState, modulePersons} from "./modules/persons";
export type RootState = {
motive: MotiveStates;
ticket: TicketStates;
comment: CommentStates;
addressee: AddresseeStates;
persons: PersonsState;
};
export const store = createStore<RootState>({
@ -17,5 +19,6 @@ export const store = createStore<RootState>({
ticket: moduleTicket,
comment: moduleComment,
addressee: moduleAddressee,
persons: modulePersons,
},
});

View File

@ -0,0 +1,32 @@
import {
makeFetch,
} from "../../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
import { Person } from "../../../../../../../../ChillPersonBundle/Resources/public/types";
import { Module } from "vuex";
import { RootState } from "..";
import {Ticket} from "../../../../types";
export interface State {};
export const modulePersons: Module<State, RootState> = {
actions: {
async setPersons(
{ commit, rootState: RootState },
payload: { persons: Person[] }
) {
const persons = payload.persons.map((person: Person) => ({id: person.id, type: person.type}));
try {
const result: Ticket = await makeFetch(
"POST",
`/api/1.0/ticket/${RootState.ticket.ticket.id}/persons/set`,
{ persons },
);
commit("setTicket", result);
return Promise.resolve();
} catch (e: any) {
throw e.name;
}
},
}
}

View File

@ -17,6 +17,7 @@ export const moduleTicket: Module<State, RootState> = {
set_motive: "fa fa-paint-brush",
//add_addressee: "fa fa-paper-plane",
addressees_state: "fa fa-paper-plane",
set_persons: "fa fa-eyedropper",
},
toto: "toto",
}),

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Serializer\Normalizer;
use Chill\PersonBundle\Entity\Person;
use Chill\TicketBundle\Action\Ticket\SetPersonsCommand;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
class SetPersonsCommandDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
public function denormalize($data, string $type, ?string $format = null, array $context = [])
{
if (null === $data) {
return null;
}
if (!array_key_exists('persons', $data)) {
throw new UnexpectedValueException("key 'persons' does exists");
}
if (!is_array($data['persons'])) {
throw new UnexpectedValueException("key 'persons' must be an array");
}
$persons = [];
foreach ($data['persons'] as $person) {
$persons[] = $this->denormalizer->denormalize($person, Person::class, $format, $context);
}
return new SetPersonsCommand($persons);
}
public function supportsDenormalization($data, string $type, ?string $format = null)
{
return SetPersonsCommand::class === $type;
}
}