User: Verify and improve Terms & Conditions and GDPR - refs #5121
	
		
	
				
					
				
			
							parent
							
								
									9b0e6fc6df
								
							
						
					
					
						commit
						bfa7400628
					
				@ -0,0 +1,53 @@ | 
				
			||||
<template> | 
				
			||||
  <div class="base-editor"> | 
				
			||||
    <label v-if="title" :for="editorId">{{ title }}</label> | 
				
			||||
    <TinyEditor | 
				
			||||
      :id="editorId" | 
				
			||||
      :model-value="modelValue" | 
				
			||||
      :init="editorConfig" | 
				
			||||
      :required="required" | 
				
			||||
      @update:model-value="updateValue" | 
				
			||||
      @input="updateValue" | 
				
			||||
    /> | 
				
			||||
    <p v-if="helpText" class="help-text">{{ helpText }}</p> | 
				
			||||
  </div> | 
				
			||||
</template> | 
				
			||||
 | 
				
			||||
<script setup> | 
				
			||||
import { ref, computed, watch } from 'vue' | 
				
			||||
const props = defineProps({ | 
				
			||||
  editorId: String, | 
				
			||||
  modelValue: String, | 
				
			||||
  required: Boolean, | 
				
			||||
  editorConfig: Object, | 
				
			||||
  title: String, | 
				
			||||
  helpText: String | 
				
			||||
}) | 
				
			||||
const emit = defineEmits(['update:modelValue']) | 
				
			||||
const updateValue = (value) => { | 
				
			||||
  emit('update:modelValue', value) | 
				
			||||
  document.getElementById(props.editorId).value = value | 
				
			||||
} | 
				
			||||
const defaultEditorConfig = { | 
				
			||||
  skin_url: '/build/libs/tinymce/skins/ui/oxide', | 
				
			||||
  content_css: '/build/libs/tinymce/skins/content/default/content.css', | 
				
			||||
  branding: false, | 
				
			||||
  relative_urls: false, | 
				
			||||
  height: 280, | 
				
			||||
  toolbar_mode: 'sliding', | 
				
			||||
  file_picker_callback: browser, | 
				
			||||
  autosave_ask_before_unload: true, | 
				
			||||
  plugins: [ | 
				
			||||
    'fullpage advlist autolink lists link image charmap print preview anchor', | 
				
			||||
    'searchreplace visualblocks code fullscreen', | 
				
			||||
    'insertdatetime media table paste wordcount emoticons', | 
				
			||||
  ], | 
				
			||||
  toolbar: 'undo redo | bold italic underline strikethrough | ...', | 
				
			||||
} | 
				
			||||
const editorConfig = computed(() => ({ | 
				
			||||
  ...defaultEditorConfig, | 
				
			||||
  ...props.editorConfig | 
				
			||||
})) | 
				
			||||
function browser(callback, value, meta) { | 
				
			||||
} | 
				
			||||
</script> | 
				
			||||
@ -0,0 +1,18 @@ | 
				
			||||
export default { | 
				
			||||
  path: '/resources/terms-conditions', | 
				
			||||
  meta: { requiresAuth: true }, | 
				
			||||
  name: 'TermsConditions', | 
				
			||||
  component: () => import('../views/terms/TermsLayout.vue'), | 
				
			||||
  children: [ | 
				
			||||
    { | 
				
			||||
      name: 'TermsConditionsList', | 
				
			||||
      path: '', | 
				
			||||
      component: () => import('../views/terms/TermsList.vue') | 
				
			||||
    }, | 
				
			||||
    { | 
				
			||||
      name: 'TermsConditionsEdit', | 
				
			||||
      path: 'edit', | 
				
			||||
      component: () => import('../views/terms/TermsEdit.vue') | 
				
			||||
    } | 
				
			||||
  ] | 
				
			||||
} | 
				
			||||
@ -0,0 +1,20 @@ | 
				
			||||
import makeService from './api' | 
				
			||||
import { ENTRYPOINT } from "../config/entrypoint" | 
				
			||||
 | 
				
			||||
const legalExtensions = { | 
				
			||||
  async findAllAvailable() { | 
				
			||||
    const url = new URL(`${ENTRYPOINT}languages`) | 
				
			||||
    url.searchParams.append("available", "true") | 
				
			||||
    try { | 
				
			||||
      const response = await fetch(url.toString()) | 
				
			||||
      if (!response.ok) { | 
				
			||||
        throw new Error('Network response was not ok') | 
				
			||||
      } | 
				
			||||
      return await response.json() | 
				
			||||
    } catch (error) { | 
				
			||||
      console.error('Error fetching available languages:', error) | 
				
			||||
      throw error | 
				
			||||
    } | 
				
			||||
  }, | 
				
			||||
} | 
				
			||||
export default makeService('languages', legalExtensions) | 
				
			||||
@ -0,0 +1,37 @@ | 
				
			||||
import makeService from './api'; | 
				
			||||
import { ENTRYPOINT } from "../config/entrypoint" | 
				
			||||
 | 
				
			||||
const legalExtensions = { | 
				
			||||
  async findAllByLanguage(languageId) { | 
				
			||||
    const params = new URLSearchParams({ | 
				
			||||
      languageId: languageId, | 
				
			||||
      'order[version]': 'desc' | 
				
			||||
    }); | 
				
			||||
    return fetch(`${ENTRYPOINT}legals?${params.toString()}`); | 
				
			||||
  }, | 
				
			||||
  async saveOrUpdateLegal(payload) { | 
				
			||||
    console.log('Saving or updating legal terms'); | 
				
			||||
    return fetch(`/legal/save`, { | 
				
			||||
      method: 'POST', | 
				
			||||
      headers: { | 
				
			||||
        'Content-Type': 'application/json' | 
				
			||||
      }, | 
				
			||||
      body: JSON.stringify(payload) | 
				
			||||
    }); | 
				
			||||
  }, | 
				
			||||
  async fetchExtraFields(termId = null) { | 
				
			||||
    try { | 
				
			||||
      const url = termId ? `/legal/extra-fields?termId=${termId}` : `/legal/extra-fields`; | 
				
			||||
      const response = await fetch(url); | 
				
			||||
      if (!response.ok) { | 
				
			||||
        throw new Error('Network response was not ok'); | 
				
			||||
      } | 
				
			||||
      return await response.json(); | 
				
			||||
    } catch (error) { | 
				
			||||
      console.error('Error loading extra fields:', error); | 
				
			||||
      throw error; | 
				
			||||
    } | 
				
			||||
  }, | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
export default makeService('legals', legalExtensions); | 
				
			||||
@ -0,0 +1,196 @@ | 
				
			||||
<template> | 
				
			||||
  <div class="terms-edit-view mb-8"> | 
				
			||||
 | 
				
			||||
    <Message severity="info" icon="pi pi-send" :closable="false" class="mt-5"> | 
				
			||||
      {{ t('Display a Terms & Conditions statement on the registration page, require visitor to accept the T&C to register') }} | 
				
			||||
    </Message> | 
				
			||||
 | 
				
			||||
    <BaseToolbar showTopBorder> | 
				
			||||
      <div class="flex justify-between w-full items-center"> | 
				
			||||
        <BaseDropdown | 
				
			||||
          class="w-96 mb-0" | 
				
			||||
          :options="languages" | 
				
			||||
          v-model="selectedLanguage" | 
				
			||||
          optionLabel="name" | 
				
			||||
          placeholder="Select a language" | 
				
			||||
          inputId="language-dropdown" | 
				
			||||
          label="Language" | 
				
			||||
          name="language" | 
				
			||||
        /> | 
				
			||||
        <BaseButton :label="t('Load')" @click="loadTermsByLanguage" icon="search" type="button" class="ml-4"/> | 
				
			||||
        <BaseButton :label="t('All versions')" type="secondary" @click="backToList" icon="back" class="ml-4" /> | 
				
			||||
      </div> | 
				
			||||
    </BaseToolbar> | 
				
			||||
 | 
				
			||||
    <div v-if="termsLoaded"> | 
				
			||||
      <form @submit.prevent="saveTerms"> | 
				
			||||
        <BaseEditor | 
				
			||||
          :editorId="'item_content'" | 
				
			||||
          v-model="termData.content" | 
				
			||||
          :title="t('Personal Data Collection')" | 
				
			||||
        > | 
				
			||||
          <template #help-text> | 
				
			||||
            <p>{{ t('Why do we collect this data?') }}</p> | 
				
			||||
          </template> | 
				
			||||
        </BaseEditor> | 
				
			||||
 | 
				
			||||
 | 
				
			||||
        <BaseRadioButtons | 
				
			||||
          :options="typeOptions" | 
				
			||||
          v-model="termData.type" | 
				
			||||
          name="termsType" | 
				
			||||
          :title="t('Type of Terms')" | 
				
			||||
        /> | 
				
			||||
 | 
				
			||||
        <Dialog v-model:visible="dialogVisible" :style="{ width: '50vw' }" :header="t('Preview')" :modal="true"> | 
				
			||||
          <div v-html="previewContent" /> | 
				
			||||
        </Dialog> | 
				
			||||
 | 
				
			||||
        <!-- Extra fields --> | 
				
			||||
        <div v-for="field in extraFields" :key="field.id" class="extra-field"> | 
				
			||||
          <component :is="getFieldComponent(field.type)" v-bind="field.props" @update:modelValue="field.props.modelValue = $event"> | 
				
			||||
            <template v-if="field.type === 'editor'" #help-text> | 
				
			||||
              <p>{{ field.props.helpText }}</p> | 
				
			||||
            </template> | 
				
			||||
          </component> | 
				
			||||
        </div> | 
				
			||||
 | 
				
			||||
        <BaseTextArea | 
				
			||||
          id="changes" | 
				
			||||
          label="Explain changes" | 
				
			||||
          v-model="termData.changes" | 
				
			||||
        /> | 
				
			||||
 | 
				
			||||
        <div class="form-actions"> | 
				
			||||
          <BaseButton label="Back" type="secondary" @click="backToList" icon="back" class="mr-4" /> | 
				
			||||
          <BaseButton label="Preview" type="primary" @click="previewTerms" icon="search"  class="mr-4" /> | 
				
			||||
          <BaseButton label="Save" type="success" isSubmit icon="save"  class="mr-4" /> | 
				
			||||
        </div> | 
				
			||||
      </form> | 
				
			||||
    </div> | 
				
			||||
  </div> | 
				
			||||
</template> | 
				
			||||
 | 
				
			||||
<script setup> | 
				
			||||
import { onMounted, ref, watch } from "vue" | 
				
			||||
import { useRouter } from "vue-router" | 
				
			||||
import BaseToolbar from "../../components/basecomponents/BaseToolbar.vue" | 
				
			||||
import BaseButton from "../../components/basecomponents/BaseButton.vue" | 
				
			||||
import BaseTextArea from "../../components/basecomponents/BaseTextArea.vue" | 
				
			||||
import Message from "primevue/message" | 
				
			||||
import BaseDropdown from "../../components/basecomponents/BaseDropdown.vue" | 
				
			||||
import BaseRadioButtons from "../../components/basecomponents/BaseRadioButtons.vue" | 
				
			||||
import BaseInputText from "../../components/basecomponents/BaseInputText.vue" | 
				
			||||
import BaseEditor from "../../components/basecomponents/BaseEditor.vue" | 
				
			||||
import { useI18n } from "vue-i18n" | 
				
			||||
import languageService from "../../services/languageService" | 
				
			||||
import legalService from "../../services/legalService" | 
				
			||||
 | 
				
			||||
const { t } = useI18n() | 
				
			||||
 | 
				
			||||
const router = useRouter() | 
				
			||||
const languages = ref([]) | 
				
			||||
const selectedLanguage = ref(null) | 
				
			||||
const termsLoaded = ref(false) | 
				
			||||
const termData = ref({ | 
				
			||||
  language: '', | 
				
			||||
  content: '', | 
				
			||||
  type: '0', | 
				
			||||
  changes: '', | 
				
			||||
}) | 
				
			||||
const dialogVisible = ref(false) | 
				
			||||
const previewContent = ref('') | 
				
			||||
const typeOptions = ref([ | 
				
			||||
  { label: 'HTML', value: '0' }, | 
				
			||||
  { label: 'Page Link', value: '1' } | 
				
			||||
]) | 
				
			||||
const loadTermsByLanguage = async () => { | 
				
			||||
  if (!selectedLanguage.value) return | 
				
			||||
  termsLoaded.value = false | 
				
			||||
  try { | 
				
			||||
    const response = await legalService.findAllByLanguage(selectedLanguage.value.id) | 
				
			||||
    if (response.ok) { | 
				
			||||
      const data = await response.json() | 
				
			||||
      const latestTerm = data['hydra:member'].length ? data['hydra:member'][0] : null | 
				
			||||
      termData.value = latestTerm ? { | 
				
			||||
        id: latestTerm.id, | 
				
			||||
        content: latestTerm.content, | 
				
			||||
        type: latestTerm.type.toString(), | 
				
			||||
        changes: latestTerm.changes, | 
				
			||||
      } : { | 
				
			||||
        content: '', | 
				
			||||
        type: '0', | 
				
			||||
        changes: '', | 
				
			||||
      } | 
				
			||||
      extraFields.value = await legalService.fetchExtraFields(latestTerm ? latestTerm.id : null) | 
				
			||||
    } | 
				
			||||
  } catch (error) { | 
				
			||||
    console.error('Error loading terms:', error) | 
				
			||||
  } finally { | 
				
			||||
    termsLoaded.value = true | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
const saveTerms = async () => { | 
				
			||||
  const payload = { | 
				
			||||
    lang: selectedLanguage.value.id, | 
				
			||||
    content: termData.value.content, | 
				
			||||
    type: termData.value.type.toString(), | 
				
			||||
    changes: termData.value.changes, | 
				
			||||
    extraFields: {}, | 
				
			||||
  } | 
				
			||||
  extraFields.value.forEach(field => { | 
				
			||||
    payload.extraFields[field.id] = field.props.modelValue | 
				
			||||
  }) | 
				
			||||
  try { | 
				
			||||
    const response = await legalService.saveOrUpdateLegal(payload) | 
				
			||||
    if (response.ok) { | 
				
			||||
      await router.push({ name: 'TermsConditionsList' }) | 
				
			||||
    } else { | 
				
			||||
      console.error('Error saving or updating legal terms:', response.statusText) | 
				
			||||
    } | 
				
			||||
  } catch (error) { | 
				
			||||
    console.error('Error when making request:', error) | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
const previewTerms = () => { | 
				
			||||
  previewContent.value = termData.value.content | 
				
			||||
  dialogVisible.value = true | 
				
			||||
} | 
				
			||||
const closePreview = () => { | 
				
			||||
  dialogVisible.value = false | 
				
			||||
} | 
				
			||||
function backToList() { | 
				
			||||
  router.push({ name: 'TermsConditionsList' }) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
const extraFields = ref([]) | 
				
			||||
 | 
				
			||||
function getFieldComponent(type) { | 
				
			||||
  const componentMap = { | 
				
			||||
    text: BaseInputText, | 
				
			||||
    select: BaseDropdown, | 
				
			||||
    editor: BaseEditor, | 
				
			||||
    // Add more mappings as needed | 
				
			||||
  } | 
				
			||||
  return componentMap[type] || 'div' | 
				
			||||
} | 
				
			||||
 | 
				
			||||
watch(selectedLanguage, () => { | 
				
			||||
  termsLoaded.value = false | 
				
			||||
}) | 
				
			||||
onMounted(async () => { | 
				
			||||
  try { | 
				
			||||
    const response = await languageService.findAll() | 
				
			||||
    if (!response.ok) { | 
				
			||||
      throw new Error('Network response was not ok') | 
				
			||||
    } | 
				
			||||
    const data = await response.json() | 
				
			||||
    languages.value = data['hydra:member'].map(lang => ({ | 
				
			||||
      name: lang.englishName, | 
				
			||||
      id: lang.id, | 
				
			||||
    })) | 
				
			||||
  } catch (error) { | 
				
			||||
    console.error('Error loading languages:', error) | 
				
			||||
  } | 
				
			||||
}) | 
				
			||||
</script> | 
				
			||||
@ -0,0 +1,20 @@ | 
				
			||||
<template> | 
				
			||||
  <div class="terms-layout"> | 
				
			||||
    <header> | 
				
			||||
      <h1>{{ t('Terms and conditions') }}</h1> | 
				
			||||
    </header> | 
				
			||||
 | 
				
			||||
    <main> | 
				
			||||
      <router-view /> | 
				
			||||
    </main> | 
				
			||||
 | 
				
			||||
    <footer> | 
				
			||||
    </footer> | 
				
			||||
  </div> | 
				
			||||
</template> | 
				
			||||
 | 
				
			||||
<script setup> | 
				
			||||
import { useI18n } from "vue-i18n" | 
				
			||||
const { t } = useI18n() | 
				
			||||
 | 
				
			||||
</script> | 
				
			||||
@ -0,0 +1,111 @@ | 
				
			||||
<template> | 
				
			||||
  <div class="terms-list-view mt-4"> | 
				
			||||
    <BaseToolbar> | 
				
			||||
        <BaseButton | 
				
			||||
          :label="t('Edit Terms and Conditions')" | 
				
			||||
          icon="edit" | 
				
			||||
          type="primary" | 
				
			||||
          @click="editTerms" | 
				
			||||
        /> | 
				
			||||
    </BaseToolbar> | 
				
			||||
 | 
				
			||||
    <Message severity="warn" :closable="false"> | 
				
			||||
      {{ t('You should create the Term and Conditions for all the available languages.') }} | 
				
			||||
    </Message> | 
				
			||||
 | 
				
			||||
    <DataTable :value="terms" :loading="isLoading"> | 
				
			||||
      <Column field="version" header="Version"></Column> | 
				
			||||
      <Column field="language" header="Language"></Column> | 
				
			||||
 | 
				
			||||
      <Column header="Content"> | 
				
			||||
        <template #body="slotProps"> | 
				
			||||
          <div v-html="slotProps.data.content"></div> | 
				
			||||
        </template> | 
				
			||||
      </Column> | 
				
			||||
 | 
				
			||||
      <Column field="changes" header="Changes"></Column> | 
				
			||||
      <Column field="typeLabel" header="Type"></Column> | 
				
			||||
      <Column field="date"  header="Date"> | 
				
			||||
        <template #body="slotProps"> | 
				
			||||
          {{ formatDate(slotProps.data.date) }} | 
				
			||||
        </template> | 
				
			||||
      </Column> | 
				
			||||
    </DataTable> | 
				
			||||
  </div> | 
				
			||||
</template> | 
				
			||||
 | 
				
			||||
<script setup> | 
				
			||||
import { onMounted, ref } from "vue" | 
				
			||||
import DataTable from "primevue/datatable" | 
				
			||||
import Column from "primevue/column" | 
				
			||||
import BaseToolbar from "../../components/basecomponents/BaseToolbar.vue" | 
				
			||||
import BaseButton from "../../components/basecomponents/BaseButton.vue" | 
				
			||||
import { useI18n } from "vue-i18n" | 
				
			||||
import { useRouter } from "vue-router" | 
				
			||||
import Message from "primevue/message" | 
				
			||||
import languageService from "../../services/languageService" | 
				
			||||
import legalService from "../../services/legalService" | 
				
			||||
 | 
				
			||||
const { t } = useI18n() | 
				
			||||
const router = useRouter() | 
				
			||||
const terms = ref([]) | 
				
			||||
const isLoading = ref(false) | 
				
			||||
async function fetchLanguageName(languageId) { | 
				
			||||
  try { | 
				
			||||
    const response = await languageService.find("/api/languages/" + languageId) | 
				
			||||
    if (response.ok) { | 
				
			||||
      const languageData = await response.json() | 
				
			||||
      return languageData.originalName | 
				
			||||
    } | 
				
			||||
  } catch (error) { | 
				
			||||
    console.error("Error loading language details:", error) | 
				
			||||
  } | 
				
			||||
  return null | 
				
			||||
} | 
				
			||||
 | 
				
			||||
onMounted(async () => { | 
				
			||||
  isLoading.value = true | 
				
			||||
  try { | 
				
			||||
    const response = await legalService.findAll() | 
				
			||||
    if (response.ok) { | 
				
			||||
      const data = await response.json() | 
				
			||||
      terms.value = await Promise.all(data['hydra:member'].map(async (term) => { | 
				
			||||
        const languageName = await fetchLanguageName(term.languageId) | 
				
			||||
        return { | 
				
			||||
          ...term, | 
				
			||||
          language: languageName, | 
				
			||||
          typeLabel: getTypeLabel(term.type), | 
				
			||||
        } | 
				
			||||
      })) | 
				
			||||
    } else { | 
				
			||||
      console.error("The request to the API was not successful:", response.statusText) | 
				
			||||
    } | 
				
			||||
  } catch (error) { | 
				
			||||
    console.error("Error loading legal terms:", error) | 
				
			||||
  } finally { | 
				
			||||
    isLoading.value = false | 
				
			||||
  } | 
				
			||||
}) | 
				
			||||
function getTypeLabel(typeValue) { | 
				
			||||
  const typeMap = { | 
				
			||||
    '0': t('HTML'), | 
				
			||||
    '1': t('Page Link'), | 
				
			||||
  } | 
				
			||||
  return typeMap[typeValue] || 'Unknown' | 
				
			||||
} | 
				
			||||
 | 
				
			||||
function formatDate(timestamp) { | 
				
			||||
  const date = new Date(timestamp * 1000) | 
				
			||||
  const day = date.getDate().toString().padStart(2, '0') | 
				
			||||
  const month = (date.getMonth() + 1).toString().padStart(2, '0') | 
				
			||||
  const year = date.getFullYear() | 
				
			||||
  const hours = date.getHours().toString().padStart(2, '0') | 
				
			||||
  const minutes = date.getMinutes().toString().padStart(2, '0') | 
				
			||||
  const seconds = date.getSeconds().toString().padStart(2, '0') | 
				
			||||
  return `${day}/${month}/${year} ${hours}:${minutes}:${seconds}` | 
				
			||||
} | 
				
			||||
 | 
				
			||||
function editTerms() { | 
				
			||||
  router.push({ name: 'TermsConditionsEdit' }) | 
				
			||||
} | 
				
			||||
</script> | 
				
			||||
@ -0,0 +1,192 @@ | 
				
			||||
<?php | 
				
			||||
 | 
				
			||||
declare(strict_types=1); | 
				
			||||
 | 
				
			||||
/* For licensing terms, see /license.txt */ | 
				
			||||
 | 
				
			||||
namespace Chamilo\CoreBundle\Controller; | 
				
			||||
 | 
				
			||||
use Chamilo\CoreBundle\Entity\Legal; | 
				
			||||
use Chamilo\CoreBundle\Repository\LegalRepository; | 
				
			||||
use Doctrine\ORM\EntityManagerInterface; | 
				
			||||
use ExtraField; | 
				
			||||
use ExtraFieldValue; | 
				
			||||
use LegalManager; | 
				
			||||
use Symfony\Component\HttpFoundation\JsonResponse; | 
				
			||||
use Symfony\Component\HttpFoundation\Request; | 
				
			||||
use Symfony\Component\HttpFoundation\Response; | 
				
			||||
use Symfony\Component\Routing\Annotation\Route; | 
				
			||||
 | 
				
			||||
#[Route('/legal')] | 
				
			||||
class LegalController | 
				
			||||
{ | 
				
			||||
    #[Route('/save', name: 'chamilo_core_legal_save', methods: ['POST'])] | 
				
			||||
    public function saveLegal( | 
				
			||||
        Request $request, | 
				
			||||
        EntityManagerInterface $entityManager, | 
				
			||||
        LegalRepository $legalRepository | 
				
			||||
    ): Response { | 
				
			||||
        $data = json_decode($request->getContent(), true); | 
				
			||||
 | 
				
			||||
        $lang = $data['lang'] ?? null; | 
				
			||||
        $content = $data['content'] ?? null; | 
				
			||||
        $type = isset($data['type']) ? (int)$data['type'] : null; | 
				
			||||
        $changes = $data['changes'] ?? ''; | 
				
			||||
        $extraFields = $data['extraFields'] ?? []; | 
				
			||||
 | 
				
			||||
        $lastLegal = $legalRepository->findLastConditionByLanguage($lang); | 
				
			||||
        $extraFieldValue = new ExtraFieldValue('terms_and_condition'); | 
				
			||||
 | 
				
			||||
        $newVersionRequired = !$lastLegal || $lastLegal->getContent() !== $content || $this->hasExtraFieldsChanged($extraFieldValue, $lastLegal->getId(), $extraFields); | 
				
			||||
        $typeUpdateRequired = $lastLegal && $lastLegal->getType() !== $type; | 
				
			||||
 | 
				
			||||
        $legalToUpdate = $lastLegal; | 
				
			||||
        if ($newVersionRequired) { | 
				
			||||
            $legal = new Legal(); | 
				
			||||
            $legal->setLanguageId($lang); | 
				
			||||
            $legal->setContent($content); | 
				
			||||
            $legal->setType($type); | 
				
			||||
            $legal->setChanges($changes); | 
				
			||||
            $legal->setDate(time()); | 
				
			||||
            $version = $lastLegal ? $lastLegal->getVersion() + 1 : 1; | 
				
			||||
            $legal->setVersion($version); | 
				
			||||
 | 
				
			||||
            $entityManager->persist($legal); | 
				
			||||
            $legalToUpdate = $legal; | 
				
			||||
        } elseif ($typeUpdateRequired) { | 
				
			||||
            $lastLegal->setType($type); | 
				
			||||
            $lastLegal->setChanges($changes); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        $entityManager->flush(); | 
				
			||||
 | 
				
			||||
        if ($newVersionRequired || $typeUpdateRequired) { | 
				
			||||
            $this->updateExtraFields($extraFieldValue, $legalToUpdate->getId(), $extraFields); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        return new Response('Term and condition saved or updated successfully', Response::HTTP_OK); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    #[Route('/extra-fields', name: 'chamilo_core_get_extra_fields')] | 
				
			||||
    public function getExtraFields(Request $request): JsonResponse | 
				
			||||
    { | 
				
			||||
 | 
				
			||||
        $extraField = new ExtraField('terms_and_condition'); | 
				
			||||
        $types = LegalManager::getTreatmentTypeList(); | 
				
			||||
 | 
				
			||||
        foreach ($types as $variable => $name) { | 
				
			||||
            $label = 'PersonalData'.ucfirst($name).'Title'; | 
				
			||||
            $params = [ | 
				
			||||
                'variable' => $variable, | 
				
			||||
                'display_text' => $label, | 
				
			||||
                'value_type' => ExtraField::FIELD_TYPE_TEXTAREA, | 
				
			||||
                'default_value' => '', | 
				
			||||
                'visible' => true, | 
				
			||||
                'changeable' => true, | 
				
			||||
                'filter' => true, | 
				
			||||
                'visible_to_self' => true, | 
				
			||||
                'visible_to_others' => true, | 
				
			||||
            ]; | 
				
			||||
            $extraField->save($params); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        $termId = $request->query->get('termId'); | 
				
			||||
        $extraData = $extraField->get_handler_extra_data($termId ?? 0); | 
				
			||||
        $extraFieldsDefinition = $extraField->get_all(); | 
				
			||||
        $fieldsData = $extraField->getExtraFieldsData( | 
				
			||||
            $extraData, | 
				
			||||
            true, | 
				
			||||
            $extraFieldsDefinition | 
				
			||||
        ); | 
				
			||||
 | 
				
			||||
        $prefix = 'extra_'; | 
				
			||||
        $extraFields = []; | 
				
			||||
        foreach ($fieldsData as $field) { | 
				
			||||
            $fieldType = $this->mapFieldType($field['type']); | 
				
			||||
            $extraField = [ | 
				
			||||
                'id' => $prefix.$field['variable'], | 
				
			||||
                'type' => $fieldType, | 
				
			||||
                'props' => [ | 
				
			||||
                    'title' => $field['title'], | 
				
			||||
                    'defaultValue' => $field['defaultValue'], | 
				
			||||
                ], | 
				
			||||
            ]; | 
				
			||||
 | 
				
			||||
            switch ($fieldType) { | 
				
			||||
                case 'editor': | 
				
			||||
                    $extraField['props']['editorId'] = $prefix.$field['variable']; | 
				
			||||
                    $extraField['props']['modelValue'] = $field['value'] ?? ''; | 
				
			||||
                    $extraField['props']['helpText'] = 'Specific help text for ' . $field['title']; | 
				
			||||
                    break; | 
				
			||||
                case 'text': | 
				
			||||
                    $extraField['props']['label'] = $field['title']; | 
				
			||||
                    $extraField['props']['modelValue'] = $field['value'] ?? ''; | 
				
			||||
                    break; | 
				
			||||
                case 'select': | 
				
			||||
                    $extraField['props']['label'] = $field['title']; | 
				
			||||
                    $extraField['props']['options'] = []; | 
				
			||||
                    $extraField['props']['modelValue'] = $field['value'] ?? ''; | 
				
			||||
                    break; | 
				
			||||
            } | 
				
			||||
 | 
				
			||||
            $extraFields[] = $extraField; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        return new JsonResponse($extraFields); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /** | 
				
			||||
     * Checks if the extra field values have changed. | 
				
			||||
     * | 
				
			||||
     * This function compares the new values for extra fields against the old ones to determine | 
				
			||||
     * if there have been any changes. It is useful for triggering events or updates only when | 
				
			||||
     * actual changes to data occur. | 
				
			||||
     */ | 
				
			||||
    private function hasExtraFieldsChanged(ExtraFieldValue $extraFieldValue, int $legalId, array $newValues): bool | 
				
			||||
    { | 
				
			||||
        $oldValues = $extraFieldValue->getAllValuesByItem($legalId); | 
				
			||||
        $oldValues = array_column($oldValues, 'value', 'variable'); | 
				
			||||
 | 
				
			||||
        foreach ($newValues as $key => $newValue) { | 
				
			||||
            if (isset($oldValues[$key]) && $newValue != $oldValues[$key]) { | 
				
			||||
                return true; | 
				
			||||
            } elseif (!isset($oldValues[$key])) { | 
				
			||||
                return true; | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
        return false; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /** | 
				
			||||
     * Updates the extra fields with new values for a specific item. | 
				
			||||
     */ | 
				
			||||
    private function updateExtraFields(ExtraFieldValue $extraFieldValue, int $legalId, array $values): void | 
				
			||||
    { | 
				
			||||
        $values['item_id'] = $legalId; | 
				
			||||
        $extraFieldValue->saveFieldValues($values); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /** | 
				
			||||
     * Maps an integer representing a field type to its corresponding string value. | 
				
			||||
     */ | 
				
			||||
    private function mapFieldType(int $type): string | 
				
			||||
    { | 
				
			||||
        switch ($type) { | 
				
			||||
            case ExtraField::FIELD_TYPE_TEXT: | 
				
			||||
                return 'text'; | 
				
			||||
            case ExtraField::FIELD_TYPE_TEXTAREA: | 
				
			||||
                return 'editor'; | 
				
			||||
            case ExtraField::FIELD_TYPE_SELECT_MULTIPLE: | 
				
			||||
            case ExtraField::FIELD_TYPE_DATE: | 
				
			||||
            case ExtraField::FIELD_TYPE_DATETIME: | 
				
			||||
            case ExtraField::FIELD_TYPE_DOUBLE_SELECT: | 
				
			||||
            case ExtraField::FIELD_TYPE_RADIO: | 
				
			||||
                // Manage as needed | 
				
			||||
                break; | 
				
			||||
            case ExtraField::FIELD_TYPE_SELECT: | 
				
			||||
                return 'select'; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        return 'text'; | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
					Loading…
					
					
				
		Reference in new issue