mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-10 08:44:58 +00:00
Compare commits
6 Commits
362-bug-ma
...
refactor_a
Author | SHA1 | Date | |
---|---|---|---|
724e6c7365 | |||
a8f8c23027 | |||
cb7e2d752e | |||
1019a7bcd4 | |||
9b0ae7198a | |||
30e7009178 |
6
.changes/unreleased/Feature-20250211-142243.yaml
Normal file
6
.changes/unreleased/Feature-20250211-142243.yaml
Normal 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
|
@@ -203,3 +203,5 @@ export interface WorkflowAttachment {
|
||||
updatedBy: User | null;
|
||||
genericDoc: null | GenericDoc;
|
||||
}
|
||||
|
||||
export type PrivateCommentEmbeddable = any;
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -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');
|
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -0,0 +1 @@
|
||||
export type Thirdparty = any;
|
@@ -0,0 +1 @@
|
||||
export type ThirdParty = any;
|
Reference in New Issue
Block a user