mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-31 09:18:24 +00:00 
			
		
		
		
	Compare commits
	
		
			11 Commits
		
	
	
		
			v3.8.0
			...
			refactor_a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 724e6c7365 | |||
| a8f8c23027 | |||
| cb7e2d752e | |||
| 1019a7bcd4 | |||
| 9b0ae7198a | |||
| 30e7009178 | |||
| ab35e8c034 | |||
| 2aded2974f | |||
| f84c1632b2 | |||
| 99e4824137 | |||
| dacaaea235 | 
							
								
								
									
										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 | ||||
							
								
								
									
										3
									
								
								.changes/v3.8.1.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.changes/v3.8.1.md
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										3
									
								
								.changes/v3.8.2.md
									
									
									
									
									
										Normal 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    | ||||
| @@ -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 | ||||
|   | ||||
| @@ -203,3 +203,5 @@ export interface WorkflowAttachment { | ||||
|     updatedBy: User | null; | ||||
|     genericDoc: null | GenericDoc; | ||||
| } | ||||
|  | ||||
| export type PrivateCommentEmbeddable = any; | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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, []); | ||||
|   | ||||
| @@ -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