Compare commits

..

11 Commits

Author SHA1 Message Date
724e6c7365 Setup base for refactoring addPersons.vue 2025-02-13 17:34:59 +01:00
a8f8c23027 Add (temporary) types in Main and ThirdpartyBundle 2025-02-13 16:43:39 +01:00
cb7e2d752e Create types 2025-02-11 17:51:29 +01:00
1019a7bcd4 Improve merge service according to specifications 2025-02-11 16:46:44 +01:00
9b0ae7198a WIP merge service 2025-02-11 15:06:04 +01:00
30e7009178 First commit - changie for feature 2025-02-11 14:23:05 +01:00
ab35e8c034 Release v3.8.2 2025-02-10 14:52:22 +01:00
2aded2974f Merge branch '358-remove-filter-button' into 'master'
Remove "filter" button from attachment modal in workflows

Closes #358

See merge request Chill-Projet/chill-bundles!794
2025-02-10 13:49:09 +00:00
f84c1632b2 Remove "filter" button from attachment modal in workflows
The "filter" button was unnecessary in the document list within the "add attachment" modal and has been removed for simplicity. This change does not involve any schema modifications and resolves issue #358.
2025-02-10 14:41:53 +01:00
99e4824137 Release bundles v.3.8.1 2025-02-05 18:14:22 +01:00
dacaaea235 Fix household link in parcours banner 2025-02-05 18:09:34 +01:00
16 changed files with 436 additions and 15 deletions

View File

@@ -0,0 +1,6 @@
kind: Feature
body: Allow the merge of two accompanying period works
time: 2025-02-11T14:22:43.134106669+01:00
custom:
Issue: "359"
SchemaChange: No schema change

3
.changes/v3.8.1.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.8.1 - 2025-02-05
### Fixed
* Fix household link in the parcours banner

3
.changes/v3.8.2.md Normal file
View File

@@ -0,0 +1,3 @@
## v3.8.2 - 2025-02-10
### Fixed
* ([#358](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/358)) Remove "filter" button on list of documents in the workflow's "add attachement" modal

View File

@@ -6,6 +6,14 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
and is generated by [Changie](https://github.com/miniscruff/changie).
## v3.8.2 - 2025-02-10
### Fixed
* ([#358](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/358)) Remove "filter" button on list of documents in the workflow's "add attachement" modal
## v3.8.1 - 2025-02-05
### Fixed
* Fix household link in the parcours banner
## v3.8.0 - 2025-02-03
### Feature
* Improve the UX of the news item admin form to prevent wrong usage

View File

@@ -203,3 +203,5 @@ export interface WorkflowAttachment {
updatedBy: User | null;
genericDoc: null | GenericDoc;
}
export type PrivateCommentEmbeddable = any;

View File

@@ -232,19 +232,8 @@ const filteredDocuments = computed<GenericDocForAccompanyingPeriod[]>(() => {
</div>
</div>
</div>
<div class="row my-2">
<button
type="submit"
class="btn btn-sm btn-misc"
>
<i class="fa fa-fw fa-filter"></i>Filtrer
</button>
</div>
</div>
</div>
<div></div>
</div>
</form>
</div>

View File

@@ -7,6 +7,7 @@ import {
WorkflowAvailable,
} from "../../../ChillMainBundle/Resources/public/types";
import { StoredObject } from "../../../ChillDocStoreBundle/Resources/public/types";
import {Thirdparty} from "../../../ChillThirdPartyBundle/Resources/public/types";
export interface Person {
id: number;
@@ -41,3 +42,51 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
workflows_availables: WorkflowAvailable[];
workflows: object[];
}
export interface AccompanyingPeriodWork {
id?: number;
accompanyingPeriod?: AccompanyingPeriod;
accompanyingPeriodWorkEvaluations: AccompanyingPeriodWorkEvaluation[];
createdAt?: string;
createdAutomatically: boolean;
createdAutomaticallyReason: string;
createdBy: User;
endDate?: string;
goals: AccompanyingPeriodWorkGoal[];
handlingThierParty?: Thirdparty;
note: string;
persons: Person[];
privateComment: PrivateCommentEmbeddable;
referrersHistory: AccompanyingPeriodWorkReferrerHistory[];
results: Result[];
socialAction?: SocialAction;
startDate?: string;
thirdParties: Thirdparty[];
updatedAt?: string;
updatedBy: User;
version: number;
}
interface SocialAction {
id?: number;
parent?: SocialAction | null;
children: SocialAction[];
issue?: SocialIssue | null;
ordering: number;
title: Record<string, string>;
defaultNotificationDelay?: string | null;
desactivationDate?: string | null;
evaluations: Evaluation[];
goals: Goal[];
results: Result[];
}
type SocialIssue = any;
type Goal = any;
type Result = any;
type Evaluation = any;
type AccompanyingPeriod = any;
type AccompanyingPeriodWorkEvaluation = any;
type AccompanyingPeriodWorkGoal = any;
type PrivateCommentEmbeddable = any;
type AccompanyingPeriodWorkReferrerHistory = any;

View File

@@ -25,10 +25,10 @@
:class="{ household: pk > -1, 'no-household': pk === -1 }"
:key="h.id"
>
<a v-if="pk !== -1" :href="householdLink(h)">
<a v-if="pk !== -1" :href="householdLink(pk)">
<i
class="fa fa-home fa-fw text-light"
:title="$t('persons_associated.show_household_number', { id: h })"
:title="$t('persons_associated.show_household_number', { id: pk })"
></i>
</a>
<span v-for="person in persons" class="me-1" :key="person.id">
@@ -87,8 +87,8 @@ export default {
personsByHousehold() {
const households = new Map();
this.accompanyingCourse.participations
.filter((p) => p.endDate === null)
.map((p) => p.person)
.filter((part) => part.endDate === null)
.map((part) => part.person)
.forEach((person) => {
if (!households.has(person.current_household_id || -1)) {
households.set(person.current_household_id || -1, []);

View File

@@ -0,0 +1,52 @@
<template>
<div>
<h2>Add {{ entityType }}</h2>
<label for="entityType">Select type:</label>
<select v-model="entityType">
<option value="person">Person</option>
<option value="thirdparty">Thirdparty</option>
</select>
<SearchEntity :entityType="entityType" @entity-selected="handleEntitySelection" />
<PersonDetails v-if="entityType === 'person' && selectedEntity" :person="selectedEntity" />
<ThirdpartyDetails v-if="entityType === 'thirdparty' && selectedEntity" :thirdparty="selectedEntity" />
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
import SearchEntity from '@/components/SearchEntity.vue';
import PersonDetails from '@/components/PersonDetails.vue';
import ThirdpartyDetails from '@/components/ThirdpartyDetails.vue';
export default {
components: {
SearchEntity,
PersonDetails,
ThirdpartyDetails
},
data() {
return {
entityType: 'person'
};
},
computed: {
...mapState('entities', ['selectedEntity'])
},
methods: {
...mapMutations('entities', ['SET_SELECTED_ENTITY']),
handleEntitySelection(entity) {
this.SET_SELECTED_ENTITY(entity);
}
}
};
</script>
<style scoped>
select {
margin: 10px 0;
padding: 5px;
}
</style>

View File

@@ -0,0 +1,25 @@
<template>
<div v-if="person">
<h2>{{ person.name }}</h2>
<p><strong>Email:</strong> {{ person.email }}</p>
<p><strong>Phone:</strong> {{ person.phone }}</p>
<div v-if="person.household">
<h3>Household</h3>
<ul>
<li v-for="member in person.household" :key="member.id">{{ member.name }}</li>
</ul>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
const person = computed(() => store.state.selectedEntity);
</script>
<style scoped>
h2 { margin-bottom: 8px; }
</style>

View File

@@ -0,0 +1,64 @@
<template>
<div>
<label for="search">Search {{ entityType }}:</label>
<input
id="search"
v-model="query"
@input="debouncedSearch"
placeholder="Type to search..."
/>
<ul v-if="results.length">
<li v-for="result in results" :key="result.id" @click="selectEntity(result)">
{{ result.name }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue';
import { useStore } from 'vuex';
import debounce from 'lodash.debounce';
const props = defineProps({
entityType: {
type: String,
required: true,
validator: (value) => ['Person', 'Thirdparty'].includes(value),
},
});
const store = useStore();
const query = ref('');
const results = computed(() => store.state.entity.searchResults);
const searchEntities = () => {
if (query.value.trim().length > 2) {
store.dispatch('entity/searchEntities', { type: props.entityType, query: query.value });
}
};
const debouncedSearch = debounce(searchEntities, 300);
const selectEntity = (entity) => {
store.dispatch('entity/selectEntity', entity);
};
</script>
<style scoped>
ul {
list-style: none;
padding: 0;
margin-top: 5px;
}
li {
cursor: pointer;
padding: 5px;
background: #f5f5f5;
margin: 2px 0;
}
li:hover {
background: #e0e0e0;
}
</style>

View File

@@ -0,0 +1,7 @@
import { createApp } from 'vue';
import store from './store/entities';
import AddEntity from './components/AddEntity.vue';
const app = createApp(AddEntity);
app.use(store);
app.mount('#add-entity-app');

View File

@@ -0,0 +1,37 @@
export default {
namespaced: true,
state: {
selectedEntity: null,
persons: [],
thirdparties: [],
accompanyingPeriodWorks: []
},
mutations: {
SET_SELECTED_ENTITY(state, entity) {
state.selectedEntity = entity;
},
SET_PERSONS(state, persons) {
state.persons = persons;
},
SET_THIRDPARTIES(state, thirdparties) {
state.thirdparties = thirdparties;
},
SET_ACCOMPANYING_PERIOD_WORKS(state, works) {
state.accompanyingPeriodWorks = works;
}
},
actions: {
async fetchPersons({ commit }, query) {
const response = await api.get(`/persons?search=${query}`);
commit('SET_PERSONS', response.data);
},
async fetchThirdparties({ commit }, query) {
const response = await api.get(`/thirdparties?search=${query}`);
commit('SET_THIRDPARTIES', response.data);
},
async fetchAccompanyingPeriodWorks({ commit }, personId) {
const response = await api.get(`/persons/${personId}/accompanying-period-works`);
commit('SET_ACCOMPANYING_PERIOD_WORKS', response.data);
}
}
};

View File

@@ -0,0 +1,174 @@
<?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\PersonBundle\Service\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Doctrine\DBAL\Exception;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
class AccompanyingPeriodWorkMergeService
{
public function __construct(private readonly EntityManagerInterface $em) {}
/**
* @throws Exception
*/
public function merge(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void
{
// Transfer non-duplicate data
$this->transferData($toKeep, $toDelete);
// Update linked entities
$this->updateReferences($toKeep, $toDelete);
$this->em->remove($toDelete);
$this->em->flush();
}
/**
* @throws Exception
*/
private function transferData(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void
{
$conn = $this->em->getConnection();
$sqlStatements = [
$this->generateStartDateSQL(),
$this->generateEndDateSQL(),
$this->generateCommentSQL(),
];
$conn->beginTransaction();
try {
foreach ($sqlStatements as $sql) {
$conn->executeQuery($sql, ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()]);
}
$conn->commit();
} catch (\Exception $e) {
$conn->rollBack();
throw $e;
}
}
private function generateStartDateSQL(): string
{
return '
UPDATE chill_person_accompanying_period_work
SET startdate = LEAST(
COALESCE((SELECT startdate FROM chill_person_accompanying_period_work WHERE id = :toDelete), startdate),
startdate
)
WHERE id = :toKeep';
}
private function generateEndDateSQL(): string
{
return '
UPDATE chill_person_accompanying_period_work
SET enddate =
CASE
WHEN (SELECT enddate FROM chill_person_accompanying_period_work WHERE id = :toDelete) IS NULL
OR enddate IS NULL
THEN NULL
ELSE GREATEST(
COALESCE((SELECT enddate FROM chill_person_accompanying_period_work WHERE id = :toDelete), enddate),
enddate
)
END
WHERE id = :toKeep';
}
private function generateCommentSQL(): string
{
return "
UPDATE chill_person_accompanying_period_work
SET note = CONCAT_WS(
'\n',
NULLIF(TRIM(note), ''),
NULLIF(TRIM((SELECT note FROM chill_person_accompanying_period_work WHERE id = :toDelete)), '')
)
WHERE id = :toKeep";
}
/**
* @throws Exception
*/
private function updateReferences(AccompanyingPeriodWork $toKeep, AccompanyingPeriodWork $toDelete): void
{
$allMeta = $this->em->getMetadataFactory()->getAllMetadata();
$conn = $this->em->getConnection();
$sqlStatements = [];
foreach ($allMeta as $meta) {
if ($meta->isMappedSuperclass) {
continue;
}
foreach ($meta->getAssociationMappings() as $assoc) {
if (AccompanyingPeriodWork::class !== $assoc['targetEntity']) {
continue;
}
if ($assoc['type'] & ClassMetadata::TO_ONE) {
if ('handlingThirdparty' === $assoc['fieldName']) {
continue;
}
$sqlStatements[] = $this->generateToOneUpdateQuery($meta, $assoc);
}
if ($assoc['type'] & ClassMetadata::TO_MANY) {
if (!isset($assoc['joinTable'])) {
continue;
}
$sqlStatements = array_merge($sqlStatements, $this->generateToManyUpdateQueries($assoc));
}
}
}
$conn->beginTransaction();
try {
foreach ($sqlStatements as $sql) {
$conn->executeStatement($sql, ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()]);
}
$conn->commit();
} catch (\Exception $e) {
$conn->rollBack();
throw $e;
}
}
private function generateToOneUpdateQuery(ClassMetadata $meta, array $assoc): string
{
$tableName = $meta->getTableName();
$joinColumn = $meta->getSingleAssociationJoinColumnName($assoc['fieldName']);
return "UPDATE {$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete";
}
private function generateToManyUpdateQueries(array $assoc): array
{
$sqls = [];
$joinTable = $assoc['joinTable']['name'];
$owningColumn = $assoc['joinTable']['joinColumns'][0]['name'];
$inverseColumn = $assoc['joinTable']['inverseJoinColumns'][0]['name'];
// Insert relations, skip already existing ones
$sqls[] = "INSERT IGNORE INTO {$joinTable} ({$owningColumn}, {$inverseColumn})
SELECT :toKeep, {$inverseColumn} FROM {$joinTable} WHERE {$owningColumn} = :toDelete";
// Delete old references
$sqls[] = "DELETE FROM {$joinTable} WHERE {$owningColumn} = :toDelete";
return $sqls;
}
}

View File

@@ -0,0 +1 @@
export type Thirdparty = any;

View File

@@ -0,0 +1 @@
export type ThirdParty = any;