refactor(activity): replace AddPersons with PickEntity and decouple ConcernedGroups from store

- migrate `ConcernedGroups` to use `PickEntity` instead of `AddPersons`
- convert `ConcernedGroups` into a controlled component (`modelValue` + `update:modelValue`)
- move Vuex synchronization to parent containers (`Activity/App.vue`, `Calendar/App.vue`)
- remove direct Vuex usage from `PersonsBloc` via callback prop
- replace local `BaseEntity` with shared `Entities` types
- fix TS implicit-any in participations callback typing
This commit is contained in:
Boris Waaub
2026-04-14 14:56:32 +02:00
parent cca70e3e52
commit 08f0633543
4 changed files with 422 additions and 229 deletions

View File

@@ -1,5 +1,11 @@
<template>
<concerned-groups v-if="hasPerson" />
<concerned-groups
v-if="hasPerson"
:model-value="concernedGroupsModel"
:suggested="suggestedEntities"
:accompanying-course="activity.accompanyingPeriod"
@update:model-value="updateConcernedGroups"
/>
<social-issues-acc v-if="hasSocialIssues" />
<location v-if="hasLocation" />
</template>
@@ -8,6 +14,7 @@
import ConcernedGroups from "./components/ConcernedGroups.vue";
import SocialIssuesAcc from "./components/SocialIssuesAcc.vue";
import Location from "./components/Location.vue";
import { mapState } from "vuex";
export default {
name: "App",
@@ -17,5 +24,46 @@ export default {
SocialIssuesAcc,
Location,
},
computed: {
...mapState(["activity"]),
concernedGroupsModel() {
return {
persons: this.activity.persons,
thirdParties: this.activity.thirdParties,
users: this.activity.users,
};
},
suggestedEntities() {
return this.$store.getters.suggestedEntities || [];
},
},
methods: {
sameEntity(left, right) {
return left.id === right.id && left.type === right.type;
},
syncConcernedGroupType(currentList, nextList) {
nextList.forEach((nextEntity) => {
if (
!currentList.some((current) => this.sameEntity(current, nextEntity))
) {
this.$store.dispatch("addPersonsInvolved", { result: nextEntity });
}
});
currentList.forEach((currentEntity) => {
if (!nextList.some((next) => this.sameEntity(next, currentEntity))) {
this.$store.dispatch("removePersonInvolved", currentEntity);
}
});
},
updateConcernedGroups(value) {
this.syncConcernedGroupType(this.activity.persons, value.persons);
this.syncConcernedGroupType(
this.activity.thirdParties,
value.thirdParties,
);
this.syncConcernedGroupType(this.activity.users, value.users);
},
},
};
</script>

View File

@@ -7,44 +7,35 @@
:bloc="bloc"
:bloc-width="getBlocWidth"
:set-persons-in-bloc="setPersonsInBloc"
:remove-person-involved="removePersonInvolved"
/>
</div>
<div
v-if="getContext === 'accompanyingCourse' && suggestedEntities.length > 0"
>
<ul class="list-suggest add-items inline">
<li
v-for="(p, i) in suggestedEntities"
@click="addSuggestedEntity(p)"
:key="`suggestedEntities-${i}`"
>
<person-text v-if="p.type === 'person'" :person="p" />
<span v-else>{{ p.text }}</span>
</li>
</ul>
</div>
<ul class="record_actions">
<li class="add-persons">
<add-persons
:buttonTitle="trans(ACTIVITY_ADD_PERSONS)"
:modalTitle="trans(ACTIVITY_ADD_PERSONS)"
v-bind:key="addPersons.key"
v-bind:options="addPersonsOptions"
@addNewPersons="addNewPersons"
ref="addPersons"
>
</add-persons>
<pick-entity
:uniqid="pickEntityUniqId"
:types="pickEntityTypes"
:picked="pickedEntities"
:multiple="true"
:removable-if-set="false"
:display-picked="false"
:suggested="suggestedEntities"
:label="trans(ACTIVITY_ADD_PERSONS)"
@add-new-entity="addNewEntityFromPicker"
@add-new-entity-process-ended="setPersonsInBloc"
/>
</li>
</ul>
</teleport>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import PickEntity from "ChillMainAssets/vuejs/PickEntity/PickEntity.vue";
import PersonsBloc from "./ConcernedGroups/PersonsBloc.vue";
import PersonText from "ChillPersonAssets/vuejs/_components/Entity/PersonText.vue";
import { Entities, EntitiesOrMe, EntityType } from "ChillPersonAssets/types";
import {
ACTIVITY_BLOC_PERSONS,
ACTIVITY_BLOC_PERSONS_ASSOCIATED,
@@ -54,201 +45,316 @@ import {
trans,
} from "translator";
export default {
name: "ConcernedGroups",
components: {
AddPersons,
PersonsBloc,
PersonText,
},
setup() {
return {
trans,
ACTIVITY_ADD_PERSONS,
};
},
data() {
return {
personsBlocs: [
{
key: "persons",
title: trans(ACTIVITY_BLOC_PERSONS),
persons: [],
included: false,
},
{
key: "personsAssociated",
title: trans(ACTIVITY_BLOC_PERSONS_ASSOCIATED),
persons: [],
included: window.activity
? window.activity.activityType.personsVisible !== 0
: true,
},
{
key: "personsNotAssociated",
title: "activity.bloc_persons_not_associated",
persons: [],
included: window.activity
? window.activity.activityType.personsVisible !== 0
: true,
},
{
key: "thirdparty",
title: trans(ACTIVITY_BLOC_THIRDPARTY),
persons: [],
included: window.activity
? window.activity.activityType.thirdPartiesVisible !== 0
: true,
},
{
key: "users",
title: trans(ACTIVITY_BLOC_USERS),
persons: [],
included: window.activity
? window.activity.activityType.usersVisible !== 0
: true,
},
],
addPersons: {
key: "activity",
},
};
},
computed: {
isComponentVisible() {
return window.activity
? window.activity.activityType.personsVisible !== 0 ||
window.activity.activityType.thirdPartiesVisible !== 0 ||
window.activity.activityType.usersVisible !== 0
: true;
},
...mapState({
persons: (state) => state.activity.persons,
thirdParties: (state) => state.activity.thirdParties,
users: (state) => state.activity.users,
accompanyingCourse: (state) => state.activity.accompanyingPeriod,
type BlocKey =
| "persons"
| "personsAssociated"
| "personsNotAssociated"
| "thirdparty"
| "users";
interface Participation {
endDate: string | null;
person: Entities;
}
interface AccompanyingCourse {
participations: Participation[];
}
interface ConcernedGroupsModel {
persons: Entities[];
thirdParties: Entities[];
users: Entities[];
}
interface PersonBloc {
key: BlocKey;
title: string;
persons: Entities[];
included: boolean;
}
interface WindowActivity {
activityType: {
personsVisible: number;
thirdPartiesVisible: number;
usersVisible: number;
};
}
const props = withDefaults(
defineProps<{
modelValue?: ConcernedGroupsModel;
suggested?: Entities[];
accompanyingCourse?: AccompanyingCourse | null;
pickEntityUniqId?: string;
}>(),
{
modelValue: () => ({
persons: [],
thirdParties: [],
users: [],
}),
...mapGetters(["suggestedEntities"]),
getContext() {
return this.accompanyingCourse ? "accompanyingCourse" : "person";
},
contextPersonsBlocs() {
return this.personsBlocs.filter((bloc) => bloc.included !== false);
},
addPersonsOptions() {
let optionsType = [];
if (window.activity) {
if (window.activity.activityType.personsVisible !== 0) {
optionsType.push("person");
}
if (window.activity.activityType.thirdPartiesVisible !== 0) {
optionsType.push("thirdparty");
}
if (window.activity.activityType.usersVisible !== 0) {
optionsType.push("user");
}
} else {
optionsType = ["person", "thirdparty", "user"];
}
return {
type: optionsType,
priority: null,
uniq: false,
button: {
size: "btn-sm",
},
};
},
getBlocWidth() {
return Math.round(100 / this.contextPersonsBlocs.length) + "%";
},
},
mounted() {
this.setPersonsInBloc();
},
methods: {
setPersonsInBloc() {
let groups;
if (this.accompanyingCourse) {
groups = this.splitPersonsInGroups();
}
this.personsBlocs.forEach((bloc) => {
if (this.accompanyingCourse) {
switch (bloc.key) {
case "personsAssociated":
bloc.persons = groups.personsAssociated;
bloc.included = true;
break;
case "personsNotAssociated":
bloc.persons = groups.personsNotAssociated;
bloc.included = true;
break;
}
} else {
switch (bloc.key) {
case "persons":
bloc.persons = this.persons;
bloc.included = true;
break;
}
}
switch (bloc.key) {
case "thirdparty":
bloc.persons = this.thirdParties;
break;
case "users":
bloc.persons = this.users;
break;
}
}, groups);
},
splitPersonsInGroups() {
let personsAssociated = [];
let personsNotAssociated = this.persons;
let participations = this.getCourseParticipations();
this.persons.forEach((person) => {
participations.forEach((participation) => {
if (person.id === participation.id) {
//console.log(person.id);
personsAssociated.push(person);
personsNotAssociated = personsNotAssociated.filter(
(p) => p !== person,
);
}
});
});
return {
personsAssociated: personsAssociated,
personsNotAssociated: personsNotAssociated,
};
},
getCourseParticipations() {
let participations = [];
this.accompanyingCourse.participations.forEach((participation) => {
if (!participation.endDate) {
participations.push(participation.person);
}
});
return participations;
},
addNewPersons({ selected, modal }) {
console.log("@@@ CLICK button addNewPersons", selected);
selected.forEach((item) => {
this.$store.dispatch("addPersonsInvolved", item);
}, this);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
this.setPersonsInBloc();
},
addSuggestedEntity(person) {
this.$store.dispatch("addPersonsInvolved", {
result: person,
type: "person",
});
this.setPersonsInBloc();
},
suggested: () => [],
accompanyingCourse: null,
pickEntityUniqId: "activity",
},
);
const emit =
defineEmits<(e: "update:modelValue", value: ConcernedGroupsModel) => void>();
const getWindowActivity = (): WindowActivity | undefined => {
return (window as Window & { activity?: WindowActivity }).activity;
};
const hasPersonsVisible = (): boolean => {
const activity = getWindowActivity();
return activity ? activity.activityType.personsVisible !== 0 : true;
};
const windowActivity = getWindowActivity();
const personsBlocs = ref<PersonBloc[]>([
{
key: "persons",
title: trans(ACTIVITY_BLOC_PERSONS),
persons: [],
included: false,
},
{
key: "personsAssociated",
title: trans(ACTIVITY_BLOC_PERSONS_ASSOCIATED),
persons: [],
included: hasPersonsVisible(),
},
{
key: "personsNotAssociated",
title: "activity.bloc_persons_not_associated",
persons: [],
included: hasPersonsVisible(),
},
{
key: "thirdparty",
title: trans(ACTIVITY_BLOC_THIRDPARTY),
persons: [],
included: windowActivity
? windowActivity.activityType.thirdPartiesVisible !== 0
: true,
},
{
key: "users",
title: trans(ACTIVITY_BLOC_USERS),
persons: [],
included: windowActivity
? windowActivity.activityType.usersVisible !== 0
: true,
},
]);
const selectedEntities = ref<ConcernedGroupsModel>({
persons: [],
thirdParties: [],
users: [],
});
watch(
() => props.modelValue,
(value) => {
selectedEntities.value = {
persons: [...(value?.persons ?? [])],
thirdParties: [...(value?.thirdParties ?? [])],
users: [...(value?.users ?? [])],
};
},
{ immediate: true, deep: true },
);
const persons = computed(() => selectedEntities.value.persons);
const thirdParties = computed(() => selectedEntities.value.thirdParties);
const users = computed(() => selectedEntities.value.users);
const accompanyingCourse = computed<AccompanyingCourse | null>(
() => props.accompanyingCourse,
);
const suggestedEntities = computed<Entities[]>(() => {
return props.suggested;
});
const isComponentVisible = computed(() => {
const activity = getWindowActivity();
return activity
? activity.activityType.personsVisible !== 0 ||
activity.activityType.thirdPartiesVisible !== 0 ||
activity.activityType.usersVisible !== 0
: true;
});
const getContext = computed(() => {
return accompanyingCourse.value ? "accompanyingCourse" : "person";
});
const contextPersonsBlocs = computed(() => {
return personsBlocs.value.filter((bloc) => bloc.included !== false);
});
const pickEntityUniqId = computed(() => props.pickEntityUniqId);
const pickEntityTypes = computed<EntityType[]>(() => {
const optionsType: EntityType[] = [];
const activity = getWindowActivity();
if (activity) {
if (activity.activityType.personsVisible !== 0) {
optionsType.push("person");
}
if (activity.activityType.thirdPartiesVisible !== 0) {
optionsType.push("thirdparty");
}
if (activity.activityType.usersVisible !== 0) {
optionsType.push("user");
}
} else {
optionsType.push("person", "thirdparty", "user");
}
return optionsType;
});
const pickedEntities = computed<Entities[]>(() => {
return [...persons.value, ...thirdParties.value, ...users.value];
});
const getBlocWidth = computed(() => {
return `${Math.round(100 / contextPersonsBlocs.value.length)}%`;
});
const getCourseParticipations = (): Entities[] => {
const participations: Entities[] = [];
accompanyingCourse.value?.participations.forEach(
(participation: Participation) => {
if (!participation.endDate) {
participations.push(participation.person);
}
},
);
return participations;
};
const splitPersonsInGroups = (): {
personsAssociated: Entities[];
personsNotAssociated: Entities[];
} => {
const personsAssociated: Entities[] = [];
let personsNotAssociated = persons.value;
const participations = getCourseParticipations();
persons.value.forEach((person) => {
participations.forEach((participation) => {
if (person.id === participation.id) {
personsAssociated.push(person);
personsNotAssociated = personsNotAssociated.filter((p) => p !== person);
}
});
});
return {
personsAssociated,
personsNotAssociated,
};
};
const setPersonsInBloc = (): void => {
const groups = accompanyingCourse.value ? splitPersonsInGroups() : undefined;
personsBlocs.value.forEach((bloc) => {
if (accompanyingCourse.value && groups) {
switch (bloc.key) {
case "personsAssociated":
bloc.persons = groups.personsAssociated;
bloc.included = true;
break;
case "personsNotAssociated":
bloc.persons = groups.personsNotAssociated;
bloc.included = true;
break;
}
} else if (bloc.key === "persons") {
bloc.persons = persons.value;
bloc.included = true;
}
switch (bloc.key) {
case "thirdparty":
bloc.persons = thirdParties.value;
break;
case "users":
bloc.persons = users.value;
break;
}
});
};
const emitUpdatedModel = (): void => {
emit("update:modelValue", {
persons: [...persons.value],
thirdParties: [...thirdParties.value],
users: [...users.value],
});
};
const addEntityByType = (entity: Entities): void => {
switch (entity.type) {
case "person":
selectedEntities.value.persons.push(entity);
break;
case "thirdparty":
selectedEntities.value.thirdParties.push(entity);
break;
case "user":
selectedEntities.value.users.push(entity);
break;
}
};
const addNewEntityFromPicker = ({ entity }: { entity: EntitiesOrMe }): void => {
if (entity === "me") {
return;
}
addEntityByType(entity);
emitUpdatedModel();
};
const removePersonInvolved = (entity: Entities): void => {
switch (entity.type) {
case "person":
selectedEntities.value.persons = selectedEntities.value.persons.filter(
(person) => person !== entity,
);
break;
case "thirdparty":
selectedEntities.value.thirdParties =
selectedEntities.value.thirdParties.filter(
(thirdParty) => thirdParty !== entity,
);
break;
case "user":
selectedEntities.value.users = selectedEntities.value.users.filter(
(user) => user !== entity,
);
break;
}
emitUpdatedModel();
};
watch(
[persons, thirdParties, users, accompanyingCourse],
() => {
setPersonsInBloc();
},
{ immediate: true, deep: true },
);
</script>
<style lang="scss" scoped></style>

View File

@@ -25,11 +25,11 @@ export default {
components: {
PersonBadge,
},
props: ["bloc", "setPersonsInBloc", "blocWidth"],
props: ["bloc", "setPersonsInBloc", "blocWidth", "removePersonInvolved"],
methods: {
removePerson(item) {
console.log("@@ CLICK remove person: item", item);
this.$store.dispatch("removePersonInvolved", item);
this.removePersonInvolved(item);
this.setPersonsInBloc();
},
},

View File

@@ -3,16 +3,16 @@
<h2 class="chill-red">Utilisateur principal</h2>
<div>
<div>
<div v-if="null !== this.$store.getters.getMainUser">
<calendar-active :user="this.$store.getters.getMainUser" />
<div v-if="null !== $store.getters.getMainUser">
<calendar-active :user="$store.getters.getMainUser" />
</div>
<pick-entity
:multiple="false"
:types="['user']"
:uniqid="'main_user_calendar'"
:picked="
null !== this.$store.getters.getMainUser
? [this.$store.getters.getMainUser]
null !== $store.getters.getMainUser
? [$store.getters.getMainUser]
: []
"
:removable-if-set="false"
@@ -25,7 +25,12 @@
</div>
</teleport>
<concerned-groups />
<concerned-groups
:model-value="concernedGroupsModel"
:suggested="suggestedEntities"
:accompanying-course="activity.accompanyingPeriod"
@update:model-value="updateConcernedGroups"
/>
<teleport to="#schedule">
<div class="row mb-3" v-if="activity.startDate !== null">
@@ -181,6 +186,16 @@ export default {
computed: {
...mapGetters(["getMainUser"]),
...mapState(["activity"]),
concernedGroupsModel() {
return {
persons: this.activity.persons,
thirdParties: this.activity.thirdParties,
users: this.activity.users,
};
},
suggestedEntities() {
return this.$store.getters.suggestedEntities || [];
},
events() {
return this.$store.getters.getEventSources;
},
@@ -241,6 +256,32 @@ export default {
},
},
methods: {
sameEntity(left, right) {
return left.id === right.id && left.type === right.type;
},
syncConcernedGroupType(currentList, nextList) {
nextList.forEach((nextEntity) => {
if (
!currentList.some((current) => this.sameEntity(current, nextEntity))
) {
this.$store.dispatch("addPersonsInvolved", { result: nextEntity });
}
});
currentList.forEach((currentEntity) => {
if (!nextList.some((next) => this.sameEntity(next, currentEntity))) {
this.$store.dispatch("removePersonInvolved", currentEntity);
}
});
},
updateConcernedGroups(value) {
this.syncConcernedGroupType(this.activity.persons, value.persons);
this.syncConcernedGroupType(
this.activity.thirdParties,
value.thirdParties,
);
this.syncConcernedGroupType(this.activity.users, value.users);
},
setMainUser({ entity }) {
const user = entity;
console.log("setMainUser APP", entity);
@@ -275,9 +316,7 @@ export default {
},
removeMainUser(user) {
console.log("removeMainUser APP", user);
window.alert(this.$t("main_user_is_mandatory"));
return;
},
onDatesSet(event) {
console.log("onDatesSet", event);